Cisco IOS configurations use a simple block indent file syntax for segmenting configuration into sections. Ansible ios_config module provides an implementation for working with IOS configuration sections in a deterministic way.

This post continues my previous post Anisble with Cisco Part 1.

Ansible with Cisco - Part 1 Installation and basic set up
Installation I’m going to install Ansible on my Raspberry Pi for this examle. You can use any other OS including Ubuntu, MacOS etc. Our end goal is to manage both of the routers via Ansible. In this example, we will get the ‘show version | incl Version’ output on both routers using a very basic Ansi…

Agenda

Make some basic configuration changes to the Cisco IOS routers using ios_config module.

  • Configure syslog server on both routers.
  • notify and handlers
  • ios_config with variables
  • parents
  • host_vars

Diagram

diagram

Documentation

https://docs.ansible.com/ansible/2.9/modules/ios_config_module.html


Please note that I'm using Ansible on a different VM now. I copied the playbooks, inventory and variables to the new VM. I have also created an ansible.cfg file so, I don't have to specify the hosts location each time I run a play book.

ubuntu@ubuntu:~/playbooks/network_ops$ cat playbooks/ansible.cfg 
[defaults]
inventory = /home/ubuntu/playbooks/network_ops/inventory/host-file 
ubuntu@ubuntu:~/playbooks/network_ops$ pwd
/home/ubuntu/playbooks/network_ops
ubuntu@ubuntu:~/playbooks/network_ops$ tree
.
├── inventory
│   ├── group_vars
│   │   └── routers
│   │       ├── routers.yml
│   │       └── vault
│   └── host-file
└── playbooks
    ├── ansible.cfg
    ├── show_ip_int_brief.yml
    ├── show_run.yml
    └── show_version.yml

4 directories, 7 files

Configure logging to syslog server

Imagine you are managing 50 routers and you need to configure logging host 192.168.1.125 on each routers. It would be time consuming to configure each device one by one. Let's do this first task via Ansible.

The commands under lines: are exactly what you would type into the CLI if you are on that device.

Let's create a playbook and run it.

Please note that I'm using ios_config module in this example. I have used ios_command  module in the previous example. (part - 1)
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat logging_host.yml 
---

- name: Cisco syslog server
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: configure logging host
      ios_config:
        lines: logging host 192.168.1.125
      register: output

    - name: print output
      debug:
        var: output
playbook
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ ansible-playbook logging_host.yml --ask-vault-pass
Vault password: 

PLAY [Cisco syslog server] **********************************************************************************************************

TASK [configure logging host] *******************************************************************************************************
changed: [router-1]
changed: [router-2]

TASK [print output] *****************************************************************************************************************
ok: [router-1] => {
    "output": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python3"
        },
        "banners": {},
        "changed": true,
        "commands": [
            "logging host 192.168.1.125"
        ],
        "failed": false,
        "updates": [
            "logging host 192.168.1.125"
        ]
    }
}
ok: [router-2] => {
    "output": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python3"
        },
        "banners": {},
        "changed": true,
        "commands": [
            "logging host 192.168.1.125"
        ],
        "failed": false,
        "updates": [
            "logging host 192.168.1.125"
        ]
    }
}

PLAY RECAP **************************************************************************************************************************
router-1                   : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
router-2                   : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  
run playbook

Let's verify on both routers.

router-1#show run | incl logging host
logging host 192.168.1.125

router-2#show run | incl logging host
logging host 192.168.1.125
That was very easy, isn't? If I run the playbook again, Ansible will not re-apply the config. Ansible is clever enough to know that the config line is already there.

Notify and Handlers

Sometimes we need a task to run only when a change is made to the devices. For example, we may want to save the running-config if a change is made, but not if the configuration is unchanged.

Ansible uses handlers to address this use case. Handlers are tasks that only run when notified.

Let's make another change  and make sure the running-config is saved. I'm going to disable CDP on both routers by using no cdp run command.

ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat no_cdp.yml
---

- name: Disable CDP
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: Disable CDP on both routers
      ios_config:
        lines:
          - no cdp run
      notify: save config


  handlers:
    - name: save config
      ios_command:
        commands: wr
