Lesson 2 of 7

IOS and IOS-XE Modules

Objective

In this lesson you will learn to use the ios_facts, ios_command, and ios_config Ansible modules (FQCN) to discover, verify, and configure IOS / IOS‑XE devices. You will gather device facts, run ad‑hoc show commands, and push configuration (hostname and a loopback) using Ansible against devices in the lab topology. In production, these tasks are common when onboarding devices, verifying baseline state, and applying standard configuration templates across many routers/switches.

Quick Recap

We continue with the fabric topology introduced in Lesson 1. No new physical devices are added in this lesson — we will operate against the same hosts and IP addresses.

ASCII Topology (simplified, with exact IPs from the reference):

                 +-----------------+
                 |     Spine 1     |
                 |     S1          |
                 | 10.15.1.11      |
                 +--------+--------+
                          |
                          |
                 +--------+--------+
                 |     Spine 2     |
                 |     S2          |
                 | 10.15.1.12      |
                 +--------+--------+
                          |
   +----------------------+------+----------------+
   |                         |                    |
+--+--+                 +----+----+           +---+----+
| Leaf |                 | Leaf  |           | Leaf   |
| L1   |                 | L2    |           | L3     |
|10.15.1.13|             |10.15.1.14|       |10.15.1.15|
+-------+                 +--------+           +--------+

Device table

RoleHostname (logical)Management IP
SpineS110.15.1.11
SpineS210.15.1.12
LeafL110.15.1.13
LeafL210.15.1.14
LeafL310.15.1.15

Tip: Throughout this lesson we use the exact IPs listed above. For domain names use lab.nhprep.com. Passwords in examples use Lab@123.

Key Concepts (theory + practical)

  • Ansible FQCN and network_cli — Always use the Fully Qualified Collection Name (FQCN) for modules (for example cisco.ios.ios_facts). Ansible talks to IOS devices via the network_cli connection. In production this ensures consistent module resolution and predictable behavior across control nodes.
  • ios_facts gathers read‑only stateios_facts queries a device for structured data (platform, version, interfaces, serial numbers). This is how automation confirms device identity before making changes, avoiding accidental changes to the wrong platform.
  • ios_command runs arbitrary show commands — When a module for a particular function doesn't exist (or you need a precise output), ios_command sends show commands and returns the raw output. It's equivalent to typing show version or show ip interface brief at the CLI.
  • ios_config pushes intended config idempotentlyios_config sends configuration commands. Using state merge or explicit config blocks allows automation to apply only necessary changes. In production, this is used to enforce baseline configurations and reduce human error.
  • Verification is critical — After applying config, always verify with both Ansible facts/commands and device show outputs. Automation can (and should) check result code and re‑read state to ensure convergence.

Step-by-step configuration

Step 1: Create Ansible inventory and group variables

What we are doing: Build an inventory that declares the devices we will manage and the connection parameters. This is the foundation for all subsequent Ansible tasks — without a correct inventory, Ansible cannot reach the devices.

all:
  vars:
    ansible_connection: ansible.netcommon.network_cli
    ansible_user: "nxos_username"
    ansible_password: "Lab@123"
    ansible_network_os: cisco.ios.ios
  children:
    spines:
      hosts:
        10.15.1.11:
        10.15.1.12:
    leafs:
      hosts:
        10.15.1.13:
        10.15.1.14:
        10.15.1.15:

What just happened: This YAML tells Ansible to use network_cli to reach all devices (connects via SSH and uses the network CLI transport). ansible_user and ansible_password define the credentials; ansible_network_os indicates the module collection to use. Grouping into spines and leafs allows targeted playbooks.

Real-world note: Inventory grouping is how you can apply different baseline roles to spines vs leafs across hundreds of devices.

Verify:

# From the control node, list inventory hosts
ansible-inventory --list -i inventory.yml

