Ansible Network Modules: ios_config, Facts and Config Backup - 夜莺博客

Ansible Network Modules: ios_config, Facts and Config Backup

Ansible is the pragmatic entry point to network automation because it needs no agent and no API — just SSH. The failure modes are equally pragmatic: playbooks that are not idempotent, backups that silently overwrite each other, and configurations pushed without a rollback plan. This article shows the inventory and module patterns that hold up in production.

The focus here is narrow on purpose. Rather than touring every module the cisco.ios collection ships, we will dissect the three that carry most of the operational weight in a real IOS environment - ios_config, ios_facts and the backup machinery around them - and look at exactly where they surprise people. Everything below assumes SSH reachability and a working inventory; if you are still at the "what is a playbook" stage, the Ansible for network engineers complete guide is the better starting point.

Inventory: network_cli, not SSH

[all:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_user=ansible

[ios]
ios01.example.net
ios02.example.net

[ios:vars]
ansible_network_os=cisco.ios.ios
ansible_become=yes
ansible_become_method=enable
ansible_password=!vault |
  $ANSIBLE_VAULT;1.1;AES256
  3933623163613766396434...

Use ansible_connection=ansible.netcommon.network_cli with a per-platform ansible_network_os, and protect credentials with Ansible Vault instead of a plaintext group_vars file. Install the collection first: ansible-galaxy collection install cisco.ios.

The distinction between network_cli and the generic ssh connection is the single most common source of confusion for engineers coming from server automation. The generic SSH connection assumes a POSIX shell: it uploads a Python file, executes it, and reads back structured JSON. A Cisco switch has no Python interpreter and no shell, so the generic connection fails immediately. The network_cli connection plugin instead speaks the device's CLI: it logs in, handles the prompt, disables paging, and translates module parameters into CLI lines. That is why ansible_network_os is mandatory for network inventories and irrelevant for servers.

Two practical notes follow. First, the ansible_network_os value should be the fully qualified collection name (cisco.ios.ios) on modern Ansible releases; the short form ios still works in many cases but is deprecated and will eventually stop resolving. Second, ansible_become_method: enable with ansible_become: yes is how the plugin enters privileged EXEC mode. If your devices authenticate with AAA and the enable secret is managed by TACACS, test the login manually first - a device that silently drops into user EXEC mode produces some of the least readable error messages in the ecosystem.

Collections: Where the Modules Actually Live

Since Ansible 2.10, network modules are distributed as collections rather than bundled with the core engine. The naming convention is namespace.collection.module, so cisco.ios.ios_config means the ios_config module from the cisco.ios collection. In playbooks you should always write the fully qualified name; the bare ios_config shorthand still resolves in many installations thanks to redirects, but it breaks the moment two collections provide a module with the same name.

ansible-galaxy collection install cisco.ios ansible.netcommon
ansible-galaxy collection install cisco.iosxr cisco.nxos juniper.junos

# pin versions in a requirements.yml for reproducibility
cat requirements.yml
collections:
  - name: cisco.ios
    version: ">=8.0.0"
  - name: ansible.netcommon

Pinning versions matters more in network automation than in server automation, because a module update can change how a configuration is parsed. A collection upgrade that alters whitespace handling in ios_config can turn every device in your fleet into "changed" on the next run and hide genuine drift in the noise.

ios_config: parents, lines and idempotency

- name: Configure interface settings
  cisco.ios.ios_config:
    lines:
      - description Uplink to Core
      - ip address 10.0.0.1 255.255.255.0
    parents: interface GigabitEthernet1/0/1

- name: Configure ip helpers on multiple interfaces
  cisco.ios.ios_config:
    lines:
      - ip helper-address 172.26.1.10
      - ip helper-address 172.26.3.8
    parents: "{{ item }}"
  loop:
    - interface Ethernet1
    - interface Ethernet2

- name: Idempotency requires full-form commands
  cisco.ios.ios_config:
    lines:
      - shutdown          # 'shut' would be treated as a new line every run
    parents: interface GigabitEthernet1/0/11

parents is what makes a module call idempotent: the module compares the child lines inside the parent section and only pushes the difference. Two rules follow from that — always use the full command form (abbreviations defeat the comparison), and use match / replace deliberately when you want the device to mirror your list exactly rather than append to it.

The mechanism is worth understanding rather than memorising. For each entry in parents, the module enters that configuration context, then checks whether each line in lines already exists somewhere in the running configuration. If it does, nothing is pushed. If it does not, the missing lines are sent and the task reports changed. There is no semantic understanding of the CLI: the comparison is textual. This is why description Uplink to Core is idempotent but description Uplink To Core is a change, and why reordering lines inside a parent has no effect while renaming an interface does.

Anatomy of an ios_config Task

Beyond lines and parents, the parameters you will actually reach for:

  • src - a template or file containing a complete configuration block, used instead of lines for anything longer than a handful of commands.
  • match - line (default) compares individual lines; strict requires the lines to appear in the exact order; none disables comparison entirely and pushes everything.
  • replace - line (default) adds missing lines; block makes the parent section match your list exactly by removing anything not listed; block with match: none is a full overwrite and should be treated as a loaded weapon.
  • before / after - commands sent before and after the managed lines, useful for no shutdown ordering or for entering a context.
  • running_config - supply a saved configuration so the module does not have to fetch one, which speeds up large runs considerably.
  • defaults: yes - includes default-valued lines in the comparison, relevant when you are validating that an engineer did not disable something.
  • save_when - always, never, modified or changed, controlling when copy running-config startup-config is issued.

Getting replace: block right deserves a warning. On an interface where a colleague added an ACL line ten minutes ago, replace: block with a list that omits that ACL will remove it. That may be exactly what you intended - golden-config enforcement - or it may be an outage. Either way, decide it in code review rather than discovering it during a change window.

Backups that are actually usable

- name: Back up running configuration before any change
  cisco.ios.ios_config:
    backup: true
    backup_options:
      dir_path: /srv/backups/{{ inventory_hostname }}
      filename: "{{ lookup('pipe','date +%Y-%m-%d') }}-running.cfg"
    save_when: modified
  • Always set backup: true on the same task that changes configuration, not in a separate play that might not run.
  • Include a date in filename — the default name is fixed, so a nightly run silently overwrites yesterday’s copy.
  • save_when: modified writes startup-config only when the running config actually changed, which avoids a write on every playbook run.
  • For a fleet-wide backup without changes, use a dedicated playbook that only reads running_config and writes to disk, and keep it under version control.

The read-only backup playbook is the pattern most teams should build first, because it is safe, it is useful on day one, and it produces the artifact every later workflow depends on:

---
- name: Nightly configuration archive
  hosts: ios
  gather_facts: false
  tasks:
    - name: Collect running configuration
      cisco.ios.ios_command:
        commands: show running-config
      register: running

    - name: Write a dated copy to disk
      ansible.builtin.copy:
        content: "{{ running.stdout[0] }}\n"
        dest: "/srv/backups/{{ inventory_hostname }}/{{ lookup('pipe','date +%Y-%m-%d') }}-running.cfg"

    - name: Fail loudly if the backup looks truncated
      ansible.builtin.assert:
        that:
          - running.stdout[0] | length > 500
          - "'hostname' in running.stdout[0]"
        fail_msg: "Backup for {{ inventory_hostname }} looks empty or truncated"

That assertion is the difference between a backup you trust and a directory of zero-byte files. A device that reformats its prompt, a TACACS hiccup that drops the session halfway through the listing, or a module timeout that returns partial output all produce a file that exists, looks plausible, and is useless at restore time. Assert on a minimum length and a known marker on every run, and let the playbook fail loudly rather than storing rubbish.

Facts and conditional operations

- name: Gather facts
  cisco.ios.ios_facts:
    gather_subset: [config, hardware]

- name: Set boot image only when the version differs
  cisco.ios.ios_config:
    lines:
      - no boot system
      - boot system flash bootflash:{{ new_image }}
  when: ansible_net_version != version

- name: Report versions
  ansible.builtin.debug:
    msg: "{{ inventory_hostname }} runs {{ ansible_net_version }}"

Facts landing under ansible_net_* are what turn a playbook into a policy engine: only reload when the image changed, only touch VLANs on devices that are missing them, and only open a change ticket when drift exists. Combine this with diff_against: intended to compare running configuration with a golden file, and you have a compliance report that costs nothing to run nightly.

Be deliberate about the gather subset. The full gather_subset: all pulls configuration, hardware, interfaces and default data, which on a fully loaded chassis with thousands of interfaces can take tens of seconds and produce megabytes of variable data. In a fleet-wide playbook, gather hardware alone when you only need the model and version, and reserve config for the compliance runs that genuinely need it.

- name: Minimal facts for gating logic
  cisco.ios.ios_facts:
    gather_subset:
      - hardware
      - "!interfaces"
      - "!config"
  register: ios

- name: Only touch devices that are behind on NTP
  cisco.ios.ios_config:
    lines:
      - ntp server 10.10.10.1
  when: "'10.10.10.1' not in ansible_net_config"

Idempotency traps to avoid

  • Abbreviated commands - ip add 10.0.0.1 255.255.255.0 never matches the running config, which was written in full form. Always write the full command.
  • Case and whitespace - comparisons are textual. A stray double space in a template creates a perpetual changed.
  • Default values - a line that the device does not print because it matches the default will be re-pushed forever unless you set defaults: yes or avoid configuring defaults explicitly.
  • Ordering-sensitive commands - ACL entries are numbered and order matters; match: line is the safe choice, and you should design the task around the sequence numbers rather than hoping the device sorts them.
  • Auto-generated lines - timestamps, certificates and crypto keys embedded in configuration change on their own and will make diff_against reports noisy. Filter them out of the golden file.
  • Multi-line banners - banners need delimiter handling and a src template rather than lines, and they will teach you more about the parser than you wanted to learn.

Reading and validation with ios_command and cli_command

Sometimes you need output, not configuration. ios_command runs read-only show commands and returns the raw text, which is exactly what you want for a backup or a health report:

- name: Pre-flight checks before a change
  cisco.ios.ios_command:
    commands:
      - show ip interface brief
      - show standby brief
      - show version | include uptime
  register: preflight

- name: Show the pre-flight output for the change record
  ansible.builtin.debug:
    var: preflight.stdout_lines

cli_command is the vendor-neutral equivalent, and cli_config the vendor-neutral writer. Reach for them when a platform has no first-class module, and treat them as temporary scaffolding: they push text without comparison, so they are never idempotent and they will not tell you whether a change was necessary.

A validation and rollback workflow

Every configuration task should be surrounded by three cheap safeguards. Before the change, run the same playbook with --check --diff and have the resulting diff attached to the change ticket. During the change, keep backup: true on the mutating task and run with serial: 1 so a bad push stops at one device. After the change, run a read-only verification task that asserts on interface state, routing adjacencies or NTP synchronisation, and fail the run if the network is not healthy.

- name: Post-change verification
  cisco.ios.ios_command:
    commands:
      - show ip ospf neighbor
  register: ospf

- name: Assert the OSPF adjacency came back
  ansible.builtin.assert:
    that:
      - "'FULL' in ospf.stdout[0]"
    fail_msg: "OSPF adjacency not FULL on {{ inventory_hostname }} - consider restoring {{ backup_file }}"

A rollback plan, in practice, is the backup file plus a documented procedure for pushing it back - ios_config with src pointing at the saved configuration, or a manual configure replace where the platform supports it. Write the procedure down before you need it at three in the morning.

Common errors and what they usually mean

  • unable to connect to socket or a permanent UNREACHABLE - the connection plugin is ssh rather than network_cli, or ansible_network_os is missing.
  • Invalid input detected at '^' marker - the command was sent in the wrong configuration context; check that parents actually entered the intended block.
  • Task reports changed on every run with no visible difference - an abbreviated command, a casing mismatch, or a default value the device does not print.
  • timeout on large devices - raise command_timeout in the persistent connection section and gather a narrower fact subset.
  • Backups that are empty - an authorization prompt the connection plugin does not answer, or output paging that was not disabled; check with -vvv.

Related reading: Ansible network automation playbook examples, Ansible facts and configuration backup and Ansible for Cisco network automation 101.

原文链接:https://docs.ansible.com/projects/ansible/latest/collections/cisco/ios/ios_config_module.html