How to Automate Cisco NXOS infrastructure with Ansible

You manage a lot of network devices, but you are alone or you don’t have time. Ansible can help you to manage your change on your whole network very quickly based on your own template. In this article we will use Cisco Nexus 9K.

You have a new DNS server, syslog server etc and you need to modify hundred switches. No worries, with ansible it can be very simple.

First you should create at least two files. The first one will be your inventory and contains your switches. The second will be your playbook.

The first thing is to create a service account for ansible in your switches. This account could be centralize or local. In the following I’ll provide my password in cleartext. Of course, it’s not recommended and you should prefer ssh-key.

On my virtual nexus 9k, I only configured my account and my management IP address.

My topology contains :

  • Nexus-1 : IP 10.0.100.99, name: AGG1
  • Nexus-2 : IP 10.0.100.100, name: ACC1
  • Nexus-3 : IP 10.0.100.101, name: ACC2
switch(config-if)# sh run 

!Command: show running-config
!Running configuration last done at: Sat Mar 21 18:28:03 2020
!Time: Sat Mar 21 18:29:45 2020

version 9.3(2) Bios:version  
[..]
username ansible password 5 $5$.FhD0kmO$4PJV/HKJN5ul9aK7160ii.1WQ3s9pjh2QCRL7x7l
EU/  role network-admin
username ansible passphrase  lifetime 99999 warntime 14 gracetime 3
ip domain-lookup

[..]
interface mgmt0
  vrf member management
  ip address 10.0.100.100/24
line console
line vty

The inventory file will be the following. We can use two formats: YAML or INI. This one will use the INI format. This file contains a group named N9K with three switches.

[N9K]
AGR1 ansible_host=10.0.100.99  ansible_port=22
ACC1  ansible_host=10.0.100.100 ansible_port=22
ACC2  ansible_host=10.0.100.101 ansible_port=22

[N9K:vars]
ansible_user=ansible
ansible_password=@ns1b!E.
ansible_connection=network_cli
ansible_network_os=nxos
ansible_python_interpreter="/usr/bin/env python"

The following file uses the YAML format. This first playbook is very simple and contains one task to configure the switch hostname.

---
- name: Setup Nexus Devices

  hosts: all
  connection: local
  gather_facts: False


  tasks:

    - name: configure hostname
      nxos_config:
        lines: hostname {{ inventory_hostname }}
        save_when: modified

Now I’ll verify my playbook, before apply the changes. This command uses the option -i to specify which file should be use as inventory and –check to simulate the changes.

root@09cf326cc275:/ansible/NXOS# ansible-playbook -i inventory-home playbook-home.yaml --check

PLAY [Setup Nexus Devices] ***********************************************************************************************************************

TASK [configure hostname] ************************************************************************************************************************
[WARNING]: Skipping command `copy running-config startup-config` due to check_mode.  Configuration not copied to non-volatile storage
terpreter_discovery.html for more information.
changed: [ACC1]
changed: [AGR1]
changed: [ACC2]

PLAY RECAP ***************************************************************************************************************************************
ACC1                       : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ACC2                       : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
AGR1                       : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

Now I’ll do the same without the option –check and my Nexus device should be configured. You can see the message copy running is not there.

root@09cf326cc275:/ansible/NXOS# ansible-playbook -i inventory-home playbook-home.yaml        

PLAY [Setup Nexus Devices] ***********************************************************************************************************************

TASK [configure hostname] ************************************************************************************************************************

changed: [ACC1]
changed: [AGR1]
changed: [ACC2]

PLAY RECAP ***************************************************************************************************************************************
ACC1                       : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ACC2                       : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
AGR1                       : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Fantastic, my nexus have been configured !! With the command show accounting log, you can verify the command injected by ansible. In my playbook, I added the line save_when: modified to save the configuration after the changes.

