Chapter 6: Ansible for Device-Level Network Automation

Learning Objectives

6.1 Ansible for Network Automation Fundamentals

Pre-Check — Section 6.1

1. Why is Ansible's agentless architecture especially valuable for network devices like Cisco IOS XE routers?

A. Routers run special Ansible agent software that integrates with the CLI
B. IOS XE devices cannot run third-party agent software, but do have an SSH daemon that Ansible can use
C. Agentless architecture means Ansible uses SNMP instead of SSH
D. Ansible requires Python to be installed on managed network devices

2. Which Ansible connection plugin is the primary choice for CLI-based IOS XE automation?

A. ansible.netcommon.httpapi
B. ansible.builtin.ssh
C. ansible.netcommon.network_cli
D. ansible.netcommon.netconf

3. What command installs the cisco.ios Ansible Content Collection?

A. pip install cisco-ios
B. ansible-galaxy collection install cisco.ios
C. ansible-galaxy role install cisco.ios
D. apt install ansible-cisco-ios

4. What IOS XE configuration command enables NETCONF support?

A. ip restconf
B. yang enable
C. netconf-yang
D. ncclient activate

5. In an Ansible inventory, where should connection variables like ansible_connection and ansible_network_os be stored for a group named ios_devices?

A. Hardcoded directly under each host entry in hosts.yml
B. In group_vars/ios_devices/vars.yml
C. In ansible.cfg under [defaults]
D. In the playbook under vars:

Architecture: Control Node and Managed Nodes

Ansible uses an agentless push model. The control node (workstation or CI server) holds all playbooks, inventory, and modules. Managed nodes (IOS XE devices) require only SSH — no Python or agents. Ansible connects, executes, and disconnects, leaving no persistent footprint.