no cdp run playbook
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ ansible-playbook no_cdp.yml --ask-vault-pass
Vault password: 

PLAY [Disable CDP] *************************************************

TASK [Disable CDP on both routers] ********************************************************************
changed: [router-2]
changed: [router-1]

RUNNING HANDLER [save config] ********************************************************************
ok: [router-2]
ok: [router-1]

PLAY RECAP *********************************************************
router-1                   : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
router-2                   : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
playbook run
router-1#show startup-config | incl cdp
no cdp run

router-2#show startup-config | incl cdp
no cdp run
start-up config on both routers

save_when parameter

There is another  way to save the configuration by using save_when parameter as shown below. If the argument is set to modified, then the running-config will only be copied to the startup-config if it has changed since the last save to startup-config.

ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat save_when.yml 
---

- name: Disable CDP
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: Disable CDP on both routers
      ios_config:
        lines:
          - no cdp run
    
    - name: Save running-config
      ios_config:
        save_when: modified  
save_when

ios_config with Variables

We can also have the configuration lines in the group_vars file as a variable instead of hard coding in the playbook.

ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat ../inventory/group_vars/routers/routers.yml 
---

ansible_network_os: ios
ansible_user: ansible
ansible_become: yes
ansible_become_method: enable
ansible_become_password: "{{ vault_ansible_become_password }}"
ansible_password: "{{ vault_ansible_password }}"

acl:
  - access-list 101 permit tcp any any eq 80
  - access-list 101 permit tcp any any eq 443
group_var
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat acl.yml 
---

- name: Variable Lab
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: configure ACL
      ios_config:
        lines: "{{ acl }}"
      register: output

    - name: print output
      debug:
        var: output
acl playbook
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ ansible-playbook acl.yml --ask-vault-pass
Vault password: 

PLAY [Variable Lab] *****************************************************************************************************************

TASK [configure ACL] ****************************************************************************************************************
changed: [router-2]
changed: [router-1]

TASK [print output] *****************************************************************************************************************
ok: [router-1] => {
    "output": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python3"
        },
        "banners": {},
        "changed": true,
        "commands": [
            "access-list 101 permit tcp any any eq 80",
            "access-list 101 permit tcp any any eq 443"
        ],
        "failed": false,
        "updates": [
            "access-list 101 permit tcp any any eq 80",
            "access-list 101 permit tcp any any eq 443"
        ]
    }
}
ok: [router-2] => {
    "output": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python3"
        },
        "banners": {},
        "changed": true,
        "commands": [
            "access-list 101 permit tcp any any eq 80",
            "access-list 101 permit tcp any any eq 443"
        ],
        "failed": false,
        "updates": [
            "access-list 101 permit tcp any any eq 80",
            "access-list 101 permit tcp any any eq 443"
        ]
    }
}

PLAY RECAP **************************************************************************************************************************
router-1                   : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
router-2                   : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
playbook run

parents

The ordered set of parents that uniquely identifies the section or hierarchy the commands should be checked against. If the parents argument is omitted, the commands are checked against the set of top level or global commands.

The commands in the previous examples are meant to be in global config mode so, we didn't use parents. However, for example, If we need to configure an Interface, the commands need to be under the specific Interface. So, we need to add parents: line in the task.

ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ cat parents.yml 
---

- name: Parents Example
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: Interface Description - WAN
      ios_config:
        lines:
          - description WAN
        parents: Interface GigabitEthernet0/1
      notify: save config


  handlers:
    - name: save config
      ios_command:
        commands: wr
parents playbook
ubuntu@ubuntu:~/playbooks/network_ops/playbooks$ ansible-playbook parents.yml --ask-vault-pass
Vault password: 

PLAY [Parents Example] ********************************************************************************************************************************************

TASK [Interface Description - WAN] ********************************************************************************************************************************
changed: [router-2]
changed: [router-1]

RUNNING HANDLER [save config] *************************************************************************************************************************************
ok: [router-1]
ok: [router-2]

PLAY RECAP ********************************************************************************************************************************************************
router-1                   : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
router-2                   : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
parents playbook run
router-1#show interfaces description 
Interface                      Status         Protocol Description     
Gi0/1                          admin down     down     WAN