AGR1# show accounting log | last 10
Sat Mar 21 18:45:42 2020:type=stop:id=10.0.100.150@pts/2:user=ansible:cmd=shell terminated because the ssh session closed
Sat Mar 21 18:49:11 2020:type=start:id=10.0.100.150@pts/2:user=ansible:cmd=
Sat Mar 21 18:49:12 2020:type=update:id=10.0.100.150@pts/2:user=ansible:cmd=terminal length 0 (SUCCESS)
Sat Mar 21 18:49:12 2020:type=update:id=10.0.100.150@pts/2:user=ansible:cmd=terminal width 511 (SUCCESS)
Sat Mar 21 18:49:20 2020:type=update:id=10.0.100.150@pts/2:user=ansible:cmd=configure terminal ; hostname AGR1 (SUCCESS)
Sat Mar 21 18:49:26 2020:type=update:id=10.0.100.150@pts/2:user=ansible:cmd=Performing configuration copy.
Sat Mar 21 18:49:36 2020:type=start:id=vsh.bin.13650:user=admin:cmd=
Sat Mar 21 18:49:52 2020:type=update:id=10.0.100.150@pts/2:user=ansible:cmd=copy running-config startup-config (SUCCESS)
Sat Mar 21 18:49:53 2020:type=stop:id=10.0.100.150@pts/2:user=ansible:cmd=shell terminated because the ssh session closed
Sat Mar 21 18:52:35 2020:type=update:id=console0:user=admin:cmd=terminal width 511 (SUCCESS)

Now you can imagine the next step. You can add your syslog server for example.

    - name: configure syslog server
      nxos_config:
        lines:
          - logging server 10.0.100.42 4 use-vrf management facility local7
          - logging timestamp milliseconds
        save_when: modified

Before the change:

AGR1(config)# logging timestamp milliseconds ^C
AGR1(config)# sh logging 

Logging console:                enabled (Severity: critical)
Logging monitor:                enabled (Severity: notifications)
Logging linecard:               enabled (Severity: notifications)
Logging timestamp:              Seconds
Logging source-interface :      disabled
Logging rate-limit:             enabled
Logging server:                 disabled
Logging origin_id :             disabled
Logging RFC :                   disabled
Logging logflash:               enabled (Severity: notifications)
Logging logfile:                enabled
        Name - messages: Severity - notifications Size - 4194304

[..]

After the change:

AGR1(config)# 2020 Mar 21 18:58:48 AGR1 %$ VDC-1 %$  %SYSLOG-2-SYSTEM_MSG: Attempt to configure logging server with: hostname/IP 10.0.100.42,severity 4,port 514,facility local7 - syslogd
AGR1(config)# sh logging 

Logging console:                enabled (Severity: critical)
Logging monitor:                enabled (Severity: notifications)
Logging linecard:               enabled (Severity: notifications)
Logging timestamp:              Milliseconds
Logging source-interface :      disabled
Logging rate-limit:             enabled
Logging server:                 enabled
{10.0.100.42}
        This server is temporarily unreachable
        server severity:        warnings
        server facility:        local7
        server VRF:             management
        server port:            514
Logging origin_id :             disabled
Logging RFC :                   disabled
Logging logflash:               enabled (Severity: notifications)
Logging logfile:                enabled
        Name - messages: Severity - notifications Size - 4194304
[..]
root@09cf326cc275:/ansible/NXOS# ansible-playbook -i inventory-home playbook-home.yaml

PLAY [Setup Nexus Devices] ***********************************************************************************************************************

TASK [configure hostname] ************************************************************************************************************************

ok: [ACC1]
ok: [AGR1]
ok: [ACC2]

TASK [configure syslog server] *******************************************************************************************************************
changed: [ACC1]
changed: [ACC2]
changed: [AGR1]

PLAY RECAP ***************************************************************************************************************************************
ACC1                       : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ACC2                       : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
AGR1                       : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

I can be useful to manage your access-list. Imagine you install a new server for the monitoring and you need to update one entry. This time we will use another module named nxos_acl.

    - name: configure SNMP-ACCESS-LIST
      nxos_acl:
        name: ACL_SNMP-ReadOnly
        seq: "10"
        action: permit
        proto: udp
        src: 10.0.100.42/32
        dest: any
        state: present

Now we have the ACL configured on all switches. When the module exists, prefer to use the specific module.

root@09cf326cc275:/ansible/NXOS# ansible-playbook -i inventory-home playbook-home.yaml

PLAY [Setup Nexus Devices] ***********************************************************************************************************************

TASK [configure hostname] ************************************************************************************************************************

ok: [ACC1]
ok: [AGR1]
ok: [ACC2]

TASK [configure syslog server] *******************************************************************************************************************
changed: [ACC1]
changed: [ACC2]
changed: [AGR1]

TASK [configure SNMP-ACCESS-LIST] ****************************************************************************************************************
changed: [ACC1]
changed: [ACC2]
changed: [AGR1]

