Push VLANs to access switches with an Ansible resource module
AnsibleWorking
Tasks
- Install the collections and set up
ansible.cfg. - Write an inventory with an
accessgroup and the connection variables. - Keep the VLAN list in
group_vars/access/vlans.ymland the password in an encrypted vault file. - Write a playbook that merges the VLANs and saves the config only if it changed.
- Run it in check mode, then for real, then again to prove idempotency.
Solution
1Collections and ansible.cfg
Linux ansible-galaxy collection install cisco.ios ansible.netcommon # ansible.cfg [defaults] inventory = inventory.yml forks = 10 [persistent_connection] command_timeout = 60Host key checking stays on. SSH once from the control node to each switch, or load their keys into
known_hosts, before the first run.2Inventory
YAML # inventory.yml all: children: access: hosts: ASW1: ansible_host: 10.0.0.11 ASW2: ansible_host: 10.0.0.12 vars: ansible_connection: ansible.netcommon.network_cli ansible_network_os: cisco.ios.ios ansible_user: netops ansible_password: "{{ vault_netops_password }}"3VLAN data and vault
YAML # group_vars/access/vlans.yml vlans: - vlan_id: 10 name: USERS - vlan_id: 20 name: VOICE - vlan_id: 30 name: GUESTS # group_vars/access/vault.yml, created with: # ansible-vault create group_vars/access/vault.yml # and containing: # vault_netops_password: <the password>4Playbook
Ansible # vlans.yml - name: Standard VLANs on access switches hosts: access gather_facts: false tasks: - name: Merge VLANs cisco.ios.ios_vlans: config: "{{ vlans }}" state: merged register: vlan_result - name: Show commands sent ansible.builtin.debug: var: vlan_result.commands - name: Save if running differs from startup cisco.ios.ios_config: save_when: modifiedmergedadds or updates the listed VLANs and leaves any others alone.save_when: modifiedwrites memory only if the running config differs from the startup config.5Dry run, then apply
Linux ansible-playbook vlans.yml --check --diff --ask-vault-pass ansible-playbook vlans.yml --ask-vault-pass
Verify
SRV1# ansible-playbook vlans.yml --ask-vault-pass
PLAY [Standard VLANs on access switches] ***************************************
TASK [Merge VLANs] *************************************************************
changed: [ASW1]
changed: [ASW2]
TASK [Show commands sent] ******************************************************
ok: [ASW1] => {
"vlan_result.commands": [
"vlan 10",
"name USERS",
"vlan 20",
"name VOICE",
"vlan 30",
"name GUESTS"
]
}
ok: [ASW2] => {
"vlan_result.commands": [
"vlan 10",
"name USERS",
"vlan 20",
"name VOICE",
"vlan 30",
"name GUESTS"
]
}
TASK [Save if running differs from startup] ************************************
changed: [ASW1]
changed: [ASW2]
PLAY RECAP *********************************************************************
ASW1 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ASW2 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0First run creates the three VLANs on both switches and saves the config.
SRV1# ansible-playbook vlans.yml --ask-vault-pass
TASK [Merge VLANs] *************************************************************
ok: [ASW1]
ok: [ASW2]
TASK [Show commands sent] ******************************************************
ok: [ASW1] => {
"vlan_result.commands": []
}
ok: [ASW2] => {
"vlan_result.commands": []
}
TASK [Save if running differs from startup] ************************************
ok: [ASW1]
ok: [ASW2]
PLAY RECAP *********************************************************************
ASW1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ASW2 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0The second run sends no commands and reports changed=0. That is idempotency.
ASW1# show vlan brief
VLAN Name Status Ports
---- -------------------------------- --------- -------------------------------
1 default active Gi1/0/1, Gi1/0/2, Gi1/0/3, Gi1/0/4
10 USERS active
20 VOICE active
30 GUESTS active
1002 fddi-default act/unsup
1003 token-ring-default act/unsup
1004 fddinet-default act/unsup
1005 trnet-default act/unsupVLANs 10, 20 and 30 exist with the right names.
Mistakes people make
- Using
state: overriddenby habit. It deletes every VLAN that is not in your list. - Putting
ansible_network_os: iosinstead of the full namecisco.ios.ioson current collections. - Logging in with an account that lands in user mode and forgetting
ansible_become: truewithansible_become_method: enable. - Skipping the check-mode run. It is the cheapest review you will ever get.
- Saving with
write memoryon every run, which makes the play report changed forever.
On other platforms
Arista EOS uses the same resource module pattern and data shape.
- name: VLANs on EOS
arista.eos.eos_vlans:
config: "{{ vlans }}"
state: mergedJunos names the VLAN first and the ID second, but the idea is the same.
- name: VLANs on Junos
junipernetworks.junos.junos_vlans:
config:
- name: USERS
vlan_id: 10
state: mergedSay it in the interview
"VLANs live in group variables, the resource module compares them with the switch and sends only what is
missing, and merged leaves other VLANs alone. I always run check mode with diff first, then apply, then
run again to prove it reports zero changes. Saving uses save_when: modified so it stays idempotent too."