fortinet-ansible-dev / ansible-galaxy-fortios-collection

GNU General Public License v3.0
84 stars 48 forks source link

Idempotency in fortios_system_dns_database for dns_entry ? #233

Closed vingertop closed 1 year ago

vingertop commented 1 year ago

I created a playbook with the idea to bulk add DNS entries on the fortigate based on entries in a vars file:

---
- hosts: myfortigate
  ignore_unreachable: yes
  ignore_errors: yes
  gather_facts: no
  connection: httpapi
  collections:
    - fortinet.fortios

  vars:
    date: "{{ lookup('pipe', 'date +%Y-%m-%d-%Hh%Mm') }}"
    ansible_httpapi_use_ssl: yes
    ansible_httpapi_validate_certs: no
    ansible_httpapi_port: "{{ ansible_port }}"
    ansible_network_os: fortinet.fortios.fortios

  vars_files:
    - passwords.yml
    - dns-fortigate-entries.yml

  tasks:
  - name: Configure DNS databases.
    fortios_system_dns_database:
      state: "present"
      access_token: "{{ fortios_access_token }}"
      system_dns_database:
        authoritative: "enable"
        contact: "contactme"
        domain: "{{item.zone}}"
        name: "{{item.zone}}"
        primary_name: "{{item.zone}}"
        status: "enable"
        ttl: "86400"
        type: "primary"
        view: "shadow"
        rr_max: "16384"
        dns_entry:
         -
            hostname: "{{item.hostname}}"
            id: "0"
            ip: "{{item.ip}}"
            preference: "10"
            status: "enable"
            ttl: "0"
            type: "A"
    loop: "{{ entries|list }}"

The dns-fortigate-entries.yml:

---
entries:
- { hostname: "EntryList-Host1-Zone1", ip: "1.1.1.6", zone: "zone1"}
- { hostname: "EntryList-Host2-Zone1", ip: "1.1.1.7", zone: "zone1"}
- { hostname: "EntryList-Host1-Zone2", ip: "1.1.1.8", zone: "zone2"}
- { hostname: "EntryList-Host2-Zone2", ip: "1.1.1.9", zone: "zone2"}

As for adding zones to the FortiGate DNS server this works ok in the loop:

image

The task recap gives the impression all records are processed:

PLAY [myfortigate] ********************************************************************************************************************************************************************************************

TASK [Configure DNS databases.] *******************************************************************************************************************************************************************************
changed: [myfortigate] => (item={'hostname': 'EntryList-Host1-Zone1', 'ip': '1.1.1.6', 'zone': 'zone1'})
changed: [myfortigate] => (item={'hostname': 'EntryList-Host2-Zone1', 'ip': '1.1.1.7', 'zone': 'zone1'})
changed: [myfortigate] => (item={'hostname': 'EntryList-Host1-Zone2', 'ip': '1.1.1.8', 'zone': 'zone2'})
changed: [myfortigate] => (item={'hostname': 'EntryList-Host2-Zone2', 'ip': '1.1.1.9', 'zone': 'zone2'})

PLAY RECAP ****************************************************************************************************************************************************************************************************
myfortigate                : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

But the DNS host entries for each zone do not work in the loop. The last entry in the loop overwrites and is the only one present after the play:

image image

When I run a CLI debug on the FortiGate CLI I noticed it is deleting the first entry during the play:

0: config vdom
0: edit root
0: config system dns-database
0: edit "zone2"
0: config dns-entry
0: edit 0
**0: set hostname "EntryList-Host1-Zone2"
0: set ip 1.1.1.8**
0: end
cmd=config vdom
edit root
config system dns-database
edit zone2
config dns-entry
edit 0
set hostname EntryList-Host1-Zone2
set ip 1.1.1.8
end
end

0: end
cmd=end

0: end
0: config vdom
0: edit root
0: config system dns-database
0: edit "zone2"
cmd=config vdom
edit root
config system dns-database
edit zone2
end

0: end
cmd=end

0: end
0: config vdom
0: edit root
0: config system dns-database
**0: edit "zone2"
0: config dns-entry
0: delete 1**
0: end
cmd=config vdom
edit root
config system dns-database
edit zone2
config dns-entry
delete 1
end
end

