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_system_geoip_override
27short_description: Configure geographical location mapping for IP address(es) to override mappings from FortiGuard 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 system feature and geoip_override 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    system_geoip_override:
76        description:
77            - Configure geographical location mapping for IP address(es) to override mappings from FortiGuard.
78        default: null
79        type: dict
80        suboptions:
81            country_id:
82                description:
83                    - Two character Country ID code.
84                type: str
85            description:
86                description:
87                    - Description.
88                type: str
89            ip_range:
90                description:
91                    - Table of IP ranges assigned to country.
92                type: list
93                suboptions:
94                    end_ip:
95                        description:
96                            - 'Final IP address, inclusive, of the address range (format: xxx.xxx.xxx.xxx).'
97                        type: str
98                    id:
99                        description:
100                            - ID number for individual entry in the IP-Range table.
101                        required: true
102                        type: int
103                    start_ip:
104                        description:
105                            - 'Starting IP address, inclusive, of the address range (format: xxx.xxx.xxx.xxx).'
106                        type: str
107            ip6_range:
108                description:
109                    - Table of IPv6 ranges assigned to country.
110                type: list
111                suboptions:
112                    end_ip:
113                        description:
114                            - 'Ending IP address, inclusive, of the address range (format: xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx).'
115                        type: str
116                    id:
117                        description:
118                            - ID of individual entry in the IPv6 range table.
119                        required: true
120                        type: int
121                    start_ip:
122                        description:
123                            - 'Starting IP address, inclusive, of the address range (format: xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx).'
124                        type: str
125            name:
126                description:
127                    - Location name.
128                required: true
129                type: str
130'''
131
132EXAMPLES = '''
133- hosts: fortigates
134  collections:
135    - fortinet.fortios
136  connection: httpapi
137  vars:
138   vdom: "root"
139   ansible_httpapi_use_ssl: yes
140   ansible_httpapi_validate_certs: no
141   ansible_httpapi_port: 443
142  tasks:
143  - name: Configure geographical location mapping for IP address(es) to override mappings from FortiGuard.
144    fortios_system_geoip_override:
145      vdom:  "{{ vdom }}"
146      state: "present"
147      access_token: "<your_own_value>"
148      system_geoip_override:
149        country_id: "<your_own_value>"
150        description: "<your_own_value>"
151        ip_range:
152         -
153            end_ip: "<your_own_value>"
154            id:  "7"
155            start_ip: "<your_own_value>"
156        ip6_range:
157         -
158            end_ip: "<your_own_value>"
159            id:  "11"
160            start_ip: "<your_own_value>"
161        name: "default_name_13"
162
163'''
164
165RETURN = '''
166build:
167  description: Build number of the fortigate image
168  returned: always
169  type: str
170  sample: '1547'
171http_method:
172  description: Last method used to provision the content into FortiGate
173  returned: always
174  type: str
175  sample: 'PUT'
176http_status:
177  description: Last result given by FortiGate on last operation applied
178  returned: always
179  type: str
180  sample: "200"
181mkey:
182  description: Master key (id) used in the last call to FortiGate
183  returned: success
184  type: str
185  sample: "id"
186name:
187  description: Name of the table used to fulfill the request
188  returned: always
189  type: str
190  sample: "urlfilter"
191path:
192  description: Path of the table used to fulfill the request
193  returned: always
194  type: str
195  sample: "webfilter"
196revision:
197  description: Internal revision number
198  returned: always
199  type: str
200  sample: "17.0.2.10658"
201serial:
202  description: Serial number of the unit
203  returned: always
204  type: str
205  sample: "FGVMEVYYQT3AB5352"
206status:
207  description: Indication of the operation's result
208  returned: always
209  type: str
210  sample: "success"
211vdom:
212  description: Virtual domain used
213  returned: always
214  type: str
215  sample: "root"
216version:
217  description: Version of the FortiGate
218  returned: always
219  type: str
220  sample: "v5.6.3"
221
222'''
223from ansible.module_utils.basic import AnsibleModule
224from ansible.module_utils.connection import Connection
225from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import FortiOSHandler
226from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_legacy_fortiosapi
227from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import schema_to_module_spec
228from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_schema_versioning
229from ansible_collections.fortinet.fortios.plugins.module_utils.fortimanager.common import FAIL_SOCKET_MSG
230from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import is_same_comparison
231from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import serialize
232
233
234def filter_system_geoip_override_data(json):
235    option_list = ['country_id', 'description', 'ip_range',
236                   'ip6_range', 'name']
237    dictionary = {}
238
239    for attribute in option_list:
240        if attribute in json and json[attribute] is not None:
241            dictionary[attribute] = json[attribute]
242
243    return dictionary
244
245
246def underscore_to_hyphen(data):
247    if isinstance(data, list):
248        for i, elem in enumerate(data):
249            data[i] = underscore_to_hyphen(elem)
250    elif isinstance(data, dict):
251        new_data = {}
252        for k, v in data.items():
253            new_data[k.replace('_', '-')] = underscore_to_hyphen(v)
254        data = new_data
255
256    return data
257
258
259def system_geoip_override(data, fos, check_mode=False):
260
261    vdom = data['vdom']
262
263    state = data['state']
264
265    system_geoip_override_data = data['system_geoip_override']
266    filtered_data = underscore_to_hyphen(filter_system_geoip_override_data(system_geoip_override_data))
267
268    # check_mode starts from here
269    if check_mode:
270        mkey = fos.get_mkey('system', 'interface', filtered_data, vdom=vdom)
271        current_data = fos.get('system', 'interface', vdom=vdom, mkey=mkey)
272        is_existed = current_data and current_data.get('http_status') == 200 \
273            and isinstance(current_data.get('results'), list) \
274            and len(current_data['results']) > 0
275
276        # 2. if it exists and the state is 'present' then compare current settings with desired
277        if state == 'present' or state is True:
278            if mkey is None:
279                return False, True, filtered_data
280
281            # if mkey exists then compare each other
282            # record exits and they're matched or not
283            if is_existed:
284                is_same = is_same_comparison(
285                    serialize(current_data['results'][0]), serialize(filtered_data))
286                return False, not is_same, filtered_data
287
288            # record does not exist
289            return False, True, filtered_data
290
291        if state == 'absent':
292            if mkey is None:
293                return False, False, filtered_data
294
295            if is_existed:
296                return False, True, filtered_data
297            return False, False, filtered_data
298
299        return True, False, {'reason: ': 'Must provide state parameter'}
300
301    if state == "present" or state is True:
302        return fos.set('system',
303                       'geoip-override',
304                       data=filtered_data,
305                       vdom=vdom)
306
307    elif state == "absent":
308        return fos.delete('system',
309                          'geoip-override',
310                          mkey=filtered_data['name'],
311                          vdom=vdom)
312    else:
313        fos._module.fail_json(msg='state must be present or absent!')
314
315
316def is_successful_status(status):
317    return status['status'] == "success" or \
318        status['http_method'] == "DELETE" and status['http_status'] == 404
319
320
321def fortios_system(data, fos, check_mode):
322
323    if data['system_geoip_override']:
324        resp = system_geoip_override(data, fos, check_mode)
325    else:
326        fos._module.fail_json(msg='missing task body: %s' % ('system_geoip_override'))
327    if check_mode:
328        return resp
329    return not is_successful_status(resp), \
330        resp['status'] == "success" and \
331        (resp['revision_changed'] if 'revision_changed' in resp else True), \
332        resp
333
334
335versioned_schema = {
336    "type": "list",
337    "children": {
338        "ip6_range": {
339            "type": "list",
340            "children": {
341                "start_ip": {
342                    "type": "string",
343                    "revisions": {
344                        "v6.4.4": True,
345                        "v7.0.0": True,
346                        "v6.4.0": True,
347                        "v6.4.1": True
348                    }
349                },
350                "end_ip": {
351                    "type": "string",
352                    "revisions": {
353                        "v6.4.4": True,
354                        "v7.0.0": True,
355                        "v6.4.0": True,
356                        "v6.4.1": True
357                    }
358                },
359                "id": {
360                    "type": "integer",
361                    "revisions": {
362                        "v6.4.4": True,
363                        "v7.0.0": True,
364                        "v6.4.0": True,
365                        "v6.4.1": True
366                    }
367                }
368            },
369            "revisions": {
370                "v6.4.4": True,
371                "v7.0.0": True,
372                "v6.4.0": True,
373                "v6.4.1": True
374            }
375        },
376        "description": {
377            "type": "string",
378            "revisions": {
379                "v6.0.0": True,
380                "v7.0.0": True,
381                "v6.0.5": True,
382                "v6.4.4": True,
383                "v6.4.0": True,
384                "v6.4.1": True,
385                "v6.2.0": True,
386                "v6.2.3": True,
387                "v6.2.5": True,
388                "v6.2.7": True,
389                "v6.0.11": True
390            }
391        },
392        "country_id": {
393            "type": "string",
394            "revisions": {
395                "v6.0.0": True,
396                "v7.0.0": True,
397                "v6.0.5": True,
398                "v6.4.4": True,
399                "v6.4.0": True,
400                "v6.4.1": True,
401                "v6.2.0": True,
402                "v6.2.3": True,
403                "v6.2.5": True,
404                "v6.2.7": True,
405                "v6.0.11": True
406            }
407        },
408        "ip_range": {
409            "type": "list",
410            "children": {
411                "start_ip": {
412                    "type": "string",
413                    "revisions": {
414                        "v6.0.0": True,
415                        "v7.0.0": True,
416                        "v6.0.5": True,
417                        "v6.4.4": True,
418                        "v6.4.0": True,
419                        "v6.4.1": True,
420                        "v6.2.0": True,
421                        "v6.2.3": True,
422                        "v6.2.5": True,
423                        "v6.2.7": True,
424                        "v6.0.11": True
425                    }
426                },
427                "end_ip": {
428                    "type": "string",
429                    "revisions": {
430                        "v6.0.0": True,
431                        "v7.0.0": True,
432                        "v6.0.5": True,
433                        "v6.4.4": True,
434                        "v6.4.0": True,
435                        "v6.4.1": True,
436                        "v6.2.0": True,
437                        "v6.2.3": True,
438                        "v6.2.5": True,
439                        "v6.2.7": True,
440                        "v6.0.11": True
441                    }
442                },
443                "id": {
444                    "type": "integer",
445                    "revisions": {
446                        "v6.0.0": True,
447                        "v7.0.0": True,
448                        "v6.0.5": True,
449                        "v6.4.4": True,
450                        "v6.4.0": True,
451                        "v6.4.1": True,
452                        "v6.2.0": True,
453                        "v6.2.3": True,
454                        "v6.2.5": True,
455                        "v6.2.7": True,
456                        "v6.0.11": True
457                    }
458                }
459            },
460            "revisions": {
461                "v6.0.0": True,
462                "v7.0.0": True,
463                "v6.0.5": True,
464                "v6.4.4": True,
465                "v6.4.0": True,
466                "v6.4.1": True,
467                "v6.2.0": True,
468                "v6.2.3": True,
469                "v6.2.5": True,
470                "v6.2.7": True,
471                "v6.0.11": True
472            }
473        },
474        "name": {
475            "type": "string",
476            "revisions": {
477                "v6.0.0": True,
478                "v7.0.0": True,
479                "v6.0.5": True,
480                "v6.4.4": True,
481                "v6.4.0": True,
482                "v6.4.1": True,
483                "v6.2.0": True,
484                "v6.2.3": True,
485                "v6.2.5": True,
486                "v6.2.7": True,
487                "v6.0.11": True
488            }
489        }
490    },
491    "revisions": {
492        "v6.0.0": True,
493        "v7.0.0": True,
494        "v6.0.5": True,
495        "v6.4.4": True,
496        "v6.4.0": True,
497        "v6.4.1": True,
498        "v6.2.0": True,
499        "v6.2.3": True,
500        "v6.2.5": True,
501        "v6.2.7": True,
502        "v6.0.11": True
503    }
504}
505
506
507def main():
508    module_spec = schema_to_module_spec(versioned_schema)
509    mkeyname = 'name'
510    fields = {
511        "access_token": {"required": False, "type": "str", "no_log": True},
512        "enable_log": {"required": False, "type": bool},
513        "vdom": {"required": False, "type": "str", "default": "root"},
514        "state": {"required": True, "type": "str",
515                  "choices": ["present", "absent"]},
516        "system_geoip_override": {
517            "required": False, "type": "dict", "default": None,
518            "options": {
519            }
520        }
521    }
522    for attribute_name in module_spec['options']:
523        fields["system_geoip_override"]['options'][attribute_name] = module_spec['options'][attribute_name]
524        if mkeyname and mkeyname == attribute_name:
525            fields["system_geoip_override"]['options'][attribute_name]['required'] = True
526
527    check_legacy_fortiosapi()
528    module = AnsibleModule(argument_spec=fields,
529                           supports_check_mode=True)
530
531    versions_check_result = None
532    if module._socket_path:
533        connection = Connection(module._socket_path)
534        if 'access_token' in module.params:
535            connection.set_option('access_token', module.params['access_token'])
536
537        if 'enable_log' in module.params:
538            connection.set_option('enable_log', module.params['enable_log'])
539        else:
540            connection.set_option('enable_log', False)
541        fos = FortiOSHandler(connection, module, mkeyname)
542        versions_check_result = check_schema_versioning(fos, versioned_schema, "system_geoip_override")
543
544        is_error, has_changed, result = fortios_system(module.params, fos, module.check_mode)
545
546    else:
547        module.fail_json(**FAIL_SOCKET_MSG)
548
549    if versions_check_result and versions_check_result['matched'] is False:
550        module.warn("Ansible has detected version mismatch between FortOS system and your playbook, see more details by specifying option -vvv")
551
552    if not is_error:
553        if versions_check_result and versions_check_result['matched'] is False:
554            module.exit_json(changed=has_changed, version_check_warning=versions_check_result, meta=result)
555        else:
556            module.exit_json(changed=has_changed, meta=result)
557    else:
558        if versions_check_result and versions_check_result['matched'] is False:
559            module.fail_json(msg="Error in repo", version_check_warning=versions_check_result, meta=result)
560        else:
561            module.fail_json(msg="Error in repo", meta=result)
562
563
564if __name__ == '__main__':
565    main()
566