August 5, 2017 · ansible network automation ios_config multiple vendors acl mgmt

Networking with Ansible 104

This post will mainly focus on setting up and handling Cisco IOS configurations with the ios_config module. We will look at issues with how the ios_config module functions, using configuration source files, credentials handling, jinja2 templating support, backup handling & access-list management.

More information about the modules can be found here:
http://docs.ansible.com/ansible/latest/ios_config_module.html http://docs.ansible.com/ansible/latest/eos_config_module.html http://docs.ansible.com/ansible/latest/nxos_config_module.html

4.1 IOS Config Module (Part1)


IOS Example 1a

---
- name: IOS Example 1a
  hosts: pynet-rtr1
  vars:
    creds:
    host: "{{ ansible_host }}"
    username: "{{ username }}"
    password: "{{ password }}"
  tasks:
    - name: IOS FACTS 1a
      ios_facts:
        provider: "{{ creds }}"
      tags: facts_only
    - name: IOS CONFIG 1a
      ios_config:
        provider: "{{ creds }}"
        lines:
          - ip domain name bogus.com
          - ip name-server 8.8.8.8
          - ip name-server 8.8.4.4
          - ntp server 130.126.24.24
          - ntp server 152.2.21.1

The lines argument will look for the list entries in the global configuration, if not present it will configure them to be.

IOS Example 1b

Let's make these into variables to rid ourselves of the annoying habit to hard-code things into the playbook.

lines:  
  - "ip domain name {{ default_domain }}"
  - "ip name-server {{ name_server1 }}"
  - "ip name-server {{ name_server2 }}"
  - "ntp server {{ ntp1 }}"
  - "ntp server {{ ntp2 }}"

We also create a group_vars folder and a file named all.yml. all is a group encompassing, as the name states, all targets. In this file we will put the following variables:

---
name_server1: 8.8.8.8  
name_server2: 8.8.4.4  
default_domain: bogus.com  
ntp1: 130.126.24.24  
ntp2: 152.2.21.1  

Previously to us configuring this we had an erronously entered ip name server value of 8.8.3.3 and this will alert us to the drawback of doing configuration changes this way, look at the following output:

show run | inc name-server
ip name-server 8.8.8.8
ip name-server 8.8.4.4
ip name-server 8.8.3.3

While it is great that the two named servers in our playbook are present it makes no sense in keeping abundant and problematic configurations are present in the future.

Let's start with adding the following to our playbook:

        - "ip domain-lookup"

After running this we have verified that we get ICMP results back from running ping google.com but another problem we currently have is that some commands are hidden from the configuration and subsequently Ansible cannot know these are already set, which makes this an non-idempotent behaviour. No matter how many times we run this, a change will occur due to the command ip domain-lookup. We will look at fixing this in another section.

Currently we have only been passing the credentials to the playbooks by either hard-coding them or passing them from hostvars or groupvars files.

Example Playbook:

---
- name: IOS Example
  hosts: pynet-rtr1
  vars:
    creds:
      host: "{{ ansible_host }}"
  tasks:
    - name: IOS CONFIG 1e
      ios_config:
        provider: "{{ creds }}"
        lines:
          - "ip domain name {{ default_domain }}"
          - "ip name-server {{ name_server1 }}"
          - "ip name-server {{ name_server2 }}"
          - "ntp server {{ ntp1 }}"
          - "ntp server {{ ntp2 }}"
          - "ip domain-lookup"

We can make ansible prompt us for the password upon runtime by running our playbook with the following flags:

ansible-playbook ./ios_config1e.yml -i ./../ansible-hosts -u pyclass -k

These credentials can be safely stored with ansible vault which will be introduced later in this blog series. While you will still have to provide a password (the vault master password) you would not have set the target credentials more than once.

Takeaways from this section should be that while it is easy to start pushing configuration changes towards cisco ios devices, making ansible and these devices behave in an idempotent way demands a bit more thought. We have not put much thought when it comes to handling credentials, this is another problem that might arise outside of the safety of a dedicated lab environment.

4.2 IOS Config Module (Part2) - source files, jinja2 templating & config backup


In this section we will be using a source file instead of directly specifying the information.

We create a new playbook called ios_config2a.yml:

- name: IOS Example 2a
  hosts: pynet-rtr1
  vars:
    creds:
      host: "{{ ansible_host }}"
      username: "{{ username }}"
      password: "{{ password }}"

  tasks:
    - name: IOS configuration source example
      ios_config:
        provider: "{{ creds }}"
        src: my_config2a.txt

And we put the following in our my_config2a file:

ip domain name {{ default_domain }}
ip name-server {{ name_server1 }}
ip name-server {{ name_server2 }}
ntp server {{ ntp1 }}
ntp server {{ ntp2 }}

Instead of directly specifying the lines we will fetch the configuration elements from this file which passes in variables elsewhere defined, we can therefore convert this to whatever the variables resolves to.