# Expected output (trimmed for clarity but showing hosts):
{
    "_meta": {
        "hostvars": {
            "10.15.1.11": {},
            "10.15.1.12": {},
            "10.15.1.13": {},
            "10.15.1.14": {},
            "10.15.1.15": {}
        }
    },
    "all": {
        "children": [
            "spines",
            "leafs"
        ],
        "vars": {
            "ansible_connection": "ansible.netcommon.network_cli",
            "ansible_user": "nxos_username",
            "ansible_password": "Lab@123",
            "ansible_network_os": "cisco.ios.ios"
        }
    },
    "spines": {
        "hosts": [
            "10.15.1.11",
            "10.15.1.12"
        ]
    },
    "leafs": {
        "hosts": [
            "10.15.1.13",
            "10.15.1.14",
            "10.15.1.15"
        ]
    }
}

Step 2: Gather facts from devices with ios_facts

What we are doing: Use cisco.ios.ios_facts to collect device facts (platform, version, uptime, interfaces). Gathering facts before making changes lets automation confirm the device type and current state.

- name: Gather IOS facts from all devices
  hosts: all
  gather_facts: no
  tasks:
    - name: Collect IOS facts
      cisco.ios.ios_facts:
      register: ios_facts_result

What just happened: Ansible connected to each host and ran the module which issues the appropriate show commands under the hood (for example show version, show inventory, show ip interface brief). The returned data is stored in ios_facts_result — a structured dictionary you can reference in later tasks to make decisions.

Real-world note: In large deployments you might gate configuration changes by facts (for example, only apply a template if the OS version meets a minimum).

Verify:

# Run a playbook to gather facts
ansible-playbook -i inventory.yml gather_facts.yml

# Expected snippet of output for one device (full JSON-like fact data):
TASK [Collect IOS facts] *******************************************************************
ok: [10.15.1.13] => {
    "ansible_facts": {
        "ansible_net_version": "16.9.4",
        "ansible_net_hostname": "L1",
        "ansible_net_model": "ISR4451",
        "ansible_net_interfaces": {
            "GigabitEthernet0/0": {},
            "Loopback0": {}
        }
    },
    "changed": false
}

Step 3: Run ad-hoc show commands with ios_command

What we are doing: Use cisco.ios.ios_command to run specific show commands, for example show version and show ip interface brief. This is useful when you need parsed output not available via facts, or to collect a specific verification snapshot.

- name: Run show commands on leafs
  hosts: leafs
  gather_facts: no
  tasks:
    - name: Run show version and show ip interface brief
      cisco.ios.ios_command:
        commands:
          - show version
          - show ip interface brief
      register: show_output

What just happened: Ansible sent the two show commands to each leaf device and returned the raw output as text in show_output. Under the hood, the module handles prompts and pagination so automation gets deterministic output.

Real-world note: Use ios_command when you need exact CLI output (for auditing) or to capture information not included in facts.

Verify:

# Execute the playbook
ansible-playbook -i inventory.yml show_commands.yml

# Expected output snippet for one device:
TASK [Run show version and show ip interface brief] ***************************************
ok: [10.15.1.13] => {
    "stdout": [
        [
            "Cisco IOS XE Software, Version 16.09.04",
            "Technical Support: http://www.cisco.com/techsupport",
            "ROM: Bootstrap program is C1100"
        ],
        [
            "Interface              IP-Address      OK? Method Status                Protocol",
            "GigabitEthernet0/0     10.15.1.13      YES manual up                    up",
            "Loopback0              unassigned      YES manual administratively down down"
        ]
    ],
    "changed": false
}

Step 4: Configure hostname and a Loopback with ios_config

What we are doing: Use cisco.ios.ios_config to set a standardized hostname and create a Loopback0 with a /32 address. This demonstrates idempotent configuration changes — Ansible will only push changes if they are not already present.

- name: Configure hostname and Loopback0
  hosts: leafs
  gather_facts: no
  tasks:
    - name: Set hostname to match inventory IP (example)
      cisco.ios.ios_config:
        lines:
          - hostname L-{{ inventory_hostname | replace('.','-') }}
    - name: Create Loopback0 with unique address per device
      cisco.ios.ios_config:
        lines:
          - interface Loopback0
          - ip address 10.15.99.{{ inventory_hostname.split('.')[-1] }} 255.255.255.255

