Managing NTP on Cisco IOS with Ansible

Andrea Dainese
February 17, 2017
Post cover

After installing Ansible (see previous post), it’s now time to manage how to send multiple commands (and check) to Cisco IOS devices. This post will explain a playbook (recipe) for clock and NTP configuration.

See the complete script on my Ansible repository .

Planning an Ansible playbook

When we plan what we want, things are more easy. The idea for our NTP playbook is:

  • read configuration from an external file;
  • get router/Switch configuration;
  • update timezone if different;
  • configure summertime or update it if different;
  • configure ntp source interface or update it if different;
  • add ntp servers and remove wrong ones;
  • save configuration only if necessary.

Configuration file is:

# cat etc/ntp_config.yaml
timezone: "clock timezone CET 1 0"
summertime: "clock summer-time CEST recurring last Sun Mar 2:00 last Sun Oct 3:00"
ntp_source: "ntp source Ethernet0/0"
ntp_servers:
- "ntp server 1.1.1.1"
- "ntp server 1.1.1.2"
- "ntp server 1.1.1.3"
- "ntp server 1.1.1.4"

Getting router/switch configuration

Ansible provides the ios_command useful to send a single command and grab the output. The following task will grab clock and ntp configuration:

- name: "GET NTP CONFIGURATION"
  register: get_ntp_config
  ios_command:
    provider: "{{ provider }}"
    commands:
      - "show running-config | include clock timezone"
      - "show running-config | include clock summer-time"
      - "show running-config | include ntp source"
      - "show running-config | include ntp server"

Output will be saved on get_ntp_config array. Each command will have a different key, starting from 0. Each value is an array because each command can grab multiple lines. So get_ntp_config is an array of arrays.

Setting timezone

Ansible provides the ios_config useful to send a multiple command in configuration mode. The following task will configure timezone if needed:

- name: "SET TIMEZONE"
  when: "(timezone is defined) and (timezone != get_ntp_config.stdout_lines[0][0])"
  register: set_timezone
  ios_config:
    provider: "{{ provider }}"
    lines:
      - "{{ timezone }}"
- name: "POSTPONE CONFIGURATION SAVE"
  when: "(set_timezone.changed == true)"
  set_fact: configured=true

The above tasks will:

  • update timezone, only if needed;
  • track the result in set_timezone variable;
  • track the changes in a single variable (configured).

Setting and removing summertime

Summertime must be:

  • configured, if missing;
  • updated, if different from desiderata;
  • removed, if not present in configuration file.
- name: "SET SUMMERTIME"
  when: "(summertime is defined) and (summertime != get_ntp_config.stdout_lines[1][0])"
  register: set_summertime
  ios_config:
    provider: "{{ provider }}"
    lines:
      - "{{ summertime }}"
- name: "POSTPONE CONFIGURATION SAVE"
  when: "(set_timezone.changed == true)"
  set_fact: configured=true
- name: "REMOVE SUMMERTIME"
  when: "(summertime is not defined) and (get_ntp_config.stdout_lines[1][0] != \"\")"
  register: remove_summertime
  ios_config:
    provider: "{{ provider }}"
    lines:
      - "no {{ get_ntp_config.stdout_lines[1][0] }}"
- name: "POSTPONE CONFIGURATION SAVE"
  when: "(remove_summertime.changed == true)"
  set_fact: configured=true

Setting and removing NTP servers

Configuration of NTP servers is a little bit complex because we need to manage arrays. Each NTP server must be:

  • added, if missing;
  • removed, if not present in configuration file.
- name: "SET NTP SERVER"
  when: "(item not in get_ntp_config.stdout_lines[3])"
  with_items: "{{ ntp_servers }}"
  register: set_ntp_server
  ios_config:
    provider: "{{ provider }}"
    lines:
      - "{{ item }}"
- name: "POSTPONE CONFIGURATION SAVE"
  when: "(set_ntp_server.changed == true)"
  set_fact: configured=true
- name: "REMOVE NTP SERVER"
  when: "(item not in ntp_servers)"
  with_items: "{{ get_ntp_config.stdout_lines[3] }}"
  register: remove_ntp_server
  ios_config:
    provider: "{{ provider }}"
    lines:
      - "no {{ item }}"