PLAY RECAP ***************************************************************************************************************************************
ACC1                       : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ACC2                       : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
AGR1                       : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 
AGR1(config)# sh ip access-lists ACL_SNMP-ReadOnly

IP access list ACL_SNMP-ReadOnly
        10 permit udp 10.0.100.42/32 any
--
ACC1(config)# sh ip access-lists ACL_SNMP-ReadOnly

IP access list ACL_SNMP-ReadOnly
	10 permit udp 10.0.100.42/32 any
--
ACC2# sh ip access-lists ACL_SNMP-ReadOnly

IP access list ACL_SNMP-ReadOnly
        10 permit udp 10.0.100.42/32 any 

This module is idempotent. Now we will update the ACL with a second entry. The documentation is here.

    - name: configure SNMP-ACCESS-LIST
      nxos_acl:
        name: ACL_SNMP-ReadOnly
        seq: "10"
        action: permit
        proto: udp
        src: 10.0.100.42/32
        dest: any
        state: present

    - name: configure SNMP-ACCESS-LIST
      nxos_acl:
        name: ACL_SNMP-ReadOnly
        seq: "20"
        action: permit
        proto: udp
        src: 10.0.100.43/32
        dest: any
        state: present
root@09cf326cc275:/ansible/NXOS# ansible-playbook -i inventory-home playbook-home.yaml

PLAY [Setup Nexus Devices] ***********************************************************************************************************************

TASK [configure hostname] ************************************************************************************************************************
changed: [ACC1]
changed: [AGR1]
changed: [ACC2]

TASK [configure syslog server] *******************************************************************************************************************
changed: [ACC1]
changed: [ACC2]
changed: [AGR1]

TASK [configure SNMP-ACCESS-LIST] ****************************************************************************************************************
ok: [ACC1]
ok: [AGR1]
ok: [ACC2]

TASK [configure SNMP-ACCESS-LIST] ****************************************************************************************************************
changed: [ACC1]
changed: [AGR1]
changed: [ACC2]

PLAY RECAP ***************************************************************************************************************************************
ACC1                       : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ACC2                       : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
AGR1                       : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
AGR1(config)# sh ip access-lists ACL_SNMP-ReadOnly

IP access list ACL_SNMP-ReadOnly
        10 permit udp 10.0.100.42/32 any 
        20 permit udp 10.0.100.43/32 any

and they update the last entry with a new IP address.

    - name: configure SNMP-ACCESS-LIST
      nxos_acl:
        name: ACL_SNMP-ReadOnly
        seq: "10"
        action: permit
        proto: udp
        src: 10.0.100.42/32
        dest: any
        state: present

    - name: configure SNMP-ACCESS-LIST
      nxos_acl:
        name: ACL_SNMP-ReadOnly
        seq: "20"
        action: permit
        proto: udp
        src: 10.0.100.44/32
        dest: any
        state: present
root@09cf326cc275:/ansible/NXOS# ansible-playbook -i inventory-home playbook-home.yaml

PLAY [Setup Nexus Devices] ***********************************************************************************************************************

TASK [configure hostname] ************************************************************************************************************************
changed: [ACC1]
changed: [ACC2]
changed: [AGR1]

TASK [configure syslog server] *******************************************************************************************************************
changed: [ACC1]
changed: [AGR1]
changed: [ACC2]

TASK [configure SNMP-ACCESS-LIST] ****************************************************************************************************************
ok: [ACC1]
ok: [AGR1]
ok: [ACC2]

TASK [configure SNMP-ACCESS-LIST] ****************************************************************************************************************
changed: [ACC1]
changed: [AGR1]
changed: [ACC2]

PLAY RECAP ***************************************************************************************************************************************
ACC1                       : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ACC2                       : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
AGR1                       : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 
AGR1(config)# sh ip access-lists ACL_SNMP-ReadOnly

IP access list ACL_SNMP-ReadOnly
        10 permit udp 10.0.100.42/32 any 
        20 permit udp 10.0.100.44/32 any

You can image a lot of scenario now, and apply your change very quickly.

Convert your code easily with APIC Rest Python Adapter (arya)

Arya is a tool to translate an XML or JSON to Python. Arya will convert your input and use the Cisco sdk COBRA.

Generate the code with arya :

