Ansible Network Examples: Inventory, Facts and cli_command - 夜莺博客

Ansible Network Examples: Inventory, Facts and cli_command

Ansible automates network devices through a connection layer that speaks the device CLI (ansible.netcommon.network_cli), and the official documentation set includes ready-made examples that work across Arista EOS, Cisco IOS and VyOS from one playbook. This article walks through those examples: building a multi-vendor inventory, collecting facts while backing up configurations, and running platform-independent commands with cli_command.

What You Need Before the First Run

Network automation with Ansible is controller-side: there is no agent on the switch, no Python to install on the router, and no package to push. The control node does all the work and the device only has to offer SSH. That single fact explains most of the setup steps below.

python3 -m pip install --user ansible
ansible-galaxy collection install ansible.netcommon
ansible-galaxy collection install arista.eos cisco.ios vyos.vyos

# Confirm the connection plugin and vendor modules are visible
ansible-doc -t connection ansible.netcommon.network_cli | head -20
ansible-doc cisco.ios.ios_facts | head -20

Pin the versions in requirements.yml and install with ansible-galaxy collection install -r requirements.yml. An unpinned controller that silently upgrades a major collection version is the classic cause of a playbook that worked on Friday and fails on Monday with changed module arguments.

Then create a project-local ansible.cfg. The persistent-connection block is not optional for network gear: because the CLI session is held open by a background process for the duration of the play, the connect and command timeouts determine how long Ansible waits before giving up on a slow device or a long-running show command.

[defaults]
inventory = ./inventory.ini
gather_facts = False
host_key_checking = False
retry_files_enabled = False
forks = 10

[persistent_connection]
connect_timeout = 60
command_timeout = 120
persistent_command_timeout = 120

Disabling the implicit fact gathering up front saves the first confusing error of every network project: the setup module attempting to log in with the standard SSH plugin and failing with a generic "unable to open shell" message.

Inventory: Groups and Connection Variables

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

[switches:children]
eos
ios
vyos

[eos]
veos01 ansible_host=veos-01.example.net

[eos:vars]
ansible_become=yes
ansible_become_method=enable
ansible_network_os=arista.eos.eos
ansible_user=my_eos_user

[ios]
ios01 ansible_host=ios-01.example.net

[ios:vars]
ansible_become=yes
ansible_become_method=enable
ansible_network_os=cisco.ios.ios

Note the pattern: ansible_network_os tells Ansible which platform module set to load, and ansible_become_method=enable handles the IOS/EOS enable-mode jump automatically.

The inventory also shows why the group hierarchy matters. switches is a parent group that exists only so a playbook can say hosts: switches and hit every vendor; the child groups eos, ios and vyos carry the platform-specific variables. Add vyos with ansible_become=no, because VyOS privilege escalation is a different mechanism and the enable-mode logic would simply time out waiting for a prompt that never appears.

For anything larger than a lab, move the variables out of the INI file into a directory tree. This is the layout most production teams converge on, and Ansible loads it automatically without any extra configuration:

inventory.ini
group_vars/
  all/            # vault.yml with every password, referenced by name
  switches.yml    # defaults common to every device
  eos.yml         # show command override, become settings
  ios.yml
  vyos.yml
host_vars/
  veos01.yml      # per-device values that cannot be derived
  ios01.yml

An inventory host nickname and the host_vars filename must match exactly — veos01 in the inventory, host_vars/veos01.yml on disk. Case matters too, and a mismatch produces no error at all: the variables just quietly resolve to nothing.

Example 1: Collect Facts and Back Up Configs

The first official example gathers facts from every device and writes backup files of the running configuration:

---
- hosts: switches
  gather_facts: false
  connection: ansible.netcommon.network_cli
  tasks:
    - name: Collect facts from network devices
      ansible.builtin.import_role:
        name: ansible.netcommon.network
      vars:
        network_operations: gather_facts
        network_gather_facts_dir: "{{ playbook_dir }}/facts"

Running the playbook produces a recap like eos01 ok=7 changed=2 and drops a timestamped fact file per device under the facts directory - an instant configuration backup and inventory audit in one command.

The ansible.netcommon.network role is a thin wrapper that selects the right vendor facts module from the operating system, so adding a fourth vendor does not require a new task, only a new ansible_network_os value. The same role can do more than gather facts — swap network_operations and you get the other two official examples without writing any module calls:

    - name: Back up every device configuration
      ansible.builtin.import_role:
        name: ansible.netcommon.network
      vars:
        network_operations: backup
        network_backup_dir: "{{ playbook_dir }}/backups"

    - name: Gather facts and persist them for later plays
      ansible.builtin.import_role:
        name: ansible.netcommon.network
      vars:
        network_operations: persist

Whichever operation you choose, the fact file is the deliverable. It contains hostname, model, serial number, software version, interface list and the running configuration, which means a single scheduled playbook gives you both an asset inventory and a daily configuration snapshot with no additional tooling. Schedule it with cron or a CI pipeline and the historical record builds itself:

# /etc/cron.d/net-facts  -- nightly at 02:15
15 2 * * * ansible /srv/ansible/netbackup.yml --vault-password-file /root/.vaultpass >> /var/log/net-facts.log 2>&1

Example 2: Platform-Independent cli_command

Instead of one module per vendor, ansible.netcommon.cli_command runs the same command everywhere; the when clause keeps it honest about which OS is answering:

---
- hosts: network
  gather_facts: false
  connection: ansible.netcommon.network_cli
  tasks:
    - name: Run cli_command on Arista
      ansible.netcommon.cli_command:
        command: show ip int br
      register: result
      when: ansible_network_os == 'arista.eos.eos'

    - name: Display the output
      ansible.builtin.debug:
        var: result.stdout_lines

The register plus debug pair is the debugging backbone of every network playbook: it shows exactly what the device returned, line by line, before you build anything on top of it. Two extensions turn it from a trick into a tool. First, drop the when clauses by defining the command per platform group:

# group_vars/eos.yml
show_uptime: "show version | json"
# group_vars/ios.yml
show_uptime: "show version | include uptime"
# group_vars/vyos.yml
show_uptime: "show system uptime"

# tasks/main.yml
- name: Read device uptime on any platform
  ansible.netcommon.cli_command:
    command: "{{ show_uptime }}"
  register: uptime
- name: Report
  ansible.builtin.debug:
    msg: "{{ inventory_hostname }}: {{ uptime.stdout_lines | first }}"

Second, add check_mode awareness for read-only tasks and no_log for anything secret. Interactive commands that expect a follow-up prompt are handled by the prompt and answer keys, which is how password changes and confirmation-gated commands are automated without screen scraping.

- name: Change a password and answer the prompts
  ansible.netcommon.cli_command:
    command: configure terminal
    prompt:
      - "New password:"
      - "Retype new password:"
    answer:
      - "{{ new_password }}"
      - "{{ new_password }}"
  no_log: true

Example 3: Desired-State Configuration with Resource Modules

cli_command is perfect for reading and for one-off writes, but it cannot tell whether the change is already in place. For configuration that must be idempotent, the vendor collections provide resource modules that model each feature as data.

    - name: Ensure NTP servers are configured (vendor-neutral shape)
      ansible.netcommon.resource_module:
        name: "{{ ansible_network_os }}_ntp_global"
        config:
          servers:
            - server: time.google.com
            - server: time1.google.com
        state: replaced
      when: ansible_network_os != 'vyos.vyos.vyos'

Run that twice and the second run reports changed=0. That property is what makes a network change reviewable: a diff only appears when something actually differs, so --check --diff becomes a real pre-flight gate instead of a wall of always-changed output. Validate the desired state with an assert task before pushing, so a typo in a variable fails the play on the controller rather than on a hundred switches:

    - name: Fail fast if a required variable is missing
      ansible.builtin.assert:
        that:
          - show_uptime is defined
          - ansible_network_os is defined
        fail_msg: "Inventory is incomplete for {{ inventory_hostname }}"
        quiet: true

Why This Pattern Matters

  • One inventory, many vendors - the group structure keeps secrets and connection methods per platform without duplicating playbooks.
  • Backup as a side effect - facts collection with the network role writes config backups, giving you free change history.
  • cli_command over vendor modules - fewer moving parts for read-only tasks; use vendor modules when you need structured output or config diffs.
  • Group variables over conditionals - a per-platform command in group_vars replaces the when ladder and keeps one task in the play.

Reading the Output and Verifying the Change

The recap line is the fastest diagnostic you have. ok counts tasks that ran and reported no change, changed counts tasks that altered state, unreachable means the SSH session never came up and every later task for that host was skipped, and failed stops the host by default. On a facts-and-backup play, expect changed to be non-zero on the first run (files are written) and zero on a re-run into the same directory. To prove a configuration landed, ask the device rather than the log:

ansible -i inventory.ini switches -m ansible.netcommon.cli_command \
  -a "command='show running-config | include ntp server'" --ask-vault-pass

Troubleshooting Common Errors

"Unable to open shell" is the generic wrapper for a bad ansible_network_os, a wrong username, or SSH not enabled on the device. Test with a plain ssh from the controller and fix that first. "Privilege escalation failed" means ansible_become_method=enable is missing or the enable password is unset. Commands time out on large outputs when the device pager is still active — send terminal length 0 (IOS/EOS) once per session and raise persistent_command_timeout in ansible.cfg. Variables silently ignored come from a host_vars filename that does not match the inventory name. Provider deprecation warnings mean an old playbook is using ansible.netcommon.local with a provider dictionary; move the connection variables into group_vars and switch to network_cli. And if a play returns unreachable for every host at once, suspect the SSH key, the vault password, or a device-side AAA lockout from too many concurrent logins — reduce forks and retry before changing anything else.

FAQ

Do I need Netmiko or Paramiko alongside Ansible? No. network_cli uses Paramiko underneath, but the plugin is part of ansible.netcommon. Keep Netmiko for Python scripts and Ansible for orchestration; mixing them in one playbook adds dependencies without adding capability.

Can I test playbooks without real hardware? Yes. Containerlab or the vendor virtual images (vEOS, Cisco CML, VyOS) give you real CLIs in containers. Point the same inventory at the lab and use --check --diff to validate before touching production.

How do these playbooks fit into CI? Run --syntax-check plus an inventory assert play on every commit, and a facts-and-backup job nightly. Because the facts role writes files, a pipeline can open a merge request whenever a device's configuration changes without a corresponding commit — drift detection for free.

Related Reading on This Site

See the Ansible facts collection and config backup examples, the Ansible Cisco automation playbooks, Ansible network modules: ios_config, facts and config backup, and Jinja2 templates for network device configs.

原文链接:https://docs.ansible.com/projects/ansible/latest/network/user_guide/network_best_practices_2.5.html