- name: "POSTPONE CONFIGURATION SAVE"
  when: "(remove_ntp_server.changed == true)"
  set_fact: configured=true

Save Cisco IOS configuration

To send a single command, we use ios_command again:

- name: "SAVE CONFIGURATION"
  when: "(configured is defined) and (configured == true)"
  register: save_config
  ios_command:
    provider: "{{ provider }}"
    commands:
      - "write memory"

Mind that write memory do not need any interactive command. Using copy running-config startup-config need a confirm, and currently seems it’s not supported by the module.

Running the playbook

$ ansible-playbook -i hosts --extra-vars "config=etc/ntp_config.yaml secrets=secrets.yaml" ntp_config_ios.yaml

PLAY [ios] *********************************************************************

TASK [OBTAIN LOGIN CREDENTIALS] ************************************************
ok: [172.17.0.2]
ok: [172.17.0.3]

TASK [DEFINE PROVIDER] *********************************************************
ok: [172.17.0.2]
ok: [172.17.0.3]

TASK [GET NTP CONFIGURATION] ***************************************************
ok: [172.17.0.2]
ok: [172.17.0.3]

TASK [SET TIMEZONE] ************************************************************
skipping: [172.17.0.2]
skipping: [172.17.0.3]

TASK [POSTPONE CONFIGURATION SAVE] *********************************************
skipping: [172.17.0.2]
skipping: [172.17.0.3]

TASK [SET SUMMERTIME] **********************************************************
skipping: [172.17.0.2]
changed: [172.17.0.3]

TASK [POSTPONE CONFIGURATION SAVE] *********************************************
skipping: [172.17.0.2]
skipping: [172.17.0.3]

TASK [REMOVE SUMMERTIME] *******************************************************
skipping: [172.17.0.2]
skipping: [172.17.0.3]

TASK [POSTPONE CONFIGURATION SAVE] *********************************************
skipping: [172.17.0.2]
skipping: [172.17.0.3]

TASK [SET NTP SOURCE] **********************************************************
skipping: [172.17.0.2]
changed: [172.17.0.3]

TASK [POSTPONE CONFIGURATION SAVE] *********************************************
skipping: [172.17.0.2]
ok: [172.17.0.3]

TASK [REMOVE NTP SOURCE] *******************************************************
skipping: [172.17.0.2]
skipping: [172.17.0.3]

TASK [POSTPONE CONFIGURATION SAVE] *********************************************
skipping: [172.17.0.2]
skipping: [172.17.0.3]

TASK [SET NTP SERVER] **********************************************************
skipping: [172.17.0.2] => (item=ntp server 1.1.1.1)
skipping: [172.17.0.2] => (item=ntp server 1.1.1.2)
skipping: [172.17.0.2] => (item=ntp server 1.1.1.3)
skipping: [172.17.0.2] => (item=ntp server 1.1.1.4)
changed: [172.17.0.3] => (item=ntp server 1.1.1.1)
changed: [172.17.0.3] => (item=ntp server 1.1.1.2)
changed: [172.17.0.3] => (item=ntp server 1.1.1.3)
skipping: [172.17.0.3] => (item=ntp server 1.1.1.4)

TASK [POSTPONE CONFIGURATION SAVE] *********************************************
skipping: [172.17.0.2]
ok: [172.17.0.3]

TASK [REMOVE NTP SERVER] *******************************************************
skipping: [172.17.0.2] => (item=ntp server 1.1.1.1)
skipping: [172.17.0.2] => (item=ntp server 1.1.1.2)
skipping: [172.17.0.2] => (item=ntp server 1.1.1.3)
skipping: [172.17.0.2] => (item=ntp server 1.1.1.4)
skipping: [172.17.0.3] => (item=ntp server 1.1.1.4)

TASK [POSTPONE CONFIGURATION SAVE] *********************************************
skipping: [172.17.0.2]
skipping: [172.17.0.3]

TASK [SAVE CONFIGURATION] ******************************************************
skipping: [172.17.0.2]
ok: [172.17.0.3]

PLAY RECAP *********************************************************************
172.17.0.2                 : ok=3    changed=0    unreachable=0    failed=0
172.17.0.3                 : ok=9    changed=3    unreachable=0    failed=0

References