Blog |Follow Nick on Mastodon| About
 

This is my second post, documenting some Ansible/Cisco examples, please check out my Ansible Cisco Primer 1 first; in this example we'll make some changes to devices.

TLDR; Some example files are on github: https://github.com/linickx/ansible-cisco

NTP & DNS, an easy example

This is a play to set NTP & DNS servers, it's a simple "quick win" because the syntax is straight forward and typically in enterprises the settings are the same on all devices... well DNS might not be, but let's pretend for a second that it is :)

Here's the play book file: set_ntp_enable.yml

---
- hosts: ios_devices*
  gather_facts: no
  connection: local
  strategy: debug

  tasks:
  - name: Include Login Credentials
    include_vars: secrets.yml

  - name: Define Provider
    set_fact:
      provider:
        host: "{{ ansible_host }}"
        username: "{{ creds['username'] }}"
        password: "{{ creds['password'] }}"

  - name: Define Provider (Enable)
    when: inventory_hostname in groups.ios_devices_enable
    set_fact:
        enable:
            auth_pass: "{{ creds['auth_pass'] }}"
            authorize: yes

  - name: Update Provider (Enable)
    when: inventory_hostname in groups.ios_devices_enable
    set_fact:
        provider: "{{ provider | combine(enable) }}"

  - name: BACKUP
    ios_config:
      provider: "{{ provider }}"
      backup: yes

  - name: RUN 'Set DNS'
    ios_config:
      provider: "{{ provider }}"
      lines:
        - ip name-server 8.8.8.8
        - ip name-server 8.8.4.4
    register: set_dns

  - name: CHECK CHANGE - dns
    when: "(set_dns.changed == true)"
    set_fact: configured=true

  - name: RUN 'Set NTP'
    ios_config:
      provider: "{{ provider }}"
      lines:
        - ntp server uk.pool.ntp.org
        - ntp server time.apple.com
    register: set_ntp

  - name: CHECK CHANGE - ntp
    when: "(set_ntp.changed == true)"
    set_fact: configured=true

  - name: RUN 'wr mem'
    when: "(configured is defined) and (configured == true)"
    register: save_config
    ios_command:
      provider: "{{ provider }}"
      commands:
        - "write memory"

As before, I've used a wildcard host group to match both [ios_devices] and [ios_devices_enable]; in my lab I have two types of router, r1 & r2 which drop you directly into Priv15 and r3 which requires you to type enable before being able to make any changes, I did this because this matches the real world, some places I visit force admins to type enable others don't.

The router configs can be found here: github.com/linickx/ansible-cisco/lab_rtr_configs

The first task imports secrets.yml, this is less secure than my show_clock_prompt.yml example but shows something significant, the file read is part of a task, not before them, that's because include_vars is a task module; once this is read the 2nd task is to define the provider for all devices.

Task 3 is different - Define Provider (Enable)

The third task, Define Provider (Enable) is our first example of limiting a task to a specific group: when: inventory_hostname in groups.ios_devices_enable I think the syntax is quite self explanatory, as such we're only expecting Tasks 3 & 4 to run on r3.

Backup!

The backup task uses the ios_config's built in backup feature. By default a local ./backup/ directory will be created with a config per device saved... BE WARNED ... the backups are overwritten (replaced) each time you run ansible-playbook so if you need per change copies move the files first before running the playbook again.

Onto making changes

In the playbook, there are two sets of similar tasks, this is the DNS pair that is repeated for NTP.

- name: RUN 'Set DNS'
  ios_config:
    provider: "{{ provider }}"
    lines:
      - ip name-server 8.8.8.8
      - ip name-server 8.8.4.4
  register: set_dns

- name: CHECK CHANGE - dns
  when: "(set_dns.changed == true)"
  set_fact: configured=true

Some things to know here. Ansible will add 8.8.8.8/8.8.4.4 to any device that doesn't have them, it will also skip any deivice that has them already, ansible will not remove any erroneous entries.
register: set_dns creates a variable for the task, in the previous show clock example ios_command created in an output, with ios_config we have a changed property; if the device is skipped then change = false, if a change is made it is set to True.

