Octet

Push VLANs to access switches with an Ansible resource module

AnsibleWorking

Tasks

  1. Install the collections and set up ansible.cfg.
  2. Write an inventory with an access group and the connection variables.
  3. Keep the VLAN list in group_vars/access/vlans.yml and the password in an encrypted vault file.
  4. Write a playbook that merges the VLANs and saves the config only if it changed.
  5. Run it in check mode, then for real, then again to prove idempotency.

Solution

  1. 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 = 60

    Host key checking stays on. SSH once from the control node to each switch, or load their keys into known_hosts, before the first run.

  2. 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 }}"
  3. 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>
  4. 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: modified

    merged adds or updates the listed VLANs and leaves any others alone. save_when: modified writes memory only if the running config differs from the startup config.

  5. 5Dry run, then apply
    Linux
    ansible-playbook vlans.yml --check --diff --ask-vault-pass
    ansible-playbook vlans.yml --ask-vault-pass

Verify

SRV1 output
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=0

First run creates the three VLANs on both switches and saves the config.

SRV1 output
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=0

The second run sends no commands and reports changed=0. That is idempotency.

ASW1 output
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/unsup

VLANs 10, 20 and 30 exist with the right names.

Mistakes people make

  • Using state: overridden by habit. It deletes every VLAN that is not in your list.
  • Putting ansible_network_os: ios instead of the full name cisco.ios.ios on current collections.
  • Logging in with an account that lands in user mode and forgetting ansible_become: true with ansible_become_method: enable.
  • Skipping the check-mode run. It is the cheapest review you will ever get.
  • Saving with write memory on every run, which makes the play report changed forever.

On other platforms

Arista EOS uses the same resource module pattern and data shape.

Ansible
- name: VLANs on EOS
  arista.eos.eos_vlans:
    config: "{{ vlans }}"
    state: merged

Junos names the VLAN first and the ID second, but the idea is the same.

Ansible
- name: VLANs on Junos
  junipernetworks.junos.junos_vlans:
    config:
      - name: USERS
        vlan_id: 10
    state: merged

Say 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."