graph TD CN["Control Node
(Workstation / CI Server)
ansible-playbook site.yml"] subgraph CN_COMPONENTS["Control Node Components"] INV["Inventory
(hosts.yml)"] PB["Playbooks
(site.yml)"] VAULT["Vault
(Encrypted Creds)"] COLL["cisco.ios Collection
(Modules)"] end CN --> CN_COMPONENTS CN_COMPONENTS -->|"SSH — network_cli / netconf"| RTR1["rtr1
(IOS XE)"] CN_COMPONENTS -->|"SSH — network_cli / netconf"| RTR2["rtr2
(IOS XE)"] CN_COMPONENTS -->|"SSH — network_cli / netconf"| SW1["sw1
(IOS XE)"] style CN fill:#1a4a7a,color:#fff style CN_COMPONENTS fill:#f0f4f8,color:#333 style RTR1 fill:#2d6a2d,color:#fff style RTR2 fill:#2d6a2d,color:#fff style SW1 fill:#2d6a2d,color:#fff

Connection Plugins

PluginProtocolUse Case
ansible.netcommon.network_cliSSH + pseudo-terminal (Paramiko)CLI-based modules: ios_config, resource modules
ansible.netcommon.netconfNETCONF over SSH (ncclient, XML/YANG)YANG model-driven config; requires netconf-yang on device
ansible.netcommon.httpapiRESTCONF over HTTPSREST API-based platforms
flowchart TD START([Automating an IOS XE Device]) --> Q1{"Configuration\ntarget?"} Q1 -->|CLI commands / show output| Q2{"YANG model-\ndriven path?"} Q1 -->|YANG / structured data| NETCONF Q2 -->|No — standard CLI| NETCLI["ansible.netcommon.network_cli
Protocol: SSH + pseudo-terminal
Library: Paramiko
Modules: ios_config, ios_command,
all resource modules"] Q2 -->|Yes — NETCONF RPCs| NETCONF["ansible.netcommon.netconf
Protocol: NETCONF over SSH
Library: ncclient
Requires: netconf-yang on device"] NETCLI --> PREREQ1["Prerequisite:
SSH enabled on device
ansible_network_os: cisco.ios.ios"] NETCONF --> PREREQ2["Prerequisite:
Device(config)# netconf-yang
IOS XE 16.6+"] style START fill:#1a4a7a,color:#fff style NETCLI fill:#2d6a2d,color:#fff style NETCONF fill:#7a4a1a,color:#fff style PREREQ1 fill:#e8f5e9,color:#333 style PREREQ2 fill:#fff3e0,color:#333

The cisco.ios Collection and FQCNs

Install the collection with ansible-galaxy collection install cisco.ios. Always reference modules using Fully Qualified Collection Names (FQCNs):

cisco.ios.ios_interfaces
cisco.ios.ios_vlans
cisco.ios.ios_bgp_global

Inventory Design

Use YAML inventory with group_vars to separate connection variables from host definitions. Credentials should always reference Vault-encrypted variables, never be hardcoded.

# group_vars/ios_devices/vars.yml
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: admin
ansible_password: "{{ vault_password }}"
ansible_become: true
ansible_become_method: enable
ansible_become_password: "{{ vault_enable_password }}"

Key Points — Section 6.1

Post-Check — Section 6.1

1. A network engineer wants to automate IOS XE devices using YANG-modeled data paths. Which connection plugin and device prerequisite are required?

A. network_cli; no special device config needed
B. netconf; netconf-yang must be configured on the device
C. httpapi; RESTCONF must be enabled
D. netconf; Python must be installed on the device

2. What is the correct FQCN format for the IOS VLAN resource module?

A. ios.cisco.vlans
B. ios_vlans
C. cisco.ios.ios_vlans
D. ansible.netcommon.ios_vlans

3. An engineer sets ansible_password: "{{ vault_password }}" in vars.yml. Where does vault_password come from?

A. A default built into the cisco.ios collection
B. An environment variable set in the shell
C. A Vault-encrypted vault.yml file in the same group_vars directory
D. The ansible.cfg file

4. Which Ansible variable tells the network_cli plugin to use IOS-specific terminal handling?

A. ansible_platform: ios
B. ansible_network_os: cisco.ios.ios
C. ansible_device_type: iosxe
D. ansible_connection: cisco.ios

5. Which library does the network_cli connection plugin use to establish SSH sessions to IOS XE devices?

A. ncclient
B. netmiko
C. Paramiko
D. cryptography.io

6.2 Cisco IOS Ansible Modules

Pre-Check — Section 6.2

1. What is the fundamental difference between ios_config (imperative) and ios_interfaces (declarative resource module)?

A. ios_config only works on switches; resource modules only work on routers
B. ios_config pushes raw CLI lines; resource modules describe desired state and calculate needed changes
C. Resource modules require NETCONF; ios_config uses REST
D. ios_config is idempotent; resource modules are not

2. Why should gather_facts: false be set on all Ansible network plays?

A. It improves performance by caching facts from a previous run
B. Ansible's default fact gathering uses Linux-focused SSH commands that fail on network devices
C. Network devices do not support SSH fact gathering
D. Facts are not needed for network automation

3. Which state value for a resource module reads the current device config and returns it as structured YAML without making any changes?

A. merged
B. rendered
C. gathered
D. replaced

4. What idempotency pitfall exists with ios_config when using abbreviated IOS commands?

A. Abbreviated commands cause syntax errors in Ansible YAML
B. Abbreviated commands are not sent to the device correctly
C. Text comparison fails because int gi0/1 and interface GigabitEthernet0/1 look different even though they are equivalent
D. IOS XE does not accept abbreviated commands over SSH

5. Which state value for a resource module is the most dangerous because it removes any on-device configuration not explicitly listed in the playbook?

A. deleted
B. replaced
C. overridden
D. merged

Two Module Philosophies

Imperative modules (ios_config, ios_command) tell Ansible what commands to run. Declarative resource modules (ios_interfaces, ios_vlans, etc.) tell Ansible what the device should look like — the module figures out the commands.

ios_command — Running Show Commands

- name: Check interface status
  cisco.ios.ios_command:
    commands:
      - show ip interface brief
      - show version
  register: show_output

Use ios_command for reads and verification. It is not idempotent for writes — use resource modules or ios_config for configuration changes.

ios_config — Imperative Configuration Push

- name: Configure OSPF
  cisco.ios.ios_config:
    lines:
      - router ospf 1
      - router-id 10.0.0.1
      - passive-interface default
    save_when: modified

Idempotency is achieved via text comparison. Always use full, unabbreviated IOS syntax — abbreviated commands break idempotency because the running-config stores the expanded form.

ios_facts — Gathering Device Information

Call ios_facts explicitly for network plays (never rely on default fact gathering). Common facts: ansible_net_hostname, ansible_net_version, ansible_net_interfaces, ansible_net_neighbors.

Network Resource Modules and the state Parameter

StateAdds ConfigRemoves ConfigTouches Unlisted ResourcesRequires Device
mergedYesNoNoYes
replacedYesYes (within item)NoYes
overriddenYesYes (entire type)Yes — removes unlistedYes
deletedNoYesNoYes
gatheredNoNoNo (read-only)Yes
renderedNo (offline)
flowchart TD START([Choose a Resource Module State]) --> Q1{"What is the\ngoal?"} Q1 -->|"Add or update\nspecific items only"| MERGED["state: merged
Adds/updates listed items
Leaves all others untouched
Safest for day-to-day use"] Q1 -->|"Fully rewrite\nspecific items"| REPLACED["state: replaced
Rewrites each listed item entirely
Unlisted items are untouched
Removes unspecified attributes"] Q1 -->|"Enforce complete\ncompliance"| Q2{"Understand the\nrisk?"} Q1 -->|"Remove\nconfiguration"| DELETED["state: deleted
Removes listed resources
Restores defaults
Omit config: to delete ALL"] Q1 -->|"Audit current\ndevice state"| GATHERED["state: gathered
Reads device config
Returns structured YAML data
No changes made"] Q1 -->|"Generate commands\noffline (CI/CD)"| RENDERED["state: rendered
Produces IOS CLI commands
No device connection needed
Ideal for pipeline validation"] Q2 -->|"Yes — removes ALL\nunlisted resources"| OVERRIDDEN["state: overridden
Enforces full single source of truth
Deletes any resource not in playbook
CAUTION: include mgmt interfaces"] Q2 -->|"Not sure"| MERGED style MERGED fill:#2d6a2d,color:#fff style REPLACED fill:#7a4a1a,color:#fff style OVERRIDDEN fill:#7a1a1a,color:#fff style DELETED fill:#4a4a1a,color:#fff style GATHERED fill:#1a4a7a,color:#fff style RENDERED fill:#1a3a5a,color:#fff
Interactive: Resource Module State Selector
state: merged
Adds or updates only the resources you list in config:. All other on-device resources are left untouched. This is the safest choice for day-to-day provisioning.
adds config non-destructive idempotent

Key Points — Section 6.2

Post-Check — Section 6.2

1. A playbook task uses ios_interfaces with state: replaced to configure GigabitEthernet0/1 with only a description. What happens to the interface's existing IP address?

A. The IP address is preserved because replaced only adds configuration
B. The IP address is removed because replaced rewrites the entire listed interface, removing attributes not in the task
C. The IP address is moved to Loopback0 automatically
D. The task fails with an error about the missing IP address

2. A CI/CD pipeline needs to validate that proposed VLAN changes would produce the correct IOS CLI commands, without connecting to any device. Which state should be used?

A. gathered
B. merged with --check flag
C. rendered
D. overridden

3. An engineer runs an ios_interfaces task with state: overridden but forgets to include the management interface in the config list. What is the most likely outcome?

A. The playbook skips management interfaces automatically
B. The management interface IP is removed, cutting off SSH connectivity to the device
C. The task fails with an error before making any changes
D. Ansible prompts the operator to confirm before removing the management interface

4. Which ios_facts variable contains a dictionary of all interface details collected from the device?

A. ansible_net_all_ipv4_addresses
B. ansible_net_interfaces
C. ansible_net_neighbors
D. ansible_net_config

5. An ios_config task pushes int gi0/1 but the running-config shows interface GigabitEthernet0/1. What happens on subsequent playbook runs?

A. Ansible recognizes the equivalence and skips the task (ok)
B. The task reports changed on every run because the text comparison fails
C. IOS XE rejects the abbreviated form and returns an error
D. Ansible normalizes the command before comparing

6.3 Ansible Playbook Design Patterns

Pre-Check — Section 6.3

1. In Ansible variable precedence, which source has the highest priority and always wins?

A. Role defaults (defaults/main.yml)
B. Inventory group_vars
C. Play vars:
D. Extra vars (-e on command line)

2. What does a handler do in an Ansible play?

A. It runs before every task to check preconditions
B. It runs once at the end of a play, only if at least one task notified it
C. It runs after each individual task that changes the device
D. It validates playbook YAML syntax before execution

3. What is the purpose of running ansible-playbook site.yml --check --diff?

A. It checks syntax errors without connecting to devices
B. It performs a dry run showing what changes would be made, without actually applying them
C. It runs only tasks tagged with "check"
D. It compares two inventory files for differences

Three-Phase Playbook Structure

Best practice is to separate playbooks into audit, configure, and verify phases. This improves readability and enables targeted tag-based execution.

sequenceDiagram participant OP as Operator participant AN as Ansible Control Node participant DEV as IOS XE Device (rtr1) OP->>AN: ansible-playbook site.yml rect rgb(220, 235, 252) Note over AN,DEV: Phase 1 — Audit AN->>DEV: SSH connect AN->>DEV: ios_facts (gather_subset: all) DEV-->>AN: hostname, version, interfaces, neighbors AN->>AN: Store as ansible_net_* variables end rect rgb(220, 252, 220) Note over AN,DEV: Phase 2 — Configure AN->>DEV: ios_interfaces (state: merged) DEV-->>AN: changed / ok AN->>DEV: ios_vlans (state: merged) DEV-->>AN: changed / ok AN->>DEV: ios_l2_interfaces (state: merged) DEV-->>AN: changed / ok Note over AN: Handler notified by changes AN->>DEV: write memory (handler fires once) DEV-->>AN: ok end rect rgb(252, 245, 220) Note over AN,DEV: Phase 3 — Verify AN->>DEV: ios_facts (gather_subset: interfaces) DEV-->>AN: current interface state AN->>AN: Assert all interfaces up AN-->>OP: Play recap — ok/changed/failed counts end
Playbook Execution Flow
PLAY STARThosts: ios_devices | gather_facts: false
ios_factsCollect hostname, version, interfaces
ios_interfacesstate: merged — add/update listed interfaces
ios_vlansstate: merged — add/update listed VLANs
HANDLERwrite memory — fires once if any task changed
ios_commandVerify — show interfaces status
RECAPok=5 changed=2 failed=0

Tags for Selective Execution

# Run only NTP tasks
ansible-playbook site.yml --tags ntp

# Run all routing tasks (BGP + OSPF)
ansible-playbook site.yml --tags routing

# Skip baseline tasks
ansible-playbook site.yml --skip-tags baseline

Variable Precedence (Lowest to Highest)

  1. Role defaults (roles/name/defaults/main.yml)
  2. Inventory group_vars
  3. Inventory host_vars
  4. Play vars:
  5. Extra vars (-e key=value) — always wins

register, when, and loop

# Verification workflow with register + when
- name: Fail if BGP not established
  ansible.builtin.fail:
    msg: "BGP not up on {{ inventory_hostname }}"
  when: "'Established' not in bgp_summary.stdout[0]"

# Loop over a list
- name: Check routes
  cisco.ios.ios_command:
    commands:
      - "show ip route {{ item }}"
  loop:
    - "10.0.1.0"
    - "10.0.2.0"

Key Points — Section 6.3

Post-Check — Section 6.3

1. Ten tasks in a play all notify the same handler named "save ios config". How many times does the handler execute?

A. Ten times — once per notification
B. Once at the end of the play
C. Once immediately after the first notifying task
D. Zero times — handlers must be called explicitly

2. Which command runs only the tasks tagged routing in site.yml?

A. ansible-playbook site.yml --filter routing
B. ansible-playbook site.yml --run-tags routing
C. ansible-playbook site.yml --tags routing
D. ansible-playbook site.yml --only routing

3. A task uses register: bgp_summary on an ios_command. How is the text output of the first command accessed?

A. bgp_summary.output
B. bgp_summary.stdout[0]
C. bgp_summary.result
D. bgp_summary.commands[0]

6.4 Advanced Ansible Patterns

Pre-Check — Section 6.4

1. What encryption algorithm does Ansible Vault use to protect sensitive data?

A. RSA-2048
B. AES-256
C. SHA-512
D. Blowfish

2. What is the purpose of the "two-file vault pattern" in Ansible network automation?

A. To store two separate vault passwords for redundancy
B. A plaintext vars.yml references variables defined in an encrypted vault.yml, keeping playbooks readable while secrets stay encrypted
C. To split the vault key into two files for added security
D. To store device credentials in one file and API keys in another

3. In an Ansible role, where should default variable values (lowest precedence, easily overridden) be defined?

A. roles/role_name/vars/main.yml
B. roles/role_name/defaults/main.yml
C. group_vars/all/vars.yml
D. roles/role_name/tasks/main.yml

4. What does the block/rescue/always construct in Ansible resemble in general programming?

A. if/elif/else conditional
B. try/catch/finally error handling
C. for/while loop iteration
D. function/return/yield pattern

5. Why should role variable names be prefixed with the role name (e.g., ios_base_config_ntp_servers instead of ntp_servers)?

A. Ansible requires this naming convention to locate role variables
B. Prefixing improves performance by reducing variable lookup time
C. It prevents silent namespace collisions when multiple roles define variables with the same short name
D. Ansible Vault only encrypts variables with role-prefixed names

Roles: Reusable Automation Units

Roles bundle tasks, handlers, variables, and templates into a standardized directory structure. Always prefix role variables with the role name to prevent namespace collisions across roles.

roles/
└── ios_base_config/
    ├── tasks/main.yml        # Entry point
    ├── handlers/main.yml
    ├── defaults/main.yml     # Lowest precedence — easily overridden
    ├── vars/main.yml         # High precedence
    └── templates/banner.j2

Ansible Vault: Securing Credentials

The two-file vault pattern: vault.yml (AES-256 encrypted, safe to commit) holds actual secrets; vars.yml (plaintext) references them with Jinja2 variables.

graph TD subgraph VCS["Version Control (Git)"] VAULT_FILE["group_vars/ios_devices/vault.yml
(AES-256 encrypted)
vault_password: ciphertext"] VARS_FILE["group_vars/ios_devices/vars.yml
(plaintext — safe to commit)
ansible_password: {{ vault_password }}"] end subgraph SECRETS["Secret Storage (Never Committed)"] VAULT_PASS["~/.vault_pass
or CI/CD Pipeline Secret"] end VAULT_PASS -->|"--vault-password-file"| DECRYPT["Ansible Decrypts vault.yml
at Runtime"] VAULT_FILE --> DECRYPT VARS_FILE -->|"References vault variables"| RESOLVE["Variable Resolution"] DECRYPT --> RESOLVE RESOLVE -->|"SSH login"| DEVICE["IOS XE Device"] style VAULT_FILE fill:#7a1a1a,color:#fff style VARS_FILE fill:#2d6a2d,color:#fff style VAULT_PASS fill:#7a4a1a,color:#fff style DECRYPT fill:#1a4a7a,color:#fff style RESOLVE fill:#4a1a7a,color:#fff style DEVICE fill:#1a5a3a,color:#fff

Error Handling: block / rescue / always

tasks:
  - block:
      - name: Apply routing configuration
        cisco.ios.ios_ospfv2:
          config:
            processes:
              - process_id: 1
                router_id: "{{ ospf_router_id }}"
          state: merged

      - name: Verify OSPF neighbors formed
        cisco.ios.ios_command:
          commands:
            - show ip ospf neighbor
        register: ospf_verify
        failed_when: "'FULL' not in ospf_verify.stdout[0]"

    rescue:
      - name: Collect diagnostics on failure
        cisco.ios.ios_command:
          commands:
            - show ip ospf
            - show logging | last 20
        register: diagnostics

    always:
      - name: Record task completion
        ansible.builtin.debug:
          msg: "Finished for {{ inventory_hostname }}"

retries and until — Waiting for Convergence

- name: Wait for BGP to converge
  cisco.ios.ios_command:
    commands:
      - show bgp summary
  register: bgp_state
  retries: 6
  delay: 10
  until: "'Established' in bgp_state.stdout[0]"

Handler Guard in Check Mode

handlers:
  - name: save ios config
    cisco.ios.ios_command:
      commands:
        - write memory
    when: not ansible_check_mode  # Prevents save during --check runs

Key Points — Section 6.4

Post-Check — Section 6.4

1. An operator uses ansible-vault create group_vars/ios_devices/vault.yml and stores the vault password in ~/.vault_pass. Which command runs the playbook using that password file?

A. ansible-playbook site.yml --vault-key ~/.vault_pass
B. ansible-playbook site.yml --vault-password-file ~/.vault_pass
C. ansible-playbook site.yml --secret-file ~/.vault_pass
D. ansible-playbook site.yml --decrypt ~/.vault_pass

2. A role named ios_routing has a variable ntp_servers in its defaults/main.yml. Another role named ios_base_config also defines ntp_servers. What is the likely outcome and how should it be fixed?

A. Ansible throws a fatal error on duplicate variable names — rename one role
B. One variable silently overwrites the other — fix by prefixing: ios_routing_ntp_servers and ios_base_config_ntp_servers
C. Ansible merges the two lists automatically
D. The last role loaded always wins with no ambiguity

3. A task uses failed_when: "'FULL' not in ospf_verify.stdout[0]". What does this accomplish?

A. It skips the task if OSPF is not fully converged
B. It marks the task as failed (triggering rescue) if the word "FULL" does not appear in the show command output
C. It retries the task up to 3 times until "FULL" appears
D. It logs a warning but continues the play regardless

4. Why should when: not ansible_check_mode be added to a write memory handler?

A. Check mode disables all handlers by default anyway
B. It prevents the handler from actually saving configuration during a dry run (--check), which would be inappropriate
C. It speeds up check mode by skipping the SSH connection
D. write memory only works in check mode

5. Which task pattern waits for BGP to reach the Established state, checking every 10 seconds for up to 6 attempts?

A. wait_for: timeout=60 search_regex="Established"
B. retries: 6 delay: 10 until: "'Established' in bgp_state.stdout[0]"
C. loop: "{{ range(6) }}" pause: 10
D. failed_when: bgp_retries > 6

Your Progress

Answer Explanations