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_user_peergrp
27short_description: Configure peer groups 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 user feature and peergrp 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    user_peergrp:
76        description:
77            - Configure peer groups.
78        default: null
79        type: dict
80        suboptions:
81            member:
82                description:
83                    - Peer group members.
84                type: list
85                suboptions:
86                    name:
87                        description:
88                            - Peer group member name. Source user.peer.name.
89                        required: true
90                        type: str
91            name:
92                description:
93                    - Peer group name.
94                required: true
95                type: str
96'''
97
98EXAMPLES = '''
99- hosts: fortigates
100  collections:
101    - fortinet.fortios
102  connection: httpapi
103  vars:
104   vdom: "root"
105   ansible_httpapi_use_ssl: yes
106   ansible_httpapi_validate_certs: no
107   ansible_httpapi_port: 443
108  tasks:
109  - name: Configure peer groups.
110    fortios_user_peergrp:
111      vdom:  "{{ vdom }}"
112      state: "present"
113      access_token: "<your_own_value>"
114      user_peergrp:
115        member:
116         -
117            name: "default_name_4 (source user.peer.name)"
118        name: "default_name_5"
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_user_peergrp_data(json):
192    option_list = ['member', 'name']
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 user_peergrp(data, fos, check_mode=False):
216
217    vdom = data['vdom']
218
219    state = data['state']
220
221    user_peergrp_data = data['user_peergrp']
222    filtered_data = underscore_to_hyphen(filter_user_peergrp_data(user_peergrp_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('user',
259                       'peergrp',
260                       data=filtered_data,
261                       vdom=vdom)
262
263    elif state == "absent":
264        return fos.delete('user',
265                          'peergrp',
266                          mkey=filtered_data['name'],
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_user(data, fos, check_mode):
278
279    if data['user_peergrp']:
280        resp = user_peergrp(data, fos, check_mode)
281    else:
282        fos._module.fail_json(msg='missing task body: %s' % ('user_peergrp'))
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        "member": {
295            "type": "list",
296            "children": {
297                "name": {
298                    "type": "string",
299                    "revisions": {
300                        "v6.0.0": True,
301                        "v7.0.0": True,
302                        "v6.0.5": True,
303                        "v6.4.4": True,
304                        "v6.4.0": True,
305                        "v6.4.1": True,
306                        "v6.2.0": True,
307                        "v6.2.3": True,
308                        "v6.2.5": True,
309                        "v6.2.7": True,
310                        "v6.0.11": True
311                    }
312                }
313            },
314            "revisions": {
315                "v6.0.0": True,
316                "v7.0.0": True,
317                "v6.0.5": True,
318                "v6.4.4": True,
319                "v6.4.0": True,
320                "v6.4.1": True,
321                "v6.2.0": True,
322                "v6.2.3": True,
323                "v6.2.5": True,
324                "v6.2.7": True,
325                "v6.0.11": True
326            }
327        },
328        "name": {
329            "type": "string",
330            "revisions": {
331                "v6.0.0": True,
332                "v7.0.0": True,
333                "v6.0.5": True,
334                "v6.4.4": True,
335                "v6.4.0": True,
336                "v6.4.1": True,
337                "v6.2.0": True,
338                "v6.2.3": True,
339                "v6.2.5": True,
340                "v6.2.7": True,
341                "v6.0.11": True
342            }
343        }
344    },
345    "revisions": {
346        "v6.0.0": True,
347        "v7.0.0": True,
348        "v6.0.5": True,
349        "v6.4.4": True,
350        "v6.4.0": True,
351        "v6.4.1": True,
352        "v6.2.0": True,
353        "v6.2.3": True,
354        "v6.2.5": True,
355        "v6.2.7": True,
356        "v6.0.11": True
357    }
358}
359
360
361def main():
362    module_spec = schema_to_module_spec(versioned_schema)
363    mkeyname = 'name'
364    fields = {
365        "access_token": {"required": False, "type": "str", "no_log": True},
366        "enable_log": {"required": False, "type": bool},
367        "vdom": {"required": False, "type": "str", "default": "root"},
368        "state": {"required": True, "type": "str",
369                  "choices": ["present", "absent"]},
370        "user_peergrp": {
371            "required": False, "type": "dict", "default": None,
372            "options": {
373            }
374        }
375    }
376    for attribute_name in module_spec['options']:
377        fields["user_peergrp"]['options'][attribute_name] = module_spec['options'][attribute_name]
378        if mkeyname and mkeyname == attribute_name:
379            fields["user_peergrp"]['options'][attribute_name]['required'] = True
380
381    check_legacy_fortiosapi()
382    module = AnsibleModule(argument_spec=fields,
383                           supports_check_mode=True)
384
385    versions_check_result = None
386    if module._socket_path:
387        connection = Connection(module._socket_path)
388        if 'access_token' in module.params:
389            connection.set_option('access_token', module.params['access_token'])
390
391        if 'enable_log' in module.params:
392            connection.set_option('enable_log', module.params['enable_log'])
393        else:
394            connection.set_option('enable_log', False)
395        fos = FortiOSHandler(connection, module, mkeyname)
396        versions_check_result = check_schema_versioning(fos, versioned_schema, "user_peergrp")
397
398        is_error, has_changed, result = fortios_user(module.params, fos, module.check_mode)
399
400    else:
401        module.fail_json(**FAIL_SOCKET_MSG)
402
403    if versions_check_result and versions_check_result['matched'] is False:
404        module.warn("Ansible has detected version mismatch between FortOS system and your playbook, see more details by specifying option -vvv")
405
406    if not is_error:
407        if versions_check_result and versions_check_result['matched'] is False:
408            module.exit_json(changed=has_changed, version_check_warning=versions_check_result, meta=result)
409        else:
410            module.exit_json(changed=has_changed, meta=result)
411    else:
412        if versions_check_result and versions_check_result['matched'] is False:
413            module.fail_json(msg="Error in repo", version_check_warning=versions_check_result, meta=result)
414        else:
415            module.fail_json(msg="Error in repo", meta=result)
416
417
418if __name__ == '__main__':
419    main()
420