After running this updated playbook where the configuration is gathered from our defined source file we verify that it goes through, since no changes occured it simply runs without changing anything but we know it does fetch the variables from our defined source.

$ ansible-playbook ./ios_config2a.yml -i ./../ansible-hosts

Lets create a new configuration source file called my_config2b.txt containing the following lines:

ip domain name {{ default_domain }}
{% for ns in [name_server1, name_server2] %}
ip name-server {{ ns }}
{% endfor %}
ntp server {{ ntp1 }}
ntp server {{ ntp2 }}

Here we are creating a Jinja2 for-loop and the source files we are using with the ios_command module has full support for Jinja2 templating. We will dig deeper into this at a later section.

We also create a copy of our iosconfig2a.yml file and call it iosconfig2b.yml, we then define the source to be the file my_config2b.txt instead.

A minor update to one of our values in group_vars/all.yml should make this playbook change our running configuration upon running it which we verify below.

$ ansible-playbook ./ios_config2b.yml -i ./../ansible-hosts  

One last thing is that we with the ios_config module have the possibility of executing a backup prior to running our tasks. We copy our ios_config2b.yml to a new file called ios_config2c and add the following line:

 tasks:
   - ios_config:
       provider: "{{ creds }}"
       src: my_config2b.txt
       backup: yes

After this we change some arbitrary values in group_vars/all.yml so that our playbook run results in a change:

We then locate the newly taken backup file in the folder named backup in our working directory:

workdir]$ tree
.
├── backup
│   └── pynet-rtr1_config.2017-08-04@03:09:50

And there we have it, a fully working configuration backup.

4.3 Configuring ACLS with IOS_Config module


This section will have us looking deeper into the ios_config module and we will go through some examples by configuring an ACL on an Cisco IOS router.

We start creating a playbook called acl1.yml

---

- name: Configure an ACL
  gather_facts: no
  hosts: pynet-rtr1
  vars:
    creds:
      host: "{{ ansible_host }}"
      username: "{{ username }}"
      password: "{{ password }}"

  tasks:
    - name: Configure ACL
      ios_config:
        provider: "{{ creds }}"
        parents: ["ip access-list extended TEST-ACL"]
        lines:
          - permit ip host 1.1.1.1 any log
          - permit ip host 2.2.2.2 any log
          - permit ip host 3.3.3.3 any log
          - permit ip host 4.4.4.4 any log
          - permit ip host 5.5.5.5 any log
        before: ["no ip access-list extended TEST-ACL"]
        replace: block
        match: strict

We have some new arguments we will learn about: parents, before, replace & match.

In this case if the playbook logic leads to a change needing to be made, what we pass in the before key is being run before the change. In this case we actually delete the ACL we're "changing" with no ip access-list extended TEST-ACL

The replace can be passed two arguments: block and line. When replace is set to block it will always add all the lines, if we have it set to line it will only miss the missing line/lines.

Extended IP access list TEST-ACL  
10 permit ip host 1.1.1.1 any log  
20 permit ip host 2.2.2.2 any log  
30 permit ip host 3.3.3.3 any log  
40 permit ip host 4.4.4.4 any log  

The two arguments we will be using in the majority of our playbooks will be line and Exact. Line would match line by line not bothering with the order of the lines, Exact would be matched not only on lines but also by position/order in the configuration and no additional lines may exist.

If we run our initial playbook we will create a change and the access list named TEST-ACL will be configured.

We can test an example on how the replace works by changing it to block and removing one of the lines from the router configuration.

Our current access-list looks like this:

Extended IP access list TEST-ACL  
10 permit ip host 1.1.1.1 any log

30 permit ip host 3.3.3.3 any log  
40 permit ip host 4.4.4.4 any log  

With replace set to block all the lines will be added but with replace set to line it will only add the missing line. In this case 20 permit ip host 2.2.2.2 any log.

Running:

$ ansible-playbook acl_config_1.yml -i ./../ansible-hosts

Results in a change:

We verify this in the IOS configuration:

First we remove the access-list entry 20

pynet-rtr1(config-ext-nacl)#no 20

then we show the current access-list and notice that we are missing entry 20.

pynet-rtr1(config-ext-nacl)#do show access-lists TEST-ACL

Extended IP access list TEST-ACL
10 permit ip host 1.1.1.1 any log
30 permit ip host 3.3.3.3 any log
40 permit ip host 4.4.4.4 any log
50 permit ip host 5.5.5.5 any log

After this we run our playbook block:line and get the following access-list after the change has gone through:

pynet-rtr1(config-ext-nacl)#do show access-lists TEST-ACL
Extended IP access list TEST-ACL
10 permit ip host 1.1.1.1 any log
20 permit ip host 2.2.2.2 any log
30 permit ip host 3.3.3.3 any log
40 permit ip host 4.4.4.4 any log
50 permit ip host 5.5.5.5 any log

4.4 Configuration of Multiple Platforms

