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_dns
27short_description: Configure DNS 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 dns 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    system_dns:
68        description:
69            - Configure DNS.
70        default: null
71        type: dict
72        suboptions:
73            cache_notfound_responses:
74                description:
75                    - Enable/disable response from the DNS server when a record is not in cache.
76                type: str
77                choices:
78                    - disable
79                    - enable
80            dns_cache_limit:
81                description:
82                    - Maximum number of records in the DNS cache.
83                type: int
84            dns_cache_ttl:
85                description:
86                    - Duration in seconds that the DNS cache retains information.
87                type: int
88            dns_over_tls:
89                description:
90                    - Enable/disable/enforce DNS over TLS.
91                type: str
92                choices:
93                    - disable
94                    - enable
95                    - enforce
96            domain:
97                description:
98                    - Search suffix list for hostname lookup.
99                type: list
100                suboptions:
101                    domain:
102                        description:
103                            - DNS search domain list separated by space (maximum 8 domains)
104                        required: true
105                        type: str
106            interface:
107                description:
108                    - Specify outgoing interface to reach server. Source system.interface.name.
109                type: str
110            interface_select_method:
111                description:
112                    - Specify how to select outgoing interface to reach server.
113                type: str
114                choices:
115                    - auto
116                    - sdwan
117                    - specify
118            ip6_primary:
119                description:
120                    - Primary DNS server IPv6 address.
121                type: str
122            ip6_secondary:
123                description:
124                    - Secondary DNS server IPv6 address.
125                type: str
126            primary:
127                description:
128                    - Primary DNS server IP address.
129                type: str
130            protocol:
131                description:
132                    - DNS protocols.
133                type: list
134                choices:
135                    - cleartext
136                    - dot
137                    - doh
138            retry:
139                description:
140                    - Number of times to retry (0 - 5).
141                type: int
142            secondary:
143                description:
144                    - Secondary DNS server IP address.
145                type: str
146            server_hostname:
147                description:
148                    - DNS server host name list.
149                type: list
150                suboptions:
151                    hostname:
152                        description:
153                            - DNS server host name list separated by space (maximum 4 domains).
154                        required: true
155                        type: str
156            source_ip:
157                description:
158                    - IP address used by the DNS server as its source IP.
159                type: str
160            ssl_certificate:
161                description:
162                    - Name of local certificate for SSL connections. Source certificate.local.name.
163                type: str
164            timeout:
165                description:
166                    - DNS query timeout interval in seconds (1 - 10).
167                type: int
168'''
169
170EXAMPLES = '''
171- hosts: fortigates
172  collections:
173    - fortinet.fortios
174  connection: httpapi
175  vars:
176   vdom: "root"
177   ansible_httpapi_use_ssl: yes
178   ansible_httpapi_validate_certs: no
179   ansible_httpapi_port: 443
180  tasks:
181  - name: Configure DNS.
182    fortios_system_dns:
183      vdom:  "{{ vdom }}"
184      system_dns:
185        cache_notfound_responses: "disable"
186        dns_cache_limit: "4"
187        dns_cache_ttl: "5"
188        dns_over_tls: "disable"
189        domain:
190         -
191            domain: "<your_own_value>"
192        interface: "<your_own_value> (source system.interface.name)"
193        interface_select_method: "auto"
194        ip6_primary: "<your_own_value>"
195        ip6_secondary: "<your_own_value>"
196        primary: "<your_own_value>"
197        protocol: "cleartext"
198        retry: "15"
199        secondary: "<your_own_value>"
200        server_hostname:
201         -
202            hostname: "myhostname"
203        source_ip: "84.230.14.43"
204        ssl_certificate: "<your_own_value> (source certificate.local.name)"
205        timeout: "21"
206
207'''
208
209RETURN = '''
210build:
211  description: Build number of the fortigate image
212  returned: always
213  type: str
214  sample: '1547'
215http_method:
216  description: Last method used to provision the content into FortiGate
217  returned: always
218  type: str
219  sample: 'PUT'
220http_status:
221  description: Last result given by FortiGate on last operation applied
222  returned: always
223  type: str
224  sample: "200"
225mkey:
226  description: Master key (id) used in the last call to FortiGate
227  returned: success
228  type: str
229  sample: "id"
230name:
231  description: Name of the table used to fulfill the request
232  returned: always
233  type: str
234  sample: "urlfilter"
235path:
236  description: Path of the table used to fulfill the request
237  returned: always
238  type: str
239  sample: "webfilter"
240revision:
241  description: Internal revision number
242  returned: always
243  type: str
244  sample: "17.0.2.10658"
245serial:
246  description: Serial number of the unit
247  returned: always
248  type: str
249  sample: "FGVMEVYYQT3AB5352"
250status:
251  description: Indication of the operation's result
252  returned: always
253  type: str
254  sample: "success"
255vdom:
256  description: Virtual domain used
257  returned: always
258  type: str
259  sample: "root"
260version:
261  description: Version of the FortiGate
262  returned: always
263  type: str
264  sample: "v5.6.3"
265
266'''
267from ansible.module_utils.basic import AnsibleModule
268from ansible.module_utils.connection import Connection
269from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import FortiOSHandler
270from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_legacy_fortiosapi
271from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import schema_to_module_spec
272from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_schema_versioning
273from ansible_collections.fortinet.fortios.plugins.module_utils.fortimanager.common import FAIL_SOCKET_MSG
274from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import is_same_comparison
275from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import serialize
276
277
278def filter_system_dns_data(json):
279    option_list = ['cache_notfound_responses', 'dns_cache_limit', 'dns_cache_ttl',
280                   'dns_over_tls', 'domain', 'interface',
281                   'interface_select_method', 'ip6_primary', 'ip6_secondary',
282                   'primary', 'protocol', 'retry',
283                   'secondary', 'server_hostname', 'source_ip',
284                   'ssl_certificate', 'timeout']
285    dictionary = {}
286
287    for attribute in option_list:
288        if attribute in json and json[attribute] is not None:
289            dictionary[attribute] = json[attribute]
290
291    return dictionary
292
293
294def flatten_single_path(data, path, index):
295    if not data or index == len(path) or path[index] not in data or not data[path[index]]:
296        return
297
298    if index == len(path) - 1:
299        data[path[index]] = ' '.join(str(elem) for elem in data[path[index]])
300    elif isinstance(data[path[index]], list):
301        for value in data[path[index]]:
302            flatten_single_path(value, path, index + 1)
303    else:
304        flatten_single_path(data[path[index]], path, index + 1)
305
306
307def flatten_multilists_attributes(data):
308    multilist_attrs = [[u'protocol']]
309
310    for attr in multilist_attrs:
311        flatten_single_path(data, attr, 0)
312
313    return data
314
315
316def underscore_to_hyphen(data):
317    if isinstance(data, list):
318        for i, elem in enumerate(data):
319            data[i] = underscore_to_hyphen(elem)
320    elif isinstance(data, dict):
321        new_data = {}
322        for k, v in data.items():
323            new_data[k.replace('_', '-')] = underscore_to_hyphen(v)
324        data = new_data
325
326    return data
327
328
329def system_dns(data, fos):
330    vdom = data['vdom']
331    system_dns_data = data['system_dns']
332    system_dns_data = flatten_multilists_attributes(system_dns_data)
333    filtered_data = underscore_to_hyphen(filter_system_dns_data(system_dns_data))
334
335    return fos.set('system',
336                   'dns',
337                   data=filtered_data,
338                   vdom=vdom)
339
340
341def is_successful_status(status):
342    return status['status'] == "success" or \
343        status['http_method'] == "DELETE" and status['http_status'] == 404
344
345
346def fortios_system(data, fos):
347
348    if data['system_dns']:
349        resp = system_dns(data, fos)
350    else:
351        fos._module.fail_json(msg='missing task body: %s' % ('system_dns'))
352
353    return not is_successful_status(resp), \
354        resp['status'] == "success" and \
355        (resp['revision_changed'] if 'revision_changed' in resp else True), \
356        resp
357
358
359versioned_schema = {
360    "type": "dict",
361    "children": {
362        "server_hostname": {
363            "type": "list",
364            "children": {
365                "hostname": {
366                    "type": "string",
367                    "revisions": {
368                        "v7.0.0": True,
369                        "v6.4.4": True,
370                        "v6.4.0": True,
371                        "v6.4.1": True,
372                        "v6.2.0": True,
373                        "v6.2.3": True,
374                        "v6.2.5": True,
375                        "v6.2.7": True
376                    }
377                }
378            },
379            "revisions": {
380                "v7.0.0": True,
381                "v6.4.4": True,
382                "v6.4.0": True,
383                "v6.4.1": True,
384                "v6.2.0": True,
385                "v6.2.3": True,
386                "v6.2.5": True,
387                "v6.2.7": True
388            }
389        },
390        "dns_cache_limit": {
391            "type": "integer",
392            "revisions": {
393                "v6.0.0": True,
394                "v7.0.0": True,
395                "v6.0.5": True,
396                "v6.4.4": True,
397                "v6.4.0": True,
398                "v6.4.1": True,
399                "v6.2.0": True,
400                "v6.2.3": True,
401                "v6.2.5": True,
402                "v6.2.7": True,
403                "v6.0.11": True
404            }
405        },
406        "domain": {
407            "type": "list",
408            "children": {
409                "domain": {
410                    "type": "string",
411                    "revisions": {
412                        "v6.0.0": True,
413                        "v7.0.0": True,
414                        "v6.0.5": True,
415                        "v6.4.4": True,
416                        "v6.4.0": True,
417                        "v6.4.1": True,
418                        "v6.2.0": True,
419                        "v6.2.3": True,
420                        "v6.2.5": True,
421                        "v6.2.7": True,
422                        "v6.0.11": True
423                    }
424                }
425            },
426            "revisions": {
427                "v6.0.0": True,
428                "v7.0.0": True,
429                "v6.0.5": True,
430                "v6.4.4": True,
431                "v6.4.0": True,
432                "v6.4.1": True,
433                "v6.2.0": True,
434                "v6.2.3": True,
435                "v6.2.5": True,
436                "v6.2.7": True,
437                "v6.0.11": True
438            }
439        },
440        "retry": {
441            "type": "integer",
442            "revisions": {
443                "v6.0.0": True,
444                "v7.0.0": True,
445                "v6.0.5": True,
446                "v6.4.4": True,
447                "v6.4.0": True,
448                "v6.4.1": True,
449                "v6.2.0": True,
450                "v6.2.3": True,
451                "v6.2.5": True,
452                "v6.2.7": True,
453                "v6.0.11": True
454            }
455        },
456        "protocol": {
457            "multiple_values": True,
458            "type": "list",
459            "options": [
460                {
461                    "value": "cleartext",
462                    "revisions": {
463                        "v7.0.0": True
464                    }
465                },
466                {
467                    "value": "dot",
468                    "revisions": {
469                        "v7.0.0": True
470                    }
471                },
472                {
473                    "value": "doh",
474                    "revisions": {
475                        "v7.0.0": True
476                    }
477                }
478            ],
479            "revisions": {
480                "v7.0.0": True
481            }
482        },
483        "cache_notfound_responses": {
484            "type": "string",
485            "options": [
486                {
487                    "value": "disable",
488                    "revisions": {
489                        "v6.0.0": True,
490                        "v7.0.0": True,
491                        "v6.0.5": True,
492                        "v6.4.4": True,
493                        "v6.4.0": True,
494                        "v6.4.1": True,
495                        "v6.2.0": True,
496                        "v6.2.3": True,
497                        "v6.2.5": True,
498                        "v6.2.7": True,
499                        "v6.0.11": True
500                    }
501                },
502                {
503                    "value": "enable",
504                    "revisions": {
505                        "v6.0.0": True,
506                        "v7.0.0": True,
507                        "v6.0.5": True,
508                        "v6.4.4": True,
509                        "v6.4.0": True,
510                        "v6.4.1": True,
511                        "v6.2.0": True,
512                        "v6.2.3": True,
513                        "v6.2.5": True,
514                        "v6.2.7": True,
515                        "v6.0.11": True
516                    }
517                }
518            ],
519            "revisions": {
520                "v6.0.0": True,
521                "v7.0.0": True,
522                "v6.0.5": True,
523                "v6.4.4": True,
524                "v6.4.0": True,
525                "v6.4.1": True,
526                "v6.2.0": True,
527                "v6.2.3": True,
528                "v6.2.5": True,
529                "v6.2.7": True,
530                "v6.0.11": True
531            }
532        },
533        "ssl_certificate": {
534            "type": "string",
535            "revisions": {
536                "v7.0.0": True,
537                "v6.4.4": True,
538                "v6.4.0": True,
539                "v6.4.1": True,
540                "v6.2.0": True,
541                "v6.2.3": True,
542                "v6.2.5": True,
543                "v6.2.7": True
544            }
545        },
546        "source_ip": {
547            "type": "string",
548            "revisions": {
549                "v6.0.0": True,
550                "v7.0.0": True,
551                "v6.0.5": True,
552                "v6.4.4": True,
553                "v6.4.0": True,
554                "v6.4.1": True,
555                "v6.2.0": True,
556                "v6.2.3": True,
557                "v6.2.5": True,
558                "v6.2.7": True,
559                "v6.0.11": True
560            }
561        },
562        "primary": {
563            "type": "string",
564            "revisions": {
565                "v6.0.0": True,
566                "v7.0.0": True,
567                "v6.0.5": True,
568                "v6.4.4": True,
569                "v6.4.0": True,
570                "v6.4.1": True,
571                "v6.2.0": True,
572                "v6.2.3": True,
573                "v6.2.5": True,
574                "v6.2.7": True,
575                "v6.0.11": True
576            }
577        },
578        "dns_over_tls": {
579            "type": "string",
580            "options": [
581                {
582                    "value": "disable",
583                    "revisions": {
584                        "v6.4.4": True,
585                        "v6.4.0": True,
586                        "v6.4.1": True,
587                        "v6.2.0": True,
588                        "v6.2.3": True,
589                        "v6.2.5": True,
590                        "v6.2.7": True
591                    }
592                },
593                {
594                    "value": "enable",
595                    "revisions": {
596                        "v6.4.4": True,
597                        "v6.4.0": True,
598                        "v6.4.1": True,
599                        "v6.2.0": True,
600                        "v6.2.3": True,
601                        "v6.2.5": True,
602                        "v6.2.7": True
603                    }
604                },
605                {
606                    "value": "enforce",
607                    "revisions": {
608                        "v6.4.4": True,
609                        "v6.4.0": True,
610                        "v6.4.1": True,
611                        "v6.2.0": True,
612                        "v6.2.3": True,
613                        "v6.2.5": True,
614                        "v6.2.7": True
615                    }
616                }
617            ],
618            "revisions": {
619                "v7.0.0": False,
620                "v6.4.4": True,
621                "v6.4.0": True,
622                "v6.4.1": True,
623                "v6.2.0": True,
624                "v6.2.3": True,
625                "v6.2.5": True,
626                "v6.2.7": True
627            }
628        },
629        "interface_select_method": {
630            "type": "string",
631            "options": [
632                {
633                    "value": "auto",
634                    "revisions": {
635                        "v7.0.0": True,
636                        "v6.4.4": True,
637                        "v6.4.0": True,
638                        "v6.4.1": True,
639                        "v6.2.0": True,
640                        "v6.2.5": True,
641                        "v6.2.7": True
642                    }
643                },
644                {
645                    "value": "sdwan",
646                    "revisions": {
647                        "v7.0.0": True,
648                        "v6.4.4": True,
649                        "v6.4.0": True,
650                        "v6.4.1": True,
651                        "v6.2.0": True,
652                        "v6.2.5": True,
653                        "v6.2.7": True
654                    }
655                },
656                {
657                    "value": "specify",
658                    "revisions": {
659                        "v7.0.0": True,
660                        "v6.4.4": True,
661                        "v6.4.0": True,
662                        "v6.4.1": True,
663                        "v6.2.0": True,
664                        "v6.2.5": True,
665                        "v6.2.7": True
666                    }
667                }
668            ],
669            "revisions": {
670                "v7.0.0": True,
671                "v6.4.4": True,
672                "v6.4.0": True,
673                "v6.4.1": True,
674                "v6.2.0": True,
675                "v6.2.3": False,
676                "v6.2.5": True,
677                "v6.2.7": True
678            }
679        },
680        "timeout": {
681            "type": "integer",
682            "revisions": {
683                "v6.0.0": True,
684                "v7.0.0": True,
685                "v6.0.5": True,
686                "v6.4.4": True,
687                "v6.4.0": True,
688                "v6.4.1": True,
689                "v6.2.0": True,
690                "v6.2.3": True,
691                "v6.2.5": True,
692                "v6.2.7": True,
693                "v6.0.11": True
694            }
695        },
696        "interface": {
697            "type": "string",
698            "revisions": {
699                "v7.0.0": True,
700                "v6.4.4": True,
701                "v6.4.0": True,
702                "v6.4.1": True,
703                "v6.2.0": True,
704                "v6.2.3": False,
705                "v6.2.5": True,
706                "v6.2.7": True
707            }
708        },
709        "ip6_primary": {
710            "type": "string",
711            "revisions": {
712                "v6.0.0": True,
713                "v7.0.0": True,
714                "v6.0.5": True,
715                "v6.4.4": True,
716                "v6.4.0": True,
717                "v6.4.1": True,
718                "v6.2.0": True,
719                "v6.2.3": True,
720                "v6.2.5": True,
721                "v6.2.7": True,
722                "v6.0.11": True
723            }
724        },
725        "ip6_secondary": {
726            "type": "string",
727            "revisions": {
728                "v6.0.0": True,
729                "v7.0.0": True,
730                "v6.0.5": True,
731                "v6.4.4": True,
732                "v6.4.0": True,
733                "v6.4.1": True,
734                "v6.2.0": True,
735                "v6.2.3": True,
736                "v6.2.5": True,
737                "v6.2.7": True,
738                "v6.0.11": True
739            }
740        },
741        "dns_cache_ttl": {
742            "type": "integer",
743            "revisions": {
744                "v6.0.0": True,
745                "v7.0.0": True,
746                "v6.0.5": True,
747                "v6.4.4": True,
748                "v6.4.0": True,
749                "v6.4.1": True,
750                "v6.2.0": True,
751                "v6.2.3": True,
752                "v6.2.5": True,
753                "v6.2.7": True,
754                "v6.0.11": True
755            }
756        },
757        "secondary": {
758            "type": "string",
759            "revisions": {
760                "v6.0.0": True,
761                "v7.0.0": True,
762                "v6.0.5": True,
763                "v6.4.4": True,
764                "v6.4.0": True,
765                "v6.4.1": True,
766                "v6.2.0": True,
767                "v6.2.3": True,
768                "v6.2.5": True,
769                "v6.2.7": True,
770                "v6.0.11": True
771            }
772        }
773    },
774    "revisions": {
775        "v6.0.0": True,
776        "v7.0.0": True,
777        "v6.0.5": True,
778        "v6.4.4": True,
779        "v6.4.0": True,
780        "v6.4.1": True,
781        "v6.2.0": True,
782        "v6.2.3": True,
783        "v6.2.5": True,
784        "v6.2.7": True,
785        "v6.0.11": True
786    }
787}
788
789
790def main():
791    module_spec = schema_to_module_spec(versioned_schema)
792    mkeyname = None
793    fields = {
794        "access_token": {"required": False, "type": "str", "no_log": True},
795        "enable_log": {"required": False, "type": bool},
796        "vdom": {"required": False, "type": "str", "default": "root"},
797        "system_dns": {
798            "required": False, "type": "dict", "default": None,
799            "options": {
800            }
801        }
802    }
803    for attribute_name in module_spec['options']:
804        fields["system_dns"]['options'][attribute_name] = module_spec['options'][attribute_name]
805        if mkeyname and mkeyname == attribute_name:
806            fields["system_dns"]['options'][attribute_name]['required'] = True
807
808    check_legacy_fortiosapi()
809    module = AnsibleModule(argument_spec=fields,
810                           supports_check_mode=False)
811
812    versions_check_result = None
813    if module._socket_path:
814        connection = Connection(module._socket_path)
815        if 'access_token' in module.params:
816            connection.set_option('access_token', module.params['access_token'])
817
818        if 'enable_log' in module.params:
819            connection.set_option('enable_log', module.params['enable_log'])
820        else:
821            connection.set_option('enable_log', False)
822        fos = FortiOSHandler(connection, module, mkeyname)
823        versions_check_result = check_schema_versioning(fos, versioned_schema, "system_dns")
824
825        is_error, has_changed, result = fortios_system(module.params, fos)
826
827    else:
828        module.fail_json(**FAIL_SOCKET_MSG)
829
830    if versions_check_result and versions_check_result['matched'] is False:
831        module.warn("Ansible has detected version mismatch between FortOS system and your playbook, see more details by specifying option -vvv")
832
833    if not is_error:
834        if versions_check_result and versions_check_result['matched'] is False:
835            module.exit_json(changed=has_changed, version_check_warning=versions_check_result, meta=result)
836        else:
837            module.exit_json(changed=has_changed, meta=result)
838    else:
839        if versions_check_result and versions_check_result['matched'] is False:
840            module.fail_json(msg="Error in repo", version_check_warning=versions_check_result, meta=result)
841        else:
842            module.fail_json(msg="Error in repo", meta=result)
843
844
845if __name__ == '__main__':
846    main()
847