Typically, if we've made a change, one would want to wr mem, but that's slow; instead of writing the config after every task, I create a new fact called configured, then later after all the tasks are complete I have another task that says "if we made a change, then write", like this....

- name: RUN 'wr mem'
  when: "(configured is defined) and (configured == true)"
  register: save_config
  ios_command:
    provider: "{{ provider }}"
    commands:
      - "write memory"

Here's an example of running the play.

linickx:ansible $ ansible-playbook set_ntp_enable.yml

PLAY [ios_devices*] ************************************************************

TASK [Include Login Credentials] ***********************************************
ok: [r1]
ok: [r2]
ok: [r3]

TASK [Define Provider] *********************************************************
ok: [r1]
ok: [r2]
ok: [r3]

TASK [Define Provider (Enable)] ************************************************
skipping: [r1]
ok: [r3]
skipping: [r2]

TASK [Update Provider (Enable)] ************************************************
skipping: [r1]
skipping: [r2]
ok: [r3]

TASK [BACKUP] ******************************************************************
ok: [r3]
ok: [r2]
ok: [r1]

TASK [RUN 'Set DNS'] ***********************************************************
ok: [r2]
ok: [r3]
changed: [r1]

TASK [CHECK CHANGE - dns] ******************************************************
ok: [r1]
skipping: [r2]
skipping: [r3]

TASK [RUN 'Set NTP'] ***********************************************************
ok: [r2]
ok: [r3]
changed: [r1]

TASK [CHECK CHANGE - ntp] ******************************************************
skipping: [r2]
ok: [r1]
skipping: [r3]

TASK [RUN 'wr mem'] ************************************************************
skipping: [r2]
skipping: [r3]
ok: [r1]

PLAY RECAP *********************************************************************
r1                         : ok=8    changed=2    unreachable=0    failed=0
r2                         : ok=5    changed=0    unreachable=0    failed=0
r3                         : ok=7    changed=0    unreachable=0    failed=0

linickx:ansible $

Notice how only r1 was changed?

So, how do I standardise NTP and DNS?

I mentioned that erroneous or incorrect entries are not removed; to fix that we move our lines of config into variables. With our variables we then have two tasks... add the config we need, remove anything that's not defined.

Here's the playbook:

---
- hosts: ios_devices*
  gather_facts: no
  connection: local
  strategy: debug
  #check_mode: yes

  vars:
    dns_servers:
      - ip name-server 8.8.8.8
      - ip name-server 8.8.4.4
    ntp_servers:
      - ntp server uk.pool.ntp.org
      - ntp server time.apple.com

  tasks:
  - name: Include Login Credentials
    include_vars: secrets.yml

  - name: Define Provider
    set_fact:
      provider:
        host: "{{ ansible_host }}"
        username: "{{ creds['username'] }}"
        password: "{{ creds['password'] }}"

  - name: Define Provider (Enable)
    when: inventory_hostname in groups.ios_devices_enable
    set_fact:
        enable:
            auth_pass: "{{ creds['auth_pass'] }}"
            authorize: yes

  - name: Update Provider (Enable)
    when: inventory_hostname in groups.ios_devices_enable
    set_fact:
        provider: "{{ provider | combine(enable) }}"

  - name: BACKUP
    ios_config:
      provider: "{{ provider }}"
      backup: yes

  - name: "GET CONFIG"
    ios_command:
      provider: "{{ provider }}"
      commands:
        - "show running-config | include ip name-server"
        - "show running-config | include ntp server"
    register: get_config

  - debug: var=get_config.stdout_lines

  - name: RUN 'Set DNS'
    with_items: "{{ dns_servers }}"
    ios_config:
      provider: "{{ provider }}"
      lines:
        - "{{ item }}"
    register: set_dns

  - name: RUN 'Remove DNS'
    when: "(get_config.stdout_lines[0][0] != '') and (item not in dns_servers)"
    with_items: "{{ get_config.stdout_lines[0] }}"
    register: remove_dns
    ios_config:
      provider: "{{ provider }}"
      lines:
        - "no {{ item }}"

  - name: CHECK CHANGE - dns
    when: "(set_dns.changed == true) or (remove_dns.changed == true)"
    set_fact: configured=true

  - name: RUN 'Set NTP'
    with_items: "{{ ntp_servers }}"
    ios_config:
      provider: "{{ provider }}"
      lines:
          - "{{ item }}"
    register: set_ntp

  - name: RUN 'Remove NTP'
    when: "(get_config.stdout_lines[1][0] != '') and (item not in ntp_servers)"
    with_items: "{{ get_config.stdout_lines[1] }}"
    register: remove_ntp
    ios_config:
      provider: "{{ provider }}"
      lines:
        - "no {{ item }}"

  - name: CHECK CHANGE - ntp
    when: "(set_ntp.changed == true) or (remove_ntp.changed == true)"
    set_fact: configured=true

  - name: RUN 'wr mem'
    when: "(configured is defined) and (configured == true)"
    register: save_config
    ios_command:
      provider: "{{ provider }}"
      commands:
        - "write memory"