Previously we've been focusing on one vendor and the modules pertaining to the devices from that vendor. Now we are going to run tasks against targets in an multi-vendor environment. We are also going to make our clean up our playbook by structuring our data and compacting the playbook itself.

We will be working more with group_vars to accomplish this. But we start off with creating a new playbook called mp_config_1.yml where we will create plays based on the ansible vendor config modules:

We start with our Cisco IOS play:

---
- name: IOS Config Change
  hosts: pynet-rtr1
  tasks:
    - ios_config:
        provider: "{{ creds_ssh }}"
        lines: 
          - "ip domain name {{ default_domain }}"
          - "ip name-server {{ name_server1 }}"
          - "ip name-server {{ name_server2 }}"
          - "ntp server {{ ntp1 }}"
          - "ntp server {{ ntp2 }}"
      tags: nxos    

Our arista play looks like this (notice how we have to define the VRF due to different os syntax:

- name: EOS Config Change
  hosts: pynet-sw5
  tasks:
    - eos_config:
        provider: "{{ creds_eapi }}"
        lines: 
          - "ip domain-name {{ default_domain }}"
          - "ip name-server vrf default {{ name_server1 }}"
          - "ip name-server vrf default {{ name_server2 }}"
          - "ntp server {{ ntp1 }}"
          - "ntp server {{ ntp2 }}"
      tags: eos

And finally this is our nxos_config module (notice how we enter the ip name-servers due to how nxos handles the configuration:

- name: NXOS Config Change
  hosts: nxos1
  tasks:
    - nxos_config:
        provider: "{{ creds_nxapi }}"
        lines: 
          - "ip domain-name {{ default_domain }}"
          - "ip name-server {{ name_server1 }} {{ name_server2 }}"
          - "ntp server {{ ntp1 }}"
          - "ntp server {{ ntp2 }}"
      tags: nxos

Now we are getting started with creating group_vars for the device groups:

├── group_vars
│   ├── all.yml
│   ├── arista.yml
│   ├── cisco.yml
│   └── nxos.yml

cat arista.yml

---
creds_ssh:
  host: "{{ ansible_host }}"
  username: "{{ username }}"
  password: "{{ password }}"
  transport: cli
  timeout: 60

creds_eapi:
    host: "{{ ansible_host }}"
    username: "{{ username }}"
    password: "{{ password }}"
    transport: eapi
    use_ssl: True
    validate_certs: no

  global_config:
    - "ip domain-name {{ default_domain }}"
    - "ip name-server vrf default {{ name_server1 }}"
    - "ip name-server vrf default {{ name_server2 }}"
    - "ntp server {{ ntp1 }}"
    - "ntp server {{ ntp2 }}"

$ cat cisco.yml

---
creds_ssh:
  host: "{{ ansible_host }}"
  username: "{{ username }}"
  password: "{{ password }}"

global_config:
  - "ip domain name {{ default_domain }}"
  - "ip name-server {{ name_server1 }}"
  - "ip name-server {{ name_server2 }}"
  - "ntp server {{ ntp1 }}"
  - "ntp server {{ ntp2 }}"

$ cat nxos.yml

---
creds_ssh:
  host: "{{ ansible_host }}"
  username: "{{ username }}"
  password: "{{ password }}"
  transport: cli

creds_nxapi:
  host: "{{ ansible_host }}"
  username: "{{ username }}"
  password: "{{ password }}"
  transport: nxapi
  use_ssl: yes
  validate_certs: no
  port: 8443

global_config:
  - "ip domain-name {{ default_domain }}"
  - "ip name-server {{ name_server1 }} {{ name_server2 }}"
  - "ntp server {{ ntp1 }}"
  - "ntp server {{ ntp2 }}"

Everything we just put into our group_vars we can delete from our playbook. Instead of calling upon all list items in our play, we simply defined a list called global_config which we will call upon when running our playbook.

Then we turnoff fact gathering, by adding the following line on each play:

gather_facts: False

We're also calling the device groups instead of singular hosts.

So now our playbook mp_config_1.yml looks like this instead:

---
- name: IOS Config Change
  hosts: cisco
  gather_facts: False
  tasks:
    - ios_config:
        provider: "{{ creds_ssh }}"
        lines: "{{ global_config }}"

- name: EOS Config Change
  hosts: arista
  gather_facts: False
  tasks:
    - eos_config:
        provider: "{{ creds_eapi }}"
        lines: "{{ global_config }}"

- name: NXOS Config Change
  hosts: nxos
  gather_facts: False
  tasks:
    - nxos_config:
        provider: "{{ creds_nxapi }}"
        lines: "{{ global_config }}"
      tags: nxos

Which is nice, since we almost cut the lines by half. The configuration we now change in group_vars\all.yml will now be implemented on 2 Cisco IOS routers, 4 Arista Switches and 2 Cisco Nexus Switches. I did change the ip domain-name in all.yml and I will now run our playbook again:

$ ansible-playbook mp_config_2.yml -i ./../ansible-hosts

That's it for this section.