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