Cisco Full Interface Configuration Compliance Checking With Ansible
This runs through all of my Cisco switch ports and ensures they are compliant with my configuration templates. It then builds pretty reports and emails them along with a CSV of the info. I’m building the reporting piece based on my reporting blog post here. The good part about this automation is that it will look at the full interface config, no matter what is added, be it vlans, port security, dot1x, etc., it will catch and compare it all.
The premise here is that all interfaces should contain an identifying description like: adminuser, mfguser, printer, ap. Each one of these description types will have a template they should match. So in the above example there would be four base templates. Of course each site would have a different VLAN, but that’s the good part of using a Jinja2 template, I can do variable replacement on the VLAN portion very easily. The playbook will then do all the comparisons and build the report(I’ll get into further detail down below).
Video Demo
Playbooks And Templates
First, you can find all of my materials here.
Here are my Jinja2 template files. I was lazy and only created a couple for demo purposes, but you’d need one for each interface type.
This is the printer.yml template:
1 2 3 4 5 6 7 | interface {{ port_value.key }} description printer no ip address shutdown negotiation auto no mop enabled no mop sysid |
As you can see above I’m doing variable replacement for the interface name(that way it will match what is physically on the device itself). Now if this was a switchport, which in a real environment it would be, we’d have VLAN info that we would be replacing.
Now for the playbook. I have this one named check-port-main-role.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | --- # uses this role https://github.com/gregsowell/ansible-report - name: reporting non compliance on interface configs hosts: csr1000v gather_facts: false vars: int_descs: - adminuser - mfguser - printer - ap - uplink headers: Hostname,reason,config tasks: - name: use ios facts to grab interface desc with all interfaces cisco.ios.ios_facts: gather_subset: - interfaces gather_network_resources: - l2_interfaces register: port_info - name: make the collected info a list for looping over set_fact: port_list: "{{ port_info.ansible_facts.ansible_net_interfaces | dict2items }}" - name: loop through each interface and do the compliance checking. This calls another task file for the work. include_tasks: check-port-role.yml loop: "{{ port_list }}" loop_control: loop_var: port_value # when: port_value.value.description == 'printer' - name: send the compliance email ansible.builtin.include_role: name: ansible-report vars: action_type: email |
This playbook doesn’t do all of the work, but it gets us started. I’ll break the above playbook down into separate bites so I can talk about each individually.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | --- # uses this role https://github.com/gregsowell/ansible-report - name: reporting non compliance on interface configs hosts: csr1000v gather_facts: false vars: int_descs: - adminuser - mfguser - printer - ap - uplink headers: Hostname,reason,config |
In the above I setup the variables. int_descs are the interface description types that should be configured on our infrastructure. The switch ports can have additional info on the end, but should start with these basic descriptions to follow corporate policy. These descriptions will directly map to default templates(these are the templates described at the beginning of this section).
You’ll also notice the headers section at the end. These are the headers for the CSV file being created for the report.
1 2 3 4 5 6 7 8 9 10 11 12 | tasks: - name: use ios facts to grab interface desc with all interfaces cisco.ios.ios_facts: gather_subset: - interfaces gather_network_resources: - l2_interfaces register: port_info - name: make the collected info a list for looping over set_fact: port_list: "{{ port_info.ansible_facts.ansible_net_interfaces | dict2items }}" |
Here I’m starting the first two tasks. I’m using the ios_facts module to pull all of the interfaces on the device along with their associated descriptions. This works well if it is a single switch or a large stack of switches. Keep in mind that a switch stack has one management IP, and when I query that single device it will tell me about all interfaces across the stack.
Next I convert the port info into a list so that I’m able to loop over it.
1 2 3 4 5 6 7 8 9 10 11 12 | - name: loop through each interface and do the compliance checking. This calls another task file for the work. include_tasks: check-port-role.yml loop: "{{ port_list }}" loop_control: loop_var: port_value # when: port_value.value.description == 'printer' - name: send the compliance email ansible.builtin.include_role: name: ansible-report vars: action_type: email |
This is the final couple of tasks. I’ll start with the very last; it sends the report email attaching the CSV. This is using my report role.
The second to last task is what is really doing the work in this playbook. For each host I connect to this task will loop over each port. Each port it loops over it will run them through the task file named check-port-role.yml. This task file is what does the bulk of the processing.
check-port-role.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | --- - name: grab the port config and save to variable cisco.ios.command: commands: "show run int {{ port_value.key }} | b interface" register: port_config_1 - name: variable clean up. It has some undesirable stuff in. set_fact: port_config_1: "{{ port_config_1 | regex_replace('end') | trim}}" - name: pre populate the port config 2 variable set_fact: port_config_2: notfound - name: loop through description types and once found, set the port config 2 via the template of that name. when: port_value.value.description is search(item) set_fact: port_config_2: "{{ lookup('template', item + '.yml') }}" # ignore_errors: true loop: "{{ int_descs }}" - name: if desc not matched (it's not set properly) add line to report when: port_config_2 == 'notfound' ansible.builtin.include_role: name: ansible-report vars: action_type: write write_line: "{{ inventory_hostname }},int desc mismatch,{{ port_config_1.stdout[0] | trim }}" - name: if the desc is correct, but the config doesn't match the template, add a line to report when: port_config_1.stdout[0] | trim != port_config_2 | default('none') | trim and port_config_2 != 'notfound' ansible.builtin.include_role: name: ansible-report vars: action_type: write write_line: "{{ inventory_hostname }},config mismatch,{{ port_config_1.stdout[0] | trim }}" |
I’ll break this one into chunks again and explain what each does.
1 2 3 4 5 6 7 8 | - name: grab the port config and save to variable cisco.ios.command: commands: "show run int {{ port_value.key }} | b interface" register: port_config_1 - name: variable clean up. It has some undesirable stuff in. set_fact: port_config_1: "{{ port_config_1 | regex_replace('end') | trim}}" |
First things first I connect to the interface and pull it’s config. This will give me the “show run” version, so full CLI.
Next I do a little cleanup on the variable so that it’s the raw config itself.
1 2 3 4 5 6 7 8 9 10 | - name: pre populate the port config 2 variable set_fact: port_config_2: notfound - name: loop through description types and once found, set the port config 2 via the template of that name. when: port_value.value.description is search(item) set_fact: port_config_2: "{{ lookup('template', item + '.yml') }}" # ignore_errors: true loop: "{{ int_descs }}" |
Next I’ll prepopulate the second variable so that I can easily tell if the description was found.
Now I’ll loop through all of the known good interface descriptions and compare those to what is configured on the interface itself. Once it matches it will pull the correct jinja2 template file and save it in the variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | - name: if desc not matched (it's not set properly) add line to report when: port_config_2 == 'notfound' ansible.builtin.include_role: name: ansible-report vars: action_type: write write_line: "{{ inventory_hostname }},int desc mismatch,{{ port_config_1.stdout[0] | trim }}" - name: if the desc is correct, but the config doesn't match the template, add a line to report when: port_config_1.stdout[0] | trim != port_config_2 | default('none') | trim and port_config_2 != 'notfound' ansible.builtin.include_role: name: ansible-report vars: action_type: write write_line: "{{ inventory_hostname }},config mismatch,{{ port_config_1.stdout[0] | trim }}" |
These are the final two tasks. The first checks to see if the second port_config_2 variable says ‘notfound’. If it does that means that the interface description is wrong, so go ahead and write a line to the CSV saying as such.
The last task will also write a line in the CSV saying config mismatch IF the show run doesn’t match its corresponding template.
Here’s a sample report run from my test router:
Conclusion
This can easily be run against a small group of switches…or against thousands. It will allow you to quickly determine what devices are out of compliance for one reason or another so you can then do mitigation. In fact, I’m going to have a sister playbook that will allow you to modify the generated CSV here and use it for remediation.
If you have any questions or comments…ways you’d modify this to meet your needs, please let me know; I’d be happy to hear from you.