0: end
cmd=end

0: end
0: config vdom
0: edit root
0: config system dns-database
0: edit "zone2"
**0: config dns-entry
0: edit 0
0: set hostname "EntryList-Host2-Zone2"
0: set ip 1.1.1.9**
0: end
cmd=config vdom
edit root
config system dns-database
edit zone2
config dns-entry
edit 0
set hostname EntryList-Host2-Zone2
set ip 1.1.1.9
end
end

I run these versions:

ansible 2.10.17
python version = 3.8.0 (default, Dec  9 2021, 17:53:27) [GCC 8.4.0]

/home/me/.ansible/collections/ansible_collections
fortinet.fortios 2.2.2

cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.6 LTS"

Is this a by-design choice where and am I simply not understanding why it cannot iterate over de dns entries ? Any clues on how to quick fix this for my use case ?

MaxxLiu22 commented 1 year ago

Hi @vingertop ,

Thank you for raising this question, for editing an entry under an object, we usually use member operation by setting "member_path" and "member_state" arguments in script, otherwise Ansible will replace all config with a latest config, and member operation can only be used on a existing object, my suggestion is run your script with "state: present" to create objects, then uncomment "member_path" and "member_state" to create entries like the example below. If you still have questions, feel free to let me know.

- hosts: fortigates
  collections:
  - fortinet.fortios
  connection: httpapi
  vars:
    entries:
    - { hostname: "EntryList-Host1-Zone1", ip: "1.1.1.6", zone: "zone1",  entry_id: 1}
    - { hostname: "EntryList-Host2-Zone1", ip: "1.1.1.7", zone: "zone1",  entry_id: 2}
    - { hostname: "EntryList-Host1-Zone2", ip: "1.1.1.8", zone: "zone2",  entry_id: 3}
    - { hostname: "EntryList-Host2-Zone2", ip: "1.1.1.9", zone: "zone2",  entry_id: 4}
    vdom: root
    ansible_httpapi_use_ssl: true
    ansible_httpapi_validate_certs: false
    ansible_httpapi_port: 443
  tasks:
  - name: Configure DNS databases.
    fortios_system_dns_database:
      vdom: '{{ vdom }}'
      state: present
      #member_path : dns_entry:id  # uncomment when you have created that object
      #member_state : present         # uncomment when you have created that object
      access_token: 6Hjrf1GHzNsw837wdwnxNNwN0b8f8t
      system_dns_database:
        authoritative: "enable"
        contact: "contactme"
        domain: "{{item.zone}}"
        name: "{{item.zone}}"
        primary_name: "{{item.zone}}"
        status: "enable"
        ttl: "86400"
        type: "primary"
        view: "shadow"
        rr_max: "16384"
        dns_entry:
         -
            hostname: "{{item.hostname}}"
            id: "{{item. entry_id}}"
            ip: "{{item.ip}}"
            preference: "10"
            status: "enable"
            ttl: "0"
            type: "A"
    loop: "{{ entries|list }}"

Thanks, Maxx

vingertop commented 1 year ago

Thank you Maxx for clearing that up. I split it up in 2 tasks and this works nice.

  tasks:
  - name: Configure DNS databases zones.
    fortios_system_dns_database:
      state: "present"
      access_token: "{{ fortios_access_token }}"
      system_dns_database:
        authoritative: "enable"
        contact: "contactme"
        domain: "{{ item }}"
        name: "{{ item }}"
        primary_name: "{{ item }}"
        status: "enable"
        ttl: "86400"
        type: "primary"
        view: "shadow"
        rr_max: "16384"
    loop: "{{ entries | map(attribute='zone') | unique }}"

  - name: Configure DNS databases entries.
    fortios_system_dns_database:
      state: "present"
      member_path: dns_entry:id
      member_state: present
      access_token: "{{ fortios_access_token }}"
      system_dns_database:
        name: "{{item.zone}}"
        dns_entry:
         -
            hostname: "{{item.hostname}}"
            id: "{{item.entry_id}}"
            ip: "{{item.ip}}"
            preference: "10"
            status: "enable"
            ttl: "0"
            type: "A"
    loop: "{{ entries | list }}"