What just happened: The first ios_config block entered global configuration mode and set the device hostname. The second block created Loopback0 and assigned a /32 using the last octet of the management IP to guarantee uniqueness. ios_config sends the commands via configuration mode and can detect whether the commands resulted in changes.

Real-world note: Using variables (like the inventory hostname) to generate unique addresses is a common pattern for predictable device addressing during provisioning.

Verify:

# Run the playbook to apply config
ansible-playbook -i inventory.yml configure_loopbacks.yml

# Expected verification using device show commands:
# On device 10.15.1.13
show running-config | include hostname
hostname L-10-15-1-13

show ip interface brief | include Loopback0
Loopback0              10.15.99.13     YES manual up                    up

Step 5: Re-gather facts and validate configuration

What we are doing: Re-run ios_facts and ios_command to confirm the hostname and loopback are present, ensuring idempotency and correctness.

- name: Validate config changes
  hosts: leafs
  gather_facts: no
  tasks:
    - name: Re-gather facts
      cisco.ios.ios_facts:
      register: facts_after
    - name: Check loopback via show ip interface brief
      cisco.ios.ios_command:
        commands:
          - show ip interface brief | include Loopback0
      register: loopback_check

What just happened: This playbook re-queries the devices; facts_after should reflect the new hostname and interface list, while loopback_check contains the show output confirming the loopback state. If automation finds discrepancies, you can have conditional tasks trigger remediation.

Real-world note: Always validate after changes — automation that doesn't validate risks drifting state across the fleet.

Verify:

# Expected snippets after validation
TASK [Re-gather facts] *********************************************************************
ok: [10.15.1.13] => {
    "ansible_facts": {
        "ansible_net_hostname": "L-10-15-1-13",
        "ansible_net_interfaces": {
            "Loopback0": {...}
        }
    },
    "changed": false
}

TASK [Check loopback via show ip interface brief] *****************************************
ok: [10.15.1.13] => {
    "stdout": [
        [
            "Loopback0              10.15.99.13     YES manual up                    up"
        ]
    ],
    "changed": false
}

Verification Checklist

  • Check 1: Inventory resolves all five devices — run ansible-inventory --list -i inventory.yml and confirm 10.15.1.11–10.15.1.15 appear.
  • Check 2: Facts gathered — run the facts playbook and confirm ansible_net_hostname and ansible_net_version are present for each device.
  • Check 3: Configuration applied — on each leaf (10.15.1.13, .14, .15) verify show running-config | include hostname shows the configured hostname and show ip interface brief | include Loopback0 shows the /32 address.

Common Mistakes

SymptomCauseFix
Ansible cannot connect to devicesWrong ansible_connection or incorrect credentialsEnsure ansible_connection: ansible.netcommon.network_cli and ansible_password: "Lab@123" are correct; verify SSH reachability to the management IPs
Module not found / wrong FQCNUsing non‑qualified module names or wrong collectionUse FQCN like cisco.ios.ios_config; install the collection if needed
Loopback not appearing after playbookTemplate variable produced invalid IP or command failedCheck task lines: formatting and verify the computed IP (e.g., last octet extraction) — rerun with -vvv to inspect task details
Changes re-applied on every run (not idempotent)Commands pushed in a way Ansible cannot detect state (e.g., using raw commands)Use ios_config with proper lines or use state: merged patterns to ensure idempotency

Key Takeaways

  • Always use the module FQCN (for example cisco.ios.ios_facts, cisco.ios.ios_command, cisco.ios.ios_config) and network_cli for consistent, predictable automation.
  • Use ios_facts before making changes to validate device identity and OS version — this prevents misconfigurations across different platforms.
  • Prefer ios_config for configuration to achieve idempotent changes; use ios_command when you need specific show output.
  • Verify every change programmatically: re-gather facts and run show commands to confirm success. In production, this verification step is essential for safe, automated operations.

Warning: In multi‑vendor environments, verify the ansible_network_os and target collection before running config playbooks — applying IOS commands to a NX‑OS device (or vice versa) will cause failures.