router-2#show interfaces description 
Interface                      Status         Protocol Description
Gi0/1                          admin down     down     WAN

host_vars

The host-specific variables are defined in the host_vars directory . Each file in the host_vars directory is named after the host it represents.

host_vars

Let's create a playbook to demonstrate the usage of host_vars. So far in our examples, we have seen how to send the exact same commands to both of the devices. In a real world, we may need to send different commands to each device, for example interface configs where the configs are unique to each device.

ubuntu@ubuntu:~/ansible_cisco$ tree
.
├── inventory
│   ├── group_vars
│   │   └── routers
│   │       ├── routers.yml
│   │       └── vault
│   ├── host-file
│   └── host_vars				<<< host_vars directory
│       ├── router-1.yml		<<< represents the host
│       └── router-2.yml		<<< represents the host
├── playbooks
│   ├── acl.yml
│   ├── ansible.cfg
│   ├── host_vars_example.yml	<<< host_var example
│   ├── logging_host.yml
│   ├── no_cdp.yml
│   ├── parents.yml
│   ├── save_when.yml
│   ├── show_ip_int_brief.yml
│   ├── show_run.yml
│   └── show_version.yml
└── README.md

5 directories, 16 files
host_var directory
ubuntu@ubuntu:~/ansible_cisco$ cat inventory/host_vars/router-1.yml 
---

desc:
  - description WAN1
  - ip address 172.16.1.1 255.255.255.0
  - no shutdown
router-1.yml
  ubuntu@ubuntu:~/ansible_cisco$ cat inventory/host_vars/router-2.yml 
---

desc:
  - description WAN2
  - ip address 172.16.1.2 255.255.255.0
  - no shutdown
router-2.yml
ubuntu@ubuntu:~/ansible_cisco$ cat playbooks/host_vars_example.yml 
---

- name: host_vars Example
  hosts: routers
  gather_facts: false
  connection: network_cli

  tasks:
    - name: Interface configs
      ios_config:
        lines: "{{ desc }}"
        parents: Interface GigabitEthernet0/1
        before: default interface GigabitEthernet0/1
      register: output

    - name: Save running-config
      ios_config:
        save_when: modified


    - name: print output
      debug:
        var: output
host_var example

before - Ansible will run the default interface GigabitEthernet0/1 command prior to actually running the interface configs.

ubuntu@ubuntu:~/ansible_cisco/playbooks$ ansible-playbook host_var_example.yml --ask-vault-pass
Vault password: 

PLAY [host_vars Example] ************************************************************************************************************************************************************************************************************

TASK [Interface configs] ************************************************************************************************************************************************************************************************************
changed: [router-1]
changed: [router-2]

TASK [Save running-config] **********************************************************************************************************************************************************************************************************
ok: [router-1]
ok: [router-2]

TASK [print output] *****************************************************************************************************************************************************************************************************************
ok: [router-1] => {
    "output": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python3"
        },
        "banners": {},
        "changed": true,
        "commands": [
            "default interface GigabitEthernet0/1",
            "Interface GigabitEthernet0/1",
            "description WAN1",
            "ip address 172.16.1.1 255.255.255.0",
            "no shutdown"
        ],
        "failed": false,
        "updates": [
            "default interface GigabitEthernet0/1",
            "Interface GigabitEthernet0/1",
            "description WAN1",
            "ip address 172.16.1.1 255.255.255.0",
            "no shutdown"
        ]
    }
}
ok: [router-2] => {
    "output": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python3"
        },
        "banners": {},
        "changed": true,
        "commands": [
            "default interface GigabitEthernet0/1",
            "Interface GigabitEthernet0/1",
            "description WAN2",
            "ip address 172.16.1.2 255.255.255.0",
            "no shutdown"
        ],
        "failed": false,
        "updates": [
            "default interface GigabitEthernet0/1",
            "Interface GigabitEthernet0/1",
            "description WAN2",
            "ip address 172.16.1.2 255.255.255.0",
            "no shutdown"
        ]
    }
}

PLAY RECAP **************************************************************************************************************************************************************************************************************************
router-1                   : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
router-2                   : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
playbook run

Thanks for reading.

As always, your feedback and comments are more than welcome.

Reference

https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html

https://netsudrun.wordpress.com/