Chapter 6: Ansible for Device-Level Network Automation
Learning Objectives
Build Ansible playbooks using the cisco.ios collection to manage Cisco IOS XE devices
Configure Ansible inventory, variables, and connection parameters for network automation
Implement idempotent configuration management with Ansible network resource modules
Use Ansible roles, handlers, tags, and Vault for organized and secure network automation workflows
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.
Use YAML inventory with group_vars to separate connection variables from host definitions. Credentials should always reference Vault-encrypted variables, never be hardcoded.
Ansible is agentless: the control node pushes over SSH; managed nodes need no Python or special software.
network_cli (Paramiko SSH) is the primary connection plugin for IOS XE CLI automation; netconf requires netconf-yang on the device (IOS XE 16.6+).
Install cisco.ios with ansible-galaxy collection install cisco.ios; always reference modules by FQCN.
Store connection variables in group_vars/<group>/vars.yml and credentials in a Vault-encrypted vault.yml alongside it.
The ansible_network_os: cisco.ios.ios variable tells the network_cli plugin which platform-specific terminal handler to use.
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.
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
State
Adds Config
Removes Config
Touches Unlisted Resources
Requires Device
merged
Yes
No
No
Yes
replaced
Yes
Yes (within item)
No
Yes
overridden
Yes
Yes (entire type)
Yes — removes unlisted
Yes
deleted
No
Yes
No
Yes
gathered
No
No
No (read-only)
Yes
rendered
—
—
—
No (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 confignon-destructiveidempotent
Key Points — Section 6.2
Always use full IOS syntax with ios_config — abbreviated commands (e.g., int gi0/1) break idempotency because running-config stores the expanded form.
Set gather_facts: false on all network plays and use ios_facts explicitly; default fact gathering targets Linux and fails on network devices.
Resource modules provide true idempotency — they gather device state, compute the diff, and only push what is needed.
state: overridden is powerful but dangerous — it will remove any on-device resource not in your playbook, including management interfaces.
state: rendered generates CLI commands without connecting to any device — ideal for CI/CD pipeline validation before deployment.
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 START→hosts: ios_devices | gather_facts: false
HANDLER→write memory — fires once if any task changed
ios_command→Verify — show interfaces status
RECAP→ok=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)
Role defaults (roles/name/defaults/main.yml)
Inventory group_vars
Inventory host_vars
Play vars:
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
Structure playbooks into three phases: audit (ios_facts), configure (resource modules), verify (ios_command + assert).
Handlers run once per play at the end, not after each notifying task — ideal for write memory to avoid redundant saves.
Use --check --diff before every production change — resource modules natively support both modes.
Extra vars (-e) always override everything else — useful for one-time overrides but avoid in automation pipelines.
The register keyword combined with when enables powerful verification: run a show command, then conditionally fail or act based on output.
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.
The two-file vault pattern: vault.yml (AES-256 encrypted, safe to commit) holds actual secrets; vars.yml (plaintext) references them with Jinja2 variables.
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
Roles use a standardized directory structure; always prefix role variable names with the role name to prevent namespace collisions.
Ansible Vault uses AES-256 encryption; use the two-file pattern (vault.yml + vars.yml) — never commit plaintext passwords or vault password files to version control.
block/rescue/always is the structured error handling pattern for production playbooks — equivalent to try/catch/finally.
Add when: not ansible_check_mode to write memory handlers so dry runs do not save partial configuration.
Use retries + until to poll for convergence (BGP peering, device reload recovery) rather than arbitrary wait_for delays.
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]"