1#!/usr/local/bin/python3.8
2from __future__ import (absolute_import, division, print_function)
3# Copyright 2019-2020 Fortinet, Inc.
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
18__metaclass__ = type
19
20ANSIBLE_METADATA = {'status': ['preview'],
21                    'supported_by': 'community',
22                    'metadata_version': '1.1'}
23
24DOCUMENTATION = '''
25---
26module: fortios_wireless_controller_address
27short_description: Configure the client with its MAC address in Fortinet's FortiOS and FortiGate.
28description:
29    - This module is able to configure a FortiGate or FortiOS (FOS) device by allowing the
30      user to set and modify wireless_controller feature and address category.
31      Examples include all parameters and values need to be adjusted to datasources before usage.
32      Tested with FOS v6.0.0
33version_added: "2.10"
34author:
35    - Link Zheng (@chillancezen)
36    - Jie Xue (@JieX19)
37    - Hongbin Lu (@fgtdev-hblu)
38    - Frank Shen (@frankshen01)
39    - Miguel Angel Munoz (@mamunozgonzalez)
40    - Nicolas Thomas (@thomnico)
41notes:
42    - Legacy fortiosapi has been deprecated, httpapi is the preferred way to run playbooks
43
44requirements:
45    - ansible>=2.9.0
46options:
47    access_token:
48        description:
49            - Token-based authentication.
50              Generated from GUI of Fortigate.
51        type: str
52        required: false
53    enable_log:
54        description:
55            - Enable/Disable logging for task.
56        type: bool
57        required: false
58        default: false
59    vdom:
60        description:
61            - Virtual domain, among those defined previously. A vdom is a
62              virtual instance of the FortiGate that can be configured and
63              used as a different unit.
64        type: str
65        default: root
66
67    state:
68        description:
69            - Indicates whether to create or remove the object.
70        type: str
71        required: true
72        choices:
73            - present
74            - absent
75    wireless_controller_address:
76        description:
77            - Configure the client with its MAC address.
78        default: null
79        type: dict
80        suboptions:
81            id:
82                description:
83                    - ID.
84                required: true
85                type: str
86            mac:
87                description:
88                    - MAC address.
89                type: str
90            policy:
91                description:
92                    - Allow or block the client with this MAC address.
93                type: str
94                choices:
95                    - allow
96                    - deny
97'''
98
99EXAMPLES = '''
100- hosts: fortigates
101  collections:
102    - fortinet.fortios
103  connection: httpapi
104  vars:
105   vdom: "root"
106   ansible_httpapi_use_ssl: yes
107   ansible_httpapi_validate_certs: no
108   ansible_httpapi_port: 443
109  tasks:
110  - name: Configure the client with its MAC address.
111    fortios_wireless_controller_address:
112      vdom:  "{{ vdom }}"
113      state: "present"
114      access_token: "<your_own_value>"
115      wireless_controller_address:
116        id:  "3"
117        mac: "<your_own_value>"
118        policy: "allow"
119
120'''
121
122RETURN = '''
123build:
124  description: Build number of the fortigate image
125  returned: always
126  type: str
127  sample: '1547'
128http_method:
129  description: Last method used to provision the content into FortiGate
130  returned: always
131  type: str
132  sample: 'PUT'
133http_status:
134  description: Last result given by FortiGate on last operation applied
135  returned: always
136  type: str
137  sample: "200"
138mkey:
139  description: Master key (id) used in the last call to FortiGate
140  returned: success
141  type: str
142  sample: "id"
143name:
144  description: Name of the table used to fulfill the request
145  returned: always
146  type: str
147  sample: "urlfilter"
148path:
149  description: Path of the table used to fulfill the request
150  returned: always
151  type: str
152  sample: "webfilter"
153revision:
154  description: Internal revision number
155  returned: always
156  type: str
157  sample: "17.0.2.10658"
158serial:
159  description: Serial number of the unit
160  returned: always
161  type: str
162  sample: "FGVMEVYYQT3AB5352"
163status:
164  description: Indication of the operation's result
165  returned: always
166  type: str
167  sample: "success"
168vdom:
169  description: Virtual domain used
170  returned: always
171  type: str
172  sample: "root"
173version:
174  description: Version of the FortiGate
175  returned: always
176  type: str
177  sample: "v5.6.3"
178
179'''
180from ansible.module_utils.basic import AnsibleModule
181from ansible.module_utils.connection import Connection
182from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import FortiOSHandler
183from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_legacy_fortiosapi
184from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import schema_to_module_spec
185from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_schema_versioning
186from ansible_collections.fortinet.fortios.plugins.module_utils.fortimanager.common import FAIL_SOCKET_MSG
187from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import is_same_comparison
188from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import serialize
189
190
191def filter_wireless_controller_address_data(json):
192    option_list = ['id', 'mac', 'policy']
193    dictionary = {}
194
195    for attribute in option_list:
196        if attribute in json and json[attribute] is not None:
197            dictionary[attribute] = json[attribute]
198
199    return dictionary
200
201
202def underscore_to_hyphen(data):
203    if isinstance(data, list):
204        for i, elem in enumerate(data):
205            data[i] = underscore_to_hyphen(elem)
206    elif isinstance(data, dict):
207        new_data = {}
208        for k, v in data.items():
209            new_data[k.replace('_', '-')] = underscore_to_hyphen(v)
210        data = new_data
211
212    return data
213
214
215def wireless_controller_address(data, fos, check_mode=False):
216
217    vdom = data['vdom']
218
219    state = data['state']
220
221    wireless_controller_address_data = data['wireless_controller_address']
222    filtered_data = underscore_to_hyphen(filter_wireless_controller_address_data(wireless_controller_address_data))
223
224    # check_mode starts from here
225    if check_mode:
226        mkey = fos.get_mkey('system', 'interface', filtered_data, vdom=vdom)
227        current_data = fos.get('system', 'interface', vdom=vdom, mkey=mkey)
228        is_existed = current_data and current_data.get('http_status') == 200 \
229            and isinstance(current_data.get('results'), list) \
230            and len(current_data['results']) > 0
231
232        # 2. if it exists and the state is 'present' then compare current settings with desired
233        if state == 'present' or state is True:
234            if mkey is None:
235                return False, True, filtered_data
236
237            # if mkey exists then compare each other
238            # record exits and they're matched or not
239            if is_existed:
240                is_same = is_same_comparison(
241                    serialize(current_data['results'][0]), serialize(filtered_data))
242                return False, not is_same, filtered_data
243
244            # record does not exist
245            return False, True, filtered_data
246
247        if state == 'absent':
248            if mkey is None:
249                return False, False, filtered_data
250
251            if is_existed:
252                return False, True, filtered_data
253            return False, False, filtered_data
254
255        return True, False, {'reason: ': 'Must provide state parameter'}
256
257    if state == "present" or state is True:
258        return fos.set('wireless-controller',
259                       'address',
260                       data=filtered_data,
261                       vdom=vdom)
262
263    elif state == "absent":
264        return fos.delete('wireless-controller',
265                          'address',
266                          mkey=filtered_data['id'],
267                          vdom=vdom)
268    else:
269        fos._module.fail_json(msg='state must be present or absent!')
270
271
272def is_successful_status(status):
273    return status['status'] == "success" or \
274        status['http_method'] == "DELETE" and status['http_status'] == 404
275
276
277def fortios_wireless_controller(data, fos, check_mode):
278
279    if data['wireless_controller_address']:
280        resp = wireless_controller_address(data, fos, check_mode)
281    else:
282        fos._module.fail_json(msg='missing task body: %s' % ('wireless_controller_address'))
283    if check_mode:
284        return resp
285    return not is_successful_status(resp), \
286        resp['status'] == "success" and \
287        (resp['revision_changed'] if 'revision_changed' in resp else True), \
288        resp
289
290
291versioned_schema = {
292    "type": "list",
293    "children": {
294        "policy": {
295            "type": "string",
296            "options": [
297                {
298                    "value": "allow",
299                    "revisions": {
300                        "v7.0.0": True,
301                        "v6.4.4": True,
302                        "v6.4.0": True,
303                        "v6.4.1": True,
304                        "v6.2.0": True,
305                        "v6.2.3": True,
306                        "v6.2.5": True,
307                        "v6.2.7": True
308                    }
309                },
310                {
311                    "value": "deny",
312                    "revisions": {
313                        "v7.0.0": True,
314                        "v6.4.4": True,
315                        "v6.4.0": True,
316                        "v6.4.1": True,
317                        "v6.2.0": True,
318                        "v6.2.3": True,
319                        "v6.2.5": True,
320                        "v6.2.7": True
321                    }
322                }
323            ],
324            "revisions": {
325                "v7.0.0": True,
326                "v6.4.4": True,
327                "v6.4.0": True,
328                "v6.4.1": True,
329                "v6.2.0": True,
330                "v6.2.3": True,
331                "v6.2.5": True,
332                "v6.2.7": True
333            }
334        },
335        "mac": {
336            "type": "string",
337            "revisions": {
338                "v7.0.0": True,
339                "v6.4.4": True,
340                "v6.4.0": True,
341                "v6.4.1": True,
342                "v6.2.0": True,
343                "v6.2.3": True,
344                "v6.2.5": True,
345                "v6.2.7": True
346            }
347        },
348        "id": {
349            "type": "string",
350            "revisions": {
351                "v7.0.0": True,
352                "v6.4.4": True,
353                "v6.4.0": True,
354                "v6.4.1": True,
355                "v6.2.0": True,
356                "v6.2.3": True,
357                "v6.2.5": True,
358                "v6.2.7": True
359            }
360        }
361    },
362    "revisions": {
363        "v7.0.0": True,
364        "v6.4.4": True,
365        "v6.4.0": True,
366        "v6.4.1": True,
367        "v6.2.0": True,
368        "v6.2.3": True,
369        "v6.2.5": True,
370        "v6.2.7": True
371    }
372}
373
374
375def main():
376    module_spec = schema_to_module_spec(versioned_schema)
377    mkeyname = 'id'
378    fields = {
379        "access_token": {"required": False, "type": "str", "no_log": True},
380        "enable_log": {"required": False, "type": bool},
381        "vdom": {"required": False, "type": "str", "default": "root"},
382        "state": {"required": True, "type": "str",
383                  "choices": ["present", "absent"]},
384        "wireless_controller_address": {
385            "required": False, "type": "dict", "default": None,
386            "options": {
387            }
388        }
389    }
390    for attribute_name in module_spec['options']:
391        fields["wireless_controller_address"]['options'][attribute_name] = module_spec['options'][attribute_name]
392        if mkeyname and mkeyname == attribute_name:
393            fields["wireless_controller_address"]['options'][attribute_name]['required'] = True
394
395    check_legacy_fortiosapi()
396    module = AnsibleModule(argument_spec=fields,
397                           supports_check_mode=True)
398
399    versions_check_result = None
400    if module._socket_path:
401        connection = Connection(module._socket_path)
402        if 'access_token' in module.params:
403            connection.set_option('access_token', module.params['access_token'])
404
405        if 'enable_log' in module.params:
406            connection.set_option('enable_log', module.params['enable_log'])
407        else:
408            connection.set_option('enable_log', False)
409        fos = FortiOSHandler(connection, module, mkeyname)
410        versions_check_result = check_schema_versioning(fos, versioned_schema, "wireless_controller_address")
411
412        is_error, has_changed, result = fortios_wireless_controller(module.params, fos, module.check_mode)
413
414    else:
415        module.fail_json(**FAIL_SOCKET_MSG)
416
417    if versions_check_result and versions_check_result['matched'] is False:
418        module.warn("Ansible has detected version mismatch between FortOS system and your playbook, see more details by specifying option -vvv")
419
420    if not is_error:
421        if versions_check_result and versions_check_result['matched'] is False:
422            module.exit_json(changed=has_changed, version_check_warning=versions_check_result, meta=result)
423        else:
424            module.exit_json(changed=has_changed, meta=result)
425    else:
426        if versions_check_result and versions_check_result['matched'] is False:
427            module.fail_json(msg="Error in repo", version_check_warning=versions_check_result, meta=result)
428        else:
429            module.fail_json(msg="Error in repo", meta=result)
430
431
432if __name__ == '__main__':
433    main()
434