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_switch_controller_port_policy
27short_description: Configure port policy to be applied on the managed FortiSwitch ports through NAC device 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 switch_controller feature and port_policy 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    switch_controller_port_policy:
76        description:
77            - Configure port policy to be applied on the managed FortiSwitch ports through NAC device.
78        default: null
79        type: dict
80        suboptions:
81            802_1x:
82                description:
83                    - 802.1x security policy to be applied when using this port-policy. Source switch-controller.security-policy.802-1X.name switch-controller
84                      .security-policy.captive-portal.name.
85                type: str
86            bounce_port_link:
87                description:
88                    - Enable/disable bouncing (administratively bring the link down, up) of a switch port where this port policy is applied. Helps to clear
89                       and reassign VLAN from lldp-profile.
90                type: str
91                choices:
92                    - disable
93                    - enable
94            description:
95                description:
96                    - Description for the port policy.
97                type: str
98            fortilink:
99                description:
100                    - FortiLink interface for which this port policy belongs to. Source system.interface.name.
101                type: str
102            lldp_profile:
103                description:
104                    - LLDP profile to be applied when using this port-policy. Source switch-controller.lldp-profile.name.
105                type: str
106            name:
107                description:
108                    - Port policy name.
109                required: true
110                type: str
111            qos_policy:
112                description:
113                    - QoS policy to be applied when using this port-policy. Source switch-controller.qos.qos-policy.name.
114                type: str
115            vlan_policy:
116                description:
117                    - VLAN policy to be applied when using this port-policy. Source switch-controller.vlan-policy.name.
118                type: str
119'''
120
121EXAMPLES = '''
122- hosts: fortigates
123  collections:
124    - fortinet.fortios
125  connection: httpapi
126  vars:
127   vdom: "root"
128   ansible_httpapi_use_ssl: yes
129   ansible_httpapi_validate_certs: no
130   ansible_httpapi_port: 443
131  tasks:
132  - name: Configure port policy to be applied on the managed FortiSwitch ports through NAC device.
133    fortios_switch_controller_port_policy:
134      vdom:  "{{ vdom }}"
135      state: "present"
136      access_token: "<your_own_value>"
137      switch_controller_port_policy:
138        802_1x: "<your_own_value> (source switch-controller.security-policy.802-1X.name switch-controller.security-policy.captive-portal.name)"
139        bounce_port_link: "disable"
140        description: "<your_own_value>"
141        fortilink: "<your_own_value> (source system.interface.name)"
142        lldp_profile: "<your_own_value> (source switch-controller.lldp-profile.name)"
143        name: "default_name_8"
144        qos_policy: "<your_own_value> (source switch-controller.qos.qos-policy.name)"
145        vlan_policy: "<your_own_value> (source switch-controller.vlan-policy.name)"
146
147'''
148
149RETURN = '''
150build:
151  description: Build number of the fortigate image
152  returned: always
153  type: str
154  sample: '1547'
155http_method:
156  description: Last method used to provision the content into FortiGate
157  returned: always
158  type: str
159  sample: 'PUT'
160http_status:
161  description: Last result given by FortiGate on last operation applied
162  returned: always
163  type: str
164  sample: "200"
165mkey:
166  description: Master key (id) used in the last call to FortiGate
167  returned: success
168  type: str
169  sample: "id"
170name:
171  description: Name of the table used to fulfill the request
172  returned: always
173  type: str
174  sample: "urlfilter"
175path:
176  description: Path of the table used to fulfill the request
177  returned: always
178  type: str
179  sample: "webfilter"
180revision:
181  description: Internal revision number
182  returned: always
183  type: str
184  sample: "17.0.2.10658"
185serial:
186  description: Serial number of the unit
187  returned: always
188  type: str
189  sample: "FGVMEVYYQT3AB5352"
190status:
191  description: Indication of the operation's result
192  returned: always
193  type: str
194  sample: "success"
195vdom:
196  description: Virtual domain used
197  returned: always
198  type: str
199  sample: "root"
200version:
201  description: Version of the FortiGate
202  returned: always
203  type: str
204  sample: "v5.6.3"
205
206'''
207from ansible.module_utils.basic import AnsibleModule
208from ansible.module_utils.connection import Connection
209from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import FortiOSHandler
210from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_legacy_fortiosapi
211from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import schema_to_module_spec
212from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_schema_versioning
213from ansible_collections.fortinet.fortios.plugins.module_utils.fortimanager.common import FAIL_SOCKET_MSG
214from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import is_same_comparison
215from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import serialize
216
217
218def filter_switch_controller_port_policy_data(json):
219    option_list = ['802_1x', 'bounce_port_link', 'description',
220                   'fortilink', 'lldp_profile', 'name',
221                   'qos_policy', 'vlan_policy']
222    dictionary = {}
223
224    for attribute in option_list:
225        if attribute in json and json[attribute] is not None:
226            dictionary[attribute] = json[attribute]
227
228    return dictionary
229
230
231def underscore_to_hyphen(data):
232    if isinstance(data, list):
233        for i, elem in enumerate(data):
234            data[i] = underscore_to_hyphen(elem)
235    elif isinstance(data, dict):
236        new_data = {}
237        for k, v in data.items():
238            new_data[k.replace('_', '-')] = underscore_to_hyphen(v)
239        data = new_data
240
241    return data
242
243
244def switch_controller_port_policy(data, fos, check_mode=False):
245
246    vdom = data['vdom']
247
248    state = data['state']
249
250    switch_controller_port_policy_data = data['switch_controller_port_policy']
251    filtered_data = underscore_to_hyphen(filter_switch_controller_port_policy_data(switch_controller_port_policy_data))
252
253    # check_mode starts from here
254    if check_mode:
255        mkey = fos.get_mkey('system', 'interface', filtered_data, vdom=vdom)
256        current_data = fos.get('system', 'interface', vdom=vdom, mkey=mkey)
257        is_existed = current_data and current_data.get('http_status') == 200 \
258            and isinstance(current_data.get('results'), list) \
259            and len(current_data['results']) > 0
260
261        # 2. if it exists and the state is 'present' then compare current settings with desired
262        if state == 'present' or state is True:
263            if mkey is None:
264                return False, True, filtered_data
265
266            # if mkey exists then compare each other
267            # record exits and they're matched or not
268            if is_existed:
269                is_same = is_same_comparison(
270                    serialize(current_data['results'][0]), serialize(filtered_data))
271                return False, not is_same, filtered_data
272
273            # record does not exist
274            return False, True, filtered_data
275
276        if state == 'absent':
277            if mkey is None:
278                return False, False, filtered_data
279
280            if is_existed:
281                return False, True, filtered_data
282            return False, False, filtered_data
283
284        return True, False, {'reason: ': 'Must provide state parameter'}
285
286    if state == "present" or state is True:
287        return fos.set('switch-controller',
288                       'port-policy',
289                       data=filtered_data,
290                       vdom=vdom)
291
292    elif state == "absent":
293        return fos.delete('switch-controller',
294                          'port-policy',
295                          mkey=filtered_data['name'],
296                          vdom=vdom)
297    else:
298        fos._module.fail_json(msg='state must be present or absent!')
299
300
301def is_successful_status(status):
302    return status['status'] == "success" or \
303        status['http_method'] == "DELETE" and status['http_status'] == 404
304
305
306def fortios_switch_controller(data, fos, check_mode):
307
308    if data['switch_controller_port_policy']:
309        resp = switch_controller_port_policy(data, fos, check_mode)
310    else:
311        fos._module.fail_json(msg='missing task body: %s' % ('switch_controller_port_policy'))
312    if check_mode:
313        return resp
314    return not is_successful_status(resp), \
315        resp['status'] == "success" and \
316        (resp['revision_changed'] if 'revision_changed' in resp else True), \
317        resp
318
319
320versioned_schema = {
321    "type": "list",
322    "children": {
323        "fortilink": {
324            "type": "string",
325            "revisions": {
326                "v6.4.4": True,
327                "v6.4.0": True,
328                "v6.4.1": True
329            }
330        },
331        "lldp_profile": {
332            "type": "string",
333            "revisions": {
334                "v6.4.4": True,
335                "v6.4.0": True,
336                "v6.4.1": True
337            }
338        },
339        "name": {
340            "type": "string",
341            "revisions": {
342                "v6.4.4": True,
343                "v6.4.0": True,
344                "v6.4.1": True
345            }
346        },
347        "vlan_policy": {
348            "type": "string",
349            "revisions": {
350                "v6.4.4": True,
351                "v6.4.0": True,
352                "v6.4.1": True
353            }
354        },
355        "bounce_port_link": {
356            "type": "string",
357            "options": [
358                {
359                    "value": "disable",
360                    "revisions": {
361                        "v6.4.4": True,
362                        "v6.4.0": True,
363                        "v6.4.1": True
364                    }
365                },
366                {
367                    "value": "enable",
368                    "revisions": {
369                        "v6.4.4": True,
370                        "v6.4.0": True,
371                        "v6.4.1": True
372                    }
373                }
374            ],
375            "revisions": {
376                "v6.4.4": True,
377                "v6.4.0": True,
378                "v6.4.1": True
379            }
380        },
381        "802_1x": {
382            "type": "string",
383            "revisions": {
384                "v6.4.4": True,
385                "v6.4.0": True,
386                "v6.4.1": True
387            }
388        },
389        "qos_policy": {
390            "type": "string",
391            "revisions": {
392                "v6.4.4": True,
393                "v6.4.0": True,
394                "v6.4.1": True
395            }
396        },
397        "description": {
398            "type": "string",
399            "revisions": {
400                "v6.4.4": True,
401                "v6.4.0": True,
402                "v6.4.1": True
403            }
404        }
405    },
406    "revisions": {
407        "v6.4.4": True,
408        "v6.4.0": True,
409        "v6.4.1": True
410    }
411}
412
413
414def main():
415    module_spec = schema_to_module_spec(versioned_schema)
416    mkeyname = 'name'
417    fields = {
418        "access_token": {"required": False, "type": "str", "no_log": True},
419        "enable_log": {"required": False, "type": bool},
420        "vdom": {"required": False, "type": "str", "default": "root"},
421        "state": {"required": True, "type": "str",
422                  "choices": ["present", "absent"]},
423        "switch_controller_port_policy": {
424            "required": False, "type": "dict", "default": None,
425            "options": {
426            }
427        }
428    }
429    for attribute_name in module_spec['options']:
430        fields["switch_controller_port_policy"]['options'][attribute_name] = module_spec['options'][attribute_name]
431        if mkeyname and mkeyname == attribute_name:
432            fields["switch_controller_port_policy"]['options'][attribute_name]['required'] = True
433
434    check_legacy_fortiosapi()
435    module = AnsibleModule(argument_spec=fields,
436                           supports_check_mode=True)
437
438    versions_check_result = None
439    if module._socket_path:
440        connection = Connection(module._socket_path)
441        if 'access_token' in module.params:
442            connection.set_option('access_token', module.params['access_token'])
443
444        if 'enable_log' in module.params:
445            connection.set_option('enable_log', module.params['enable_log'])
446        else:
447            connection.set_option('enable_log', False)
448        fos = FortiOSHandler(connection, module, mkeyname)
449        versions_check_result = check_schema_versioning(fos, versioned_schema, "switch_controller_port_policy")
450
451        is_error, has_changed, result = fortios_switch_controller(module.params, fos, module.check_mode)
452
453    else:
454        module.fail_json(**FAIL_SOCKET_MSG)
455
456    if versions_check_result and versions_check_result['matched'] is False:
457        module.warn("Ansible has detected version mismatch between FortOS system and your playbook, see more details by specifying option -vvv")
458
459    if not is_error:
460        if versions_check_result and versions_check_result['matched'] is False:
461            module.exit_json(changed=has_changed, version_check_warning=versions_check_result, meta=result)
462        else:
463            module.exit_json(changed=has_changed, meta=result)
464    else:
465        if versions_check_result and versions_check_result['matched'] is False:
466            module.fail_json(msg="Error in repo", version_check_warning=versions_check_result, meta=result)
467        else:
468            module.fail_json(msg="Error in repo", meta=result)
469
470
471if __name__ == '__main__':
472    main()
473