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_wanopt_peer
27short_description: Configure WAN optimization peers 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 wanopt feature and peer 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    wanopt_peer:
76        description:
77            - Configure WAN optimization peers.
78        default: null
79        type: dict
80        suboptions:
81            ip:
82                description:
83                    - Peer IP address.
84                type: str
85            peer_host_id:
86                description:
87                    - Peer host ID.
88                type: str
89'''
90
91EXAMPLES = '''
92- hosts: fortigates
93  collections:
94    - fortinet.fortios
95  connection: httpapi
96  vars:
97   vdom: "root"
98   ansible_httpapi_use_ssl: yes
99   ansible_httpapi_validate_certs: no
100   ansible_httpapi_port: 443
101  tasks:
102  - name: Configure WAN optimization peers.
103    fortios_wanopt_peer:
104      vdom:  "{{ vdom }}"
105      state: "present"
106      access_token: "<your_own_value>"
107      wanopt_peer:
108        ip: "<your_own_value>"
109        peer_host_id: "myhostname"
110
111'''
112
113RETURN = '''
114build:
115  description: Build number of the fortigate image
116  returned: always
117  type: str
118  sample: '1547'
119http_method:
120  description: Last method used to provision the content into FortiGate
121  returned: always
122  type: str
123  sample: 'PUT'
124http_status:
125  description: Last result given by FortiGate on last operation applied
126  returned: always
127  type: str
128  sample: "200"
129mkey:
130  description: Master key (id) used in the last call to FortiGate
131  returned: success
132  type: str
133  sample: "id"
134name:
135  description: Name of the table used to fulfill the request
136  returned: always
137  type: str
138  sample: "urlfilter"
139path:
140  description: Path of the table used to fulfill the request
141  returned: always
142  type: str
143  sample: "webfilter"
144revision:
145  description: Internal revision number
146  returned: always
147  type: str
148  sample: "17.0.2.10658"
149serial:
150  description: Serial number of the unit
151  returned: always
152  type: str
153  sample: "FGVMEVYYQT3AB5352"
154status:
155  description: Indication of the operation's result
156  returned: always
157  type: str
158  sample: "success"
159vdom:
160  description: Virtual domain used
161  returned: always
162  type: str
163  sample: "root"
164version:
165  description: Version of the FortiGate
166  returned: always
167  type: str
168  sample: "v5.6.3"
169
170'''
171from ansible.module_utils.basic import AnsibleModule
172from ansible.module_utils.connection import Connection
173from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import FortiOSHandler
174from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_legacy_fortiosapi
175from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import schema_to_module_spec
176from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_schema_versioning
177from ansible_collections.fortinet.fortios.plugins.module_utils.fortimanager.common import FAIL_SOCKET_MSG
178from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import is_same_comparison
179from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import serialize
180
181
182def filter_wanopt_peer_data(json):
183    option_list = ['ip', 'peer_host_id']
184    dictionary = {}
185
186    for attribute in option_list:
187        if attribute in json and json[attribute] is not None:
188            dictionary[attribute] = json[attribute]
189
190    return dictionary
191
192
193def underscore_to_hyphen(data):
194    if isinstance(data, list):
195        for i, elem in enumerate(data):
196            data[i] = underscore_to_hyphen(elem)
197    elif isinstance(data, dict):
198        new_data = {}
199        for k, v in data.items():
200            new_data[k.replace('_', '-')] = underscore_to_hyphen(v)
201        data = new_data
202
203    return data
204
205
206def wanopt_peer(data, fos, check_mode=False):
207
208    vdom = data['vdom']
209
210    state = data['state']
211
212    wanopt_peer_data = data['wanopt_peer']
213    filtered_data = underscore_to_hyphen(filter_wanopt_peer_data(wanopt_peer_data))
214
215    # check_mode starts from here
216    if check_mode:
217        mkey = fos.get_mkey('system', 'interface', filtered_data, vdom=vdom)
218        current_data = fos.get('system', 'interface', vdom=vdom, mkey=mkey)
219        is_existed = current_data and current_data.get('http_status') == 200 \
220            and isinstance(current_data.get('results'), list) \
221            and len(current_data['results']) > 0
222
223        # 2. if it exists and the state is 'present' then compare current settings with desired
224        if state == 'present' or state is True:
225            if mkey is None:
226                return False, True, filtered_data
227
228            # if mkey exists then compare each other
229            # record exits and they're matched or not
230            if is_existed:
231                is_same = is_same_comparison(
232                    serialize(current_data['results'][0]), serialize(filtered_data))
233                return False, not is_same, filtered_data
234
235            # record does not exist
236            return False, True, filtered_data
237
238        if state == 'absent':
239            if mkey is None:
240                return False, False, filtered_data
241
242            if is_existed:
243                return False, True, filtered_data
244            return False, False, filtered_data
245
246        return True, False, {'reason: ': 'Must provide state parameter'}
247
248    if state == "present" or state is True:
249        return fos.set('wanopt',
250                       'peer',
251                       data=filtered_data,
252                       vdom=vdom)
253
254    elif state == "absent":
255        return fos.delete('wanopt',
256                          'peer',
257                          mkey=filtered_data['peer-host-id'],
258                          vdom=vdom)
259    else:
260        fos._module.fail_json(msg='state must be present or absent!')
261
262
263def is_successful_status(status):
264    return status['status'] == "success" or \
265        status['http_method'] == "DELETE" and status['http_status'] == 404
266
267
268def fortios_wanopt(data, fos, check_mode):
269
270    if data['wanopt_peer']:
271        resp = wanopt_peer(data, fos, check_mode)
272    else:
273        fos._module.fail_json(msg='missing task body: %s' % ('wanopt_peer'))
274    if check_mode:
275        return resp
276    return not is_successful_status(resp), \
277        resp['status'] == "success" and \
278        (resp['revision_changed'] if 'revision_changed' in resp else True), \
279        resp
280
281
282versioned_schema = {
283    "type": "list",
284    "children": {
285        "ip": {
286            "type": "string",
287            "revisions": {
288                "v6.0.0": True,
289                "v7.0.0": True,
290                "v6.0.5": True,
291                "v6.4.4": True,
292                "v6.4.0": True,
293                "v6.4.1": True,
294                "v6.2.0": True,
295                "v6.2.3": True,
296                "v6.2.5": True,
297                "v6.2.7": True,
298                "v6.0.11": True
299            }
300        },
301        "peer_host_id": {
302            "type": "string",
303            "revisions": {
304                "v6.0.0": True,
305                "v7.0.0": True,
306                "v6.0.5": True,
307                "v6.4.4": True,
308                "v6.4.0": True,
309                "v6.4.1": True,
310                "v6.2.0": True,
311                "v6.2.3": True,
312                "v6.2.5": True,
313                "v6.2.7": True,
314                "v6.0.11": True
315            }
316        }
317    },
318    "revisions": {
319        "v6.0.0": True,
320        "v7.0.0": True,
321        "v6.0.5": True,
322        "v6.4.4": True,
323        "v6.4.0": True,
324        "v6.4.1": True,
325        "v6.2.0": True,
326        "v6.2.3": True,
327        "v6.2.5": True,
328        "v6.2.7": True,
329        "v6.0.11": True
330    }
331}
332
333
334def main():
335    module_spec = schema_to_module_spec(versioned_schema)
336    mkeyname = 'peer-host-id'
337    fields = {
338        "access_token": {"required": False, "type": "str", "no_log": True},
339        "enable_log": {"required": False, "type": bool},
340        "vdom": {"required": False, "type": "str", "default": "root"},
341        "state": {"required": True, "type": "str",
342                  "choices": ["present", "absent"]},
343        "wanopt_peer": {
344            "required": False, "type": "dict", "default": None,
345            "options": {
346            }
347        }
348    }
349    for attribute_name in module_spec['options']:
350        fields["wanopt_peer"]['options'][attribute_name] = module_spec['options'][attribute_name]
351        if mkeyname and mkeyname == attribute_name:
352            fields["wanopt_peer"]['options'][attribute_name]['required'] = True
353
354    check_legacy_fortiosapi()
355    module = AnsibleModule(argument_spec=fields,
356                           supports_check_mode=True)
357
358    versions_check_result = None
359    if module._socket_path:
360        connection = Connection(module._socket_path)
361        if 'access_token' in module.params:
362            connection.set_option('access_token', module.params['access_token'])
363
364        if 'enable_log' in module.params:
365            connection.set_option('enable_log', module.params['enable_log'])
366        else:
367            connection.set_option('enable_log', False)
368        fos = FortiOSHandler(connection, module, mkeyname)
369        versions_check_result = check_schema_versioning(fos, versioned_schema, "wanopt_peer")
370
371        is_error, has_changed, result = fortios_wanopt(module.params, fos, module.check_mode)
372
373    else:
374        module.fail_json(**FAIL_SOCKET_MSG)
375
376    if versions_check_result and versions_check_result['matched'] is False:
377        module.warn("Ansible has detected version mismatch between FortOS system and your playbook, see more details by specifying option -vvv")
378
379    if not is_error:
380        if versions_check_result and versions_check_result['matched'] is False:
381            module.exit_json(changed=has_changed, version_check_warning=versions_check_result, meta=result)
382        else:
383            module.exit_json(changed=has_changed, meta=result)
384    else:
385        if versions_check_result and versions_check_result['matched'] is False:
386            module.fail_json(msg="Error in repo", version_check_warning=versions_check_result, meta=result)
387        else:
388            module.fail_json(msg="Error in repo", meta=result)
389
390
391if __name__ == '__main__':
392    main()
393