arya -f tenant.xml
 !/usr/bin/env python
 '''
 Autogenerated code using arya
 Original Object Document Input:
 
 
 '''
 raise RuntimeError('Please review the auto generated code before ' +
                     'executing the output. Some placeholders will ' +
                     'need to be changed')
 list of packages that should be imported for this code to work
 import cobra.mit.access
 import cobra.mit.naming
 import cobra.mit.request
 import cobra.mit.session
 import cobra.model.fv
 import cobra.model.vns
 from cobra.internal.codec.xmlcodec import toXMLStr
 log into an APIC and create a directory object
 ls = cobra.mit.session.LoginSession('https://1.1.1.1', 'admin', 'password')
 md = cobra.mit.access.MoDirectory(ls)
 md.login()
 the top level object on which operations will be made
 Confirm the dn below is for your top dn
 topDn = cobra.mit.naming.Dn.fromString('uni/tn-aaaaaaaa-tn')
 topParentDn = topDn.getParent()
 topMo = md.lookupByDn(topParentDn)
 build the request using cobra syntax
 fvTenant = cobra.model.fv.Tenant(topMo, ownerKey='', name='aaaaaaaa-tn', descr='', nameAlias='', ownerTag='')
 vnsSvcCont = cobra.model.vns.SvcCont(fvTenant)
 fvRsTenantMonPol = cobra.model.fv.RsTenantMonPol(fvTenant, tnMonEPGPolName='')
 commit the generated code to APIC
 print toXMLStr(topMo)
 c = cobra.mit.request.ConfigRequest()
 c.addMo(topMo)
 md.commit(c)

How to program Cisco ACI with Ansible and Docker

Ansible guide : https://docs.ansible.com/ansible/devel/scenario_guides/guide_aci.html

I create a docker container with ansible, python and the demo from github.

git clone https://github.com/CiscoDevNet/aci-learning-labs-code-samples 
cd aci-learning-labs-code-samples 

docker image with ansible and python:

docker pull zednetwork/aci-ansible2-4

New version with ansible 2.8.2 using debian 10.

docker pull zednetwork/aci-ansible.2-8-2

Docker Compose example:

version: "3" 
services:
  ansible:
    image: zednetwork/aci-ansible2-4
    tty: true
    stdin_open: true

Start the container and connect to it:

docker-compose up -d 
Creating network "aci-ansible_default" with the default driver

Pulling ansible (zednetwork/aci-ansible2-4:)…

latest: Pulling from zednetwork/aci-ansible2-4

22dbe790f715: Downloading [>                                                  ]  465.6kB/45.34 MBf88405a685: Pulling fs layer

22dbe790f715: Downloading [=>                                                 <..>
22dbe790f715: Pull complete
3bf88405a685: Pull complete
Creating aci-ansible_ansible_1 … done

Check container

# docker images
 REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
 zednetwork/aci-ansible2-4   latest              ff17ed37f691        34 minutes ago      659MB

# docker ps
 CONTAINER ID        IMAGE                       COMMAND             CREATED              STATUS              PORTS               NAMES
 53993071ffa9        zednetwork/aci-ansible2-4   "bash"              About a minute ago   Up About a minute                       aci-ansible_ansible_1

Connect to the container. Use the Container ID above.

# docker exec -it 53993071ffa9 /bin/bash
root@53993071ffa9:/#

This container already contains an example from devnet.cisco.com ( https://developer.cisco.com/docs/aci/#ansible). This example uses a public ACI Fabric.

We can use the first playbook to create a tenant on the ACI Fabric. The fabric credential is on the inventory file.

root@53993071ffa9:~/aci_ansible_learning_labs_code_samples/intro_module# cat inventory
 [apic:vars]
 username=admin
 password=ciscopsdt
 ansible_python_interpreter="/usr/bin/env python"
 [apic]
 sandboxapicdc.cisco.com

You can connect directly to the fabric and verify if your tenant is present. https://sandboxapicdc.cisco.com/

root@53993071ffa9:~/aci_ansible_learning_labs_code_samples/intro_module# ansible-playbook -i inventory 01_aci_tenant_pb.yml
 What would you like to name your Tenant?: MyFirstTenant-tn
 PLAY [ENSURE APPLICATION CONFIGURATION EXISTS] 
 TASK [ENSURE APPLICATIONS TENANT EXISTS] 
 changed: [sandboxapicdc.cisco.com]
 PLAY RECAP 
 sandboxapicdc.cisco.com    : ok=1    changed=1    unreachable=0    failed=0

Go to ACI > Tenants

You can delete your tenant with another playbook

root@53993071ffa9:~/aci_ansible_learning_labs_code_samples/intro_module# ansible-playbook -i inventory 01-1_aci_tenant_pb.yml
 What would you like to name your Tenant?: MyFirstTenant-tn
 PLAY [ENSURE APPLICATION CONFIGURATION EXISTS] 
 TASK [ENSURE APPLICATIONS TENANT EXISTS] 
 changed: [sandboxapicdc.cisco.com]
 PLAY RECAP 
 sandboxapicdc.cisco.com    : ok=1    changed=1    unreachable=0    failed=0

Other example to list all tenants:

# cat listTenants.yml
---
- name: ENSURE APPLICATION CONFIGURATION EXISTS
  hosts: apic
  connection: local
  gather_facts: False
  
  tasks:

    - name: List all tenants
        aci_tenant:
        host: "{{ ansible_host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        state: "query"
      validate_certs: False 

# ansible-playbook -i inventory listTenants.yml -vvv

How to enable shell on Cisco IOS

Enable shell

Enable shell linux 

router(config)#shell processing full

show shell environment

router#sh shell environment
# Environment Variables:
# User Environment Variables:
?=1
# Global Environment Variables:
# Builtin Environment Variables:
PATH=CLI%Userfunctions%Builtins%SYSTEM
# Environment Functions:

# User Environment Functions:

# Global Environment Functions:

# Builtin Environment Functions:

Function namespace: DEFAULT
((              evaluate a numeric test expression


Function namespace: DEFAULT
[[              evaluate a logical test expression


Function namespace: DEFAULT
cat             output data from a pipe or file to the terminal


Function namespace: DEFAULT
cut             edit piped output


Function namespace: DEFAULT
echo            echo arguments to the terminal


Function namespace: DEFAULT
false           return false in while or if expressions, and set the result


Function namespace: DEFAULT
fetch           return values from the configuration database


Function namespace: DEFAULT
grep            search for regular expressions in piped output or files


Function namespace: DEFAULT
head            print the first lines in the input


Function namespace: DEFAULT
interface       print interfaces that match the argument


Function namespace: DEFAULT
let             evaluate a numeric expression, and set the result


Function namespace: DEFAULT
man             print information for builtins


Function namespace: DEFAULT
more            page piped output to the terminal


Function namespace: DEFAULT
nl              number the lines in the input


Function namespace: DEFAULT
null            ignore the input


Function namespace: DEFAULT
printf          output formatted data to the terminal


Function namespace: DEFAULT
read            read input into variables


Function namespace: DEFAULT
set_oper        set operational values


Function namespace: DEFAULT
sleep           pause execution of the terminal


Function namespace: DEFAULT
sort            sort the input


Function namespace: DEFAULT
tail            print the tail of the input


Function namespace: DEFAULT
true            return true in while or if expressions, and set the result


Function namespace: DEFAULT
uname           print system information


Function namespace: DEFAULT
wc              count lines, words, and chars

Example

router#sh int desc | grep up
Et0/0                          up             up
Et0/1                          up             up
Lo0                            up             up
NV0                            up             up
Tu0                            up             up

router#sh int desc | grep up | wc -l
4

 

 

 

How to Leak routes Between Global and VRF with PBR

Diagram

Interface to Internet

interface Ethernet0/0
description Outside
ip vrf forwarding INTERNET
ip address 192.0.0.1 255.255.255.252

Interface Inside in the GRT (Global Routing Table)

interface Ethernet0/1
description Inside
ip address 192.168.1.254 255.255.255.0

Leaking default route to internet

ip route 0.0.0.0 0.0.0.0 Ethernet0/0 192.0.0.2

NAT configuration

ip nat inside source list NAT interface Ethernet0/0 overload
access-list NAT permit 192.168.1.0 0.0.0.255

PBR configuration

ip prefix-list PREF seq 5 permit 192.168.1.0/24
!
route-map PBR permit 10
  match ip address 101
  set global
!
access-list 101 permit ip any 192.168.1.0 0.0.0.255

interface Ethernet0/0
 ip policy route-map PBR

Verification

Router#sh ip nat translations
Pro Inside global Inside local Outside local Outside global
icmp 192.0.0.1:7 192.168.1.1:7 8.8.8.8:7 8.8.8.8:7

Router#sh ip policy
Interface      Route map
Ethernet0/0    PBR

Router#sh route-map
route-map PBR, permit, sequence 10
  Match clauses:
    ip address (access-lists): 101
  Set clauses:
    global
  Policy routing matches: 25 packets, 2850 bytes