Series overview | Previous: Collect Cisco Syslog with Rsyslog and MySQL | Next: Trigger Ansible Playbooks from Cisco Syslog Events
Part 3 of 5: In Part 3, we build the Ansible playbooks and Jinja2 templates that translate an approved MAC-address match into a Cisco access-port, wireless trunk, 802.1X reset, or BPDU Guard response.
Safety note: Use an isolated lab, replace every example value, protect all credentials, maintain console access, and back up configurations before allowing automated changes.
Configure Ansible
Create the Ansible directories, data files, and log directory used by this guide.
sudo mkdir -p /etc/ansible/playbooks
sudo touch /etc/ansible/ansible.cfg
sudo touch /etc/ansible/DeviceMacs.txt
sudo touch /etc/ansible/OUIs.txt
sudo touch /etc/ansible/Wireless.txt
sudo mkdir -p /var/log/rsyslog
Create the Ansible configuration file. Disabling host-key checking is convenient in a lab but weakens protection against impersonation. In production, keep host-key checking enabled and manage known host keys.
sudo nano /etc/ansible/ansible.cfg
[defaults]
host_key_checking = True
Once this phase is complete, we will create playbooks to automate actions triggered by specific syslog messages received by the system. The initial playbook will manage switch port configurations based on MAC addresses. To facilitate this, we maintain three regularly updated files: one with full MAC addresses, including descriptions and assigned VLANs; a second containing OUI addresses alongside their descriptions and VLAN assignments; and a third exclusively for our wireless access point MAC addresses. Because our wireless access points require trunk ports, their MAC addresses are kept separate from those of other devices.
switch_port_config.yml
Replace the sample switch account with a dedicated automation account. Do not publish or store a real password directly in the playbook; encrypt it with Ansible Vault or retrieve it from your organization’s secrets manager. The commented email task can be enabled after you supply your relay and recipient information.
sudo nano /etc/ansible/playbooks/switch_port_config.yml
# Purpose: Ansible playbook to apply port configuration template to switch port indicated in syslog msg.
#Created by Matthew
#Last modified by Anthony
#
#Change Log
#7/30/2024 00:00 - Created file - Matthew
#7/31/2024 11:42 - Changed script names to make more sense - Anthony
#
#
#Populates variables from bash script for us in template
- name: Switchport Configuration Playbook
hosts: localhost
gather_facts: false
vars:
ip_address: ""
macaddress: ""
interface: ""
tasks:
- name: Print extracted details
debug:
msg: "IP Address: {{ ip_address }}, MAC Address: {{ macaddress }}, Interface: {{ interface }}"
- name: Add Host to memory
add_host:
name: "{{ ip_address }}"
#Assigns which host to run below tasks on
- hosts: "{{ ip_address }}"
gather_facts: false
vars:
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: my_username
ansible_password: my_password
tasks:
#Imports files for comparisons in template
- name: Import File
shell: cat /etc/ansible/DeviceMacs.txt
register: deviceMacs
delegate_to: localhost
- name: Import File
shell: cat /etc/ansible/OUIs.txt
register: ouiMacs
delegate_to: localhost
- name: Import File
shell: cat /etc/ansible/Wireless.txt
register: wirelessMacs
delegate_to: localhost
#Configures port based on template and variables.
- name: Configure Port
cli_command:
command: "{{ lookup('template','./switch_port_config.j2') }}"
register: portConfigResults
- name: Show rendered interface configuration
debug:
msg: "{{ lookup('template','./switch_port_config.j2') }}"
- name: debug
debug:
var: portConfigResults.stdout_lines
#Email notification of changes
# - name: Email each change
# community.general.mail:
# host: email server hostname
# sender: AutoIT@domain.name
# to:
# - test@domain.name
# - tech@domain.name
# subject: "Ansible Configured a Switch port"
# body:
# - "Switch IP Address: {{ ip_address }}, Device MAC Address: {{ macaddress }}, Switch Interface: {{ interface }}"
# subtype: html
# delegate_to: localhost
To generate the precise commands to be entered on the switch, we create a Jinja2 template named switch_port_config.j2
sudo nano /etc/ansible/playbooks/switch_port_config.j2
## Purpose: Configures port template based on provided mac address and returns template to ansible playbook to be executed on given port.
##Created by Matthew
##Last modified by Anthony
##
##Change Log
##7/30/2024 00:00 - Created file - Matthew
##7/31/2024 11:42 - Changed file names to make more sense - Anthony
##
##
## compares full mac address to DeviceMacs.txt and templates as necessary
{% if macaddress in deviceMacs.stdout %}
{% set matched_entry = deviceMacs.stdout_lines | select('search', '^' + macaddress + ',') | list | first %}
{% set deviceMacVlan = matched_entry.split(',')[1] %}
{% set deviceMacDescription = matched_entry.split(',')[2] %}
config t
default interface {{ interface }}
interface {{ interface }}
shutdown
description {{ deviceMacDescription }}
switchport access vlan {{ deviceMacVlan }}
switchport mode access
switchport port-security mac-address sticky
switchport port-security
storm-control broadcast level 5.00
storm-control multicast level 5.00
storm-control action shutdown
spanning-tree portfast
spanning-tree bpduguard enable
spanning-tree guard root
no shutdown
end
wri
##
{% elif '1c7d.22' in macaddress %}
config t
default interface {{ interface }}
interface {{ interface }}
shutdown
description Ansible_Xerox
switchport access vlan 20
switchport mode access
switchport port-security mac-address sticky
switchport port-security maximum 2
switchport port-security
storm-control broadcast level 5.00
storm-control multicast level 5.00
storm-control action shutdown
spanning-tree portfast
spanning-tree bpduguard enable
spanning-tree guard root
no shutdown
end
wri
## compares oui of mac address to ouis.txt and templates as necessary
{% elif macaddress[:7] in ouiMacs.stdout %}
{% set matched_entry2 = ouiMacs.stdout_lines | select('search', '^' + macaddress[:7] + ',') | list | first %}
{% set ouiMacVlan = matched_entry2.split(',')[1] %}
{% set ouiMacDescription = matched_entry2.split(',')[2] %}
config t
default interface {{ interface }}
interface {{ interface }}
shutdown
description {{ ouiMacDescription }}
switchport access vlan {{ ouiMacVlan }}
switchport mode access
switchport port-security mac-address sticky
switchport port-security
storm-control broadcast level 5.00
storm-control multicast level 5.00
storm-control action shutdown
spanning-tree portfast
spanning-tree bpduguard enable
spanning-tree guard root
no shutdown
end
wri
##configures port for Wireless AP
{% elif macaddress[:7] in wirelessMacs.stdout %}
config t
default interface {{ interface }}
interface {{ interface }}
shutdown
description Ansible_AP
switchport trunk native vlan 420
switchport trunk allowed vlan 20,420,520,1234
switchport mode trunk
power inline four-pair forced
storm-control broadcast level 5.00
storm-control multicast level 5.00
storm-control action shutdown
spanning-tree bpduguard enable
spanning-tree guard root
spanning-tree portfast trunk
no shutdown
end
wri
{% else %}
skip_mac_address_not_found
{% endif %}
The playbook above configures port security on all specified ports except wireless access points. To resolve issues caused by ports being disabled due to port security, we created a playbook that resets the port to 802.1x and applies security settings.
sudo nano /etc/ansible/playbooks/resetdot1x.yml
# Purpose: Playbook resets interface for 802.1x operations after an err-disable syslog message from switch
#Created by: Anthony
#Last modified by:
#Logging to: /var/log/ansible_playbook_output.log
#
#Change Log
#7/30/2024 00:00 - Created file
#
#
#
---
#Configures the variables to be used and logs them to the screen/logfile refer to /var/log/ansible_playbook_output.log
- name: Playbook for rsyslog integration
hosts: localhost
vars:
ip_address: ""
macaddress: ""
interface: ""
tasks:
- name: Print extracted details
debug:
msg: "IP Address: {{ ip_address }}, MAC Address: {{ macaddress }}, Interface: {{ interface }}"
- name: Add Host to memory
add_host:
name: "{{ ip_address }}"
# sets the target machine for play to be executed on
- hosts: "{{ ip_address }}"
vars:
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: switch_username
ansible_password: switch_password
gather_facts: false
tasks:
# Uses the provided variables and apply a configuration template to the port.
- name: Configure Port
cli_command:
command: "{{ lookup('template','./resetdot1x.j2') }}"
- name: debug
debug:
msg: "{{ lookup('template','./resetdot1x.j2') }}"
The accompanying Jinja2 template returns the interface to the organization’s standard 802.1X configuration. Replace the example voice and fallback VLANs before testing.
sudo nano /etc/ansible/playbooks/resetdot1x.j2
## Purpose: Configure switchport for 802.1x operations
##Created by: Anthony
##Last modified by:
##
##Change Log
##7/30/2024 00:00 - Created file
##
##
##
config t
default interface {{ interface }}
interface {{ interface }}
shutdown
switchport mode access
switchport voice vlan 120
load-interval 60
authentication event server dead action authorize vlan 999
authentication event no-response action authorize vlan 999
authentication event server alive action reinitialize
authentication order dot1x
authentication priority dot1x
authentication port-control auto
authentication periodic
authentication violation replace
no snmp trap link-status
dot1x pae authenticator
dot1x timeout quiet-period 2
dot1x timeout tx-period 3
storm-control broadcast level 5.00
storm-control multicast level 5.00
storm-control action shutdown
spanning-tree portfast
spanning-tree bpduguard enable
spanning-tree guard root
no shutdown
end
wri
The final playbook we developed is designed to notify us immediately whenever a switch port is disabled by BPDU Guard. It automatically disables the port permanently and sends an email alert to keep us informed of the action.
sudo nano /etc/ansible/playbooks/bpduguardDisable.yml
---
# Purpose: Ansible playbook to disable ports that are err-diable due to bpduguard indicated in syslog msg.
#Created by Anthony
#Last modified by Anthony
#
#Change Log
#11/20/2024 7:34AM - Created file - Anthony
#
#
#
#Populates variables from bash script for us in template
- name: Playbook for BPDUGuard port disable and report
hosts: localhost
gather_facts: false
vars:
ip_address: ""
macaddress: ""
interface: ""
tasks:
- name: Print extracted details
debug:
msg: "IP Address: {{ ip_address }}, MAC Address: {{ macaddress }}, Interface: {{ interface }}"
- name: Add Host to memory
add_host:
name: "{{ ip_address }}"
#Assigns which host to run below tasks on
- hosts: "{{ ip_address }}"
gather_facts: false
vars:
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: switch_username
ansible_password: switch_password
tasks:
#Configures port based on template and variables.
- name: Configure Port
cli_command:
command: "{{ lookup('template','./bpduguardDisable.j2') }}"
#Email notification of changes
- name: Email on disabling of port
community.general.mail:
host: mail_server
sender: AutoIT@domain.local
to:
- test@domain.local
- tech@domain.local
subject: "Ansible DISABLED a Switch port"
body:
- "IP Address: {{ ip_address }}, Interface: {{ interface }} has been disabled due to bpduguard"
subtype: html
delegate_to: localhost
Following is the jinja2 template for the commands.
sudo nano /etc/ansible/playbooks/bpduguardDisable.j2
## Purpose: Disables port due to bpduguard and alerts via email.
##Created by Anthony
##
##
##Change Log
##11/20/24 07:38AM - Created file - Anthony
##
##
##
## Commands to disable port
config t
interface {{ interface }}
shutdown
description "Port Disabled due to BPDUGuard"
end
wri
Feel free to use the above templates to create your own playbooks for activation via syslog messages from your switches. Next, we will proceed to develop the next component of the application.
Expected Result
At the end of this part, every playbook passes a syntax check and can render the intended switch commands without exposing a production credential.
Troubleshooting
- Run
ansible-playbook --syntax-checkagainst every playbook before testing connectivity. - If a collection or module is missing, install the required Cisco and community collections and record their tested versions.
- If the rendered template is empty, verify the MAC format and the contents of
DeviceMacs.txt,OUIs.txt, andWireless.txt.
Series overview | Previous: Collect Cisco Syslog with Rsyslog and MySQL | Next: Trigger Ansible Playbooks from Cisco Syslog Events
Leave a Reply