The check change tasks and final wr mem should make a lot more sense now. Oh, and what does check_mode do? Well, you can enable that for a dry-run or do-no-harm kind of thing.
Look closely at the RUN 'Set NTP' Task. The with_items says "loop through the variable ntp_servers", when ansible loops it automagically creates an {{ item }} variable, so that's the line of config we apply.

The RUN 'Remove NTP' is a little more complicated, so I'll pull out two non-contiguous tasks below...

- name: "GET CONFIG"
  ios_command:
    provider: "{{ provider }}"
    commands:
      - "show running-config | include ip name-server"
      - "show running-config | include ntp server"
  register: get_config

- name: RUN 'Remove NTP'
    when: "(get_config.stdout_lines[1][0] != '') and (item not in ntp_servers)"
    with_items: "{{ get_config.stdout_lines[1] }}"
    register: remove_ntp
    ios_config:
      provider: "{{ provider }}"
      lines:
        - "no {{ item }}"

Before we can remove anything, we need to examine what's there, that's what GET CONIG does, it creates a variable get_config with two reults [0] and [1], the former being the list of name servers, the later being the list of NTP servers.
In the Remote NTP task, we have a when: condition... and (item not in ntp_servers) is self explanatory but (get_config.stdout_lines[1][0] != '') means config "[1]", i.e. out NTP servers, the "[0]" means the output content and != '' not blank... so this task only runs when the content is not blank and the item in question is not in {{ ntp_servers }}. So what is the item in question? Well, its with_items: "{{ get_config.stdout_lines[1] }}" ... it the current lines of config (found with "show run | inc ntp"). Assuming all this succeeds, i.e. the line of config is not a pre-defined NTP server, then we "no" the line/item.

Here's what happens when we run it...

linickx:ansible $ ansible-playbook standard_ntp_enable.yml

PLAY [ios_devices*] ************************************************************

TASK [Include Login Credentials] ***********************************************
ok: [r2]
ok: [r3]
ok: [r1]

TASK [Define Provider] *********************************************************
ok: [r1]
ok: [r2]
ok: [r3]

TASK [Define Provider (Enable)] ************************************************
skipping: [r2]
skipping: [r1]
ok: [r3]

TASK [Update Provider (Enable)] ************************************************
skipping: [r1]
skipping: [r2]
ok: [r3]

TASK [BACKUP] ******************************************************************
ok: [r1]
ok: [r3]
ok: [r2]

TASK [GET CONFIG] **************************************************************
ok: [r2]
ok: [r3]
ok: [r1]

TASK [debug] *******************************************************************
ok: [r3] => {
    "get_config.stdout_lines": [
        [
            "ip name-server 8.8.8.8",
            "ip name-server 8.8.4.4"
        ],
        [
            "ntp server uk.pool.ntp.org",
            "ntp server time.apple.com"
        ]
    ]
}
ok: [r2] => {
    "get_config.stdout_lines": [
        [
            "ip name-server 8.8.8.8",
            "ip name-server 8.8.4.4",
            "ip name-server 208.67.222.222",
            "ip name-server 208.67.220.220"
        ],
        [
            "ntp server 1.uk.pool.ntp.org",
            "ntp server 0.uk.pool.ntp.org",
            "ntp server time.apple.com",
            "ntp server uk.pool.ntp.org"
        ]
    ]
}
ok: [r1] => {
    "get_config.stdout_lines": [
        [
            "ip name-server 8.8.8.8",
            "ip name-server 8.8.4.4"
        ],
        [
            "ntp server uk.pool.ntp.org",
            "ntp server time.apple.com"
        ]
    ]
}

TASK [RUN 'Set DNS'] ***********************************************************
ok: [r3] => (item=ip name-server 8.8.8.8)
ok: [r1] => (item=ip name-server 8.8.8.8)
ok: [r2] => (item=ip name-server 8.8.8.8)
ok: [r1] => (item=ip name-server 8.8.4.4)
ok: [r2] => (item=ip name-server 8.8.4.4)
ok: [r3] => (item=ip name-server 8.8.4.4)

TASK [RUN 'Remove DNS'] ********************************************************
skipping: [r2] => (item=ip name-server 8.8.8.8)
skipping: [r2] => (item=ip name-server 8.8.4.4)
skipping: [r1] => (item=ip name-server 8.8.8.8)
skipping: [r3] => (item=ip name-server 8.8.8.8)
skipping: [r1] => (item=ip name-server 8.8.4.4)
skipping: [r3] => (item=ip name-server 8.8.4.4)
changed: [r2] => (item=ip name-server 208.67.222.222)
changed: [r2] => (item=ip name-server 208.67.220.220)

TASK [CHECK CHANGE - dns] ******************************************************
skipping: [r1]
ok: [r2]
skipping: [r3]

TASK [RUN 'Set NTP'] ***********************************************************
ok: [r2] => (item=ntp server uk.pool.ntp.org)
ok: [r3] => (item=ntp server uk.pool.ntp.org)
ok: [r1] => (item=ntp server uk.pool.ntp.org)
ok: [r2] => (item=ntp server time.apple.com)
ok: [r1] => (item=ntp server time.apple.com)
ok: [r3] => (item=ntp server time.apple.com)

TASK [RUN 'Remove NTP'] ********************************************************
skipping: [r1] => (item=ntp server uk.pool.ntp.org)
skipping: [r1] => (item=ntp server time.apple.com)
skipping: [r3] => (item=ntp server uk.pool.ntp.org)
skipping: [r3] => (item=ntp server time.apple.com)
changed: [r2] => (item=ntp server 1.uk.pool.ntp.org)
changed: [r2] => (item=ntp server 0.uk.pool.ntp.org)
skipping: [r2] => (item=ntp server time.apple.com)
skipping: [r2] => (item=ntp server uk.pool.ntp.org)

TASK [CHECK CHANGE - ntp] ******************************************************
skipping: [r3]
skipping: [r1]
ok: [r2]

TASK [RUN 'wr mem'] ************************************************************
skipping: [r1]
skipping: [r3]
ok: [r2]

PLAY RECAP *********************************************************************
r1                         : ok=7    changed=0    unreachable=0    failed=0
r2                         : ok=12   changed=2    unreachable=0    failed=0
r3                         : ok=9    changed=0    unreachable=0    failed=0

linickx:ansible $

Now, I ran this playbook straight after the other one, so all the routers had the correct settings to start with, but notice r2 was different, the opendns and additional time servers have been removed.

That was a long post!

There's a lot covered in this and my last post, hopefully it helps. My ansible files are on github, feel free to download them: https://github.com/linickx/ansible-cisco

 

 
Nick Bettison ©