Skip to content

Free IT Guide

Part 3: Automate Cisco Switchports with Ansible and Jinja2

August 18, 2026 · Anthony Ventura

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-check against 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, and Wireless.txt.

Series overview | Previous: Collect Cisco Syslog with Rsyslog and MySQL | Next: Trigger Ansible Playbooks from Cisco Syslog Events

Leave a Reply

Your email address will not be published. Required fields are marked *