Skip to main content

Command Palette

Search for a command to run...

Ansible for Network Automation

Infrastructure as Code with Ansible

Published
12 min read
Ansible for Network Automation

Ansible, developed by Red Hat, is an open-source platform. Our focus is on Ansible Core, the open-source version. Red Hat also provides a commercial product, Ansible Tower, which enhances Ansible Core with features such as Role-Based Access Control (RBAC), secure network credential storage, and a RESTful API, among others.

Why Ansible for Network Automation ?

Network engineers have long relied on manual CLI workflows: SSH into a device, run show commands, copy-paste output into a spreadsheet, make a change, repeat. This approach is slow, prone to errors, and unscalable when managing numerous routers across multiple vendors.

Ansible changes the game. It's agentless, uses SSH/NETCONF under the hood, and treats your network configuration as code — version-controlled, repeatable, and auditable. For a multivendor environment running Cisco IOS-XR, Juniper vMX, and Nokia SR OS, Ansible speaks all three dialects fluently.

Ansible Architecture : The Core building blocks

Here's a visual breakdown of Ansible's architecture:

1. Inventory - Your network Source of Truth

The inventory file informs Ansible about the devices that exist and how to access them. You can use either IP addresses or fully qualified domain names. In the upcoming example we’ll introduce how to create groups. We will organize devices into vendor groups for targeted plays.

File: inventory.yml

[cisco_iosxr]
rtr-cisco-01 ansible_host=172.20.20.15

[juniper_vmx]
rtr-junos-01 ansible_host=172.20.20.16

[nokia_sros]
rtr-nokia-01 ansible_host=172.20.20.13
rtr-nokia-02 ansible_host=172.20.20.14

[core_routers:children]
cisco_iosxr
juniper_vmx
nokia_sros

2. Playbooks - Your automation Scripts

The playbook is a file that contains your automation instructions. In other words, playbooks contain the individual tasks and workflows that you want to use to automate your network. The playbook is written in YAML and consists of one or more plays. Each play, in turn, includes one or more tasks.

Let’s take a look at an example playbook to better understand its structure

---
- name: Run Pre-Change Checks
  hosts: core_routers
  gather_facts: false
  tasks:
    - name: Include vendor-specific pre-check tasks
      include_tasks: "tasks/pre_check_{{ ansible_network_os }}.yml"

Our example playbook provides an overview of the structure to be used. You should already understand the basics of YAML and indentation. Ultimately, you need a YAML list of plays and a YAML list of tasks under the tasks key. Any slight indentation errors will result in error messages from Ansible when running your playbook, so precision is crucial.

3. Modules - The Actions of Ansible

Ansible modules perform a specific operation. For network automation, you'll use vendor-specific collections:

Vendor Collection Key Modules
Cisco IOS-XR cisco.iosxr iosxr_command,iosxr_config, iosxr_facts
Juniper Junos junipernetworks.junos junos_command, junos_config, junos_facts
Nokia SROS nokia.sros sros_command, sros_config

connection types

# group_vars/cisco_iosxr.yml
ansible_network_os: cisco.iosxr.iosxr
ansible_connection: ansible.netcommon.network_cli

# group_vars/juniper_vmx.yml
ansible_network_os: junipernetworks.junos.junos
ansible_connection: ansible.netcommon.netconf

# group_vars/nokia_sros.yml
ansible_network_os: nokia.sros
ansible_connection: ansible.netcommon.network_cli

Variables and Vault Credentials

Hardcoding passwords in playbooks is a career-limiting move. Use group_vars for non-sensitive data and Ansible Vault for secrets.

Since we are testing in a lab environment we are not using Ansible Vault but it is highly recommended in a Production environment.

group_vars/nokia_sros.yml — Non-sensitive defaults

ansible_network_os: nokia.sros
ansible_connection: ansible.netcommon.network_cli
ansible_user: "admin"
ansible_password: "{{ vault_ansible_password }}""

Creating and using Ansible Vault

# Create an encrypted secrets file
ansible-vault create group_vars/all/vault.yml

# Edit it later
ansible-vault edit group_vars/all/vault.yml

Inside the vault file :

# group_vars/all/vault.yml (encrypted at rest)
vault_ansible_password: "cOdeDNetw\(rk2#%\)"

Run playbooks with Vault :

# Prompt for vault password
ansible-playbook precheck.yml --ask-vault-pass

Hands On - Multivendor Pre/Post Change Checks

The scenario involves a maintenance window for BGP configuration changes across core routers. You need to capture the device state before and after the changes, then compare them for any differences.

Action Ansible Command
Run pre-checks ansible-playbook -i inventory.yml precheck.yml
make changes Manual or separate change playbook
Run post-checks ansible-playbook -i inventory.yml postcheck.yml
Compare & Validate ansible-playbook -i inventory.yml compare.yml

Project Structure

pre-post-checks/
├── inventory.yml
├── precheck.yml
├── postcheck.yml
├── junos_checks.yml          
├── sros_checks.yml                           
├── reports/
└── group_vars/
💡
All the tasks are flattened into one directory. A separate task folder can be created

Prerequisites

pip install ansible
pip install ansible-pylibssh (optional

Pre-check Playbook

File : precheck.yml

- name: "PRE-CHANGE CHECKS"
  hosts: core_routers
  gather_facts: false
  vars:
    check_phase: "pre"
    timestamp: "{{ lookup('pipe', 'date +%Y%m%d_%H%M%S') }}"
    output_base: "{{ playbook_dir }}/reports"

  tasks:
    - name: Debug output path
      debug:
         msg: "Will create: reports/{{ inventory_hostname }}/{{ timestamp }}"
      delegate_to: localhost

    - name: Create output directory
      shell: mkdir -p "{{ output_base }}/{{ inventory_hostname }}/{{ timestamp }}"
      delegate_to: localhost

    - name: Set output_dir fact per host
      set_fact:
        output_dir: "{{ output_base }}/{{ inventory_hostname }}/{{ timestamp }}" 

    - name: Run Cisco IOS-XR checks
      include_tasks: cisco_checks.yml
      when: ansible_network_os == "cisco.iosxr.iosxr"

    - name: Run Juniper Junos checks
      include_tasks: junos_checks.yml
      when: ansible_network_os == "junipernetworks.junos.junos"

    - name: Run Nokia SR OS checks
      include_tasks: sros_checks.yml
      when: ansible_network_os == "sros"

File : cisco_checks.yml

 - name: "[Cisco] Gather BGP summary"
  cisco.iosxr.iosxr_command:
    commands:
      - show bgp ipv4 unicast summary
      - show bgp ipv6 unicast summary
  register: cisco_bgp_output

- name: "[Cisco] Gather interface status"
  cisco.iosxr.iosxr_command:
    commands:
      - show interfaces brief
      - show ipv4 interface brief
  register: cisco_intf_output

- name: "[Cisco] Gather routing table summary"
  cisco.iosxr.iosxr_command:
    commands:
      - show route summary
      - show route ipv6 summary
  register: cisco_route_output

- name: "[Cisco] Debug output_dir"
  debug:
    msg: "output_dir is: {{ output_dir }}"
  delegate_to: localhost

- name: "[Cisco] Save pre-check output to file"
  copy:
    content: |
      === CISCO IOS-XR PRE-CHECK: {{ inventory_hostname }} ===
      Timestamp: {{ timestamp }}

      --- BGP SUMMARY (IPv4) ---
      {{ cisco_bgp_output.stdout[0] }}

      --- BGP SUMMARY (IPv6) ---
      {{ cisco_bgp_output.stdout[1] }}

      --- INTERFACE STATUS ---
      {{ cisco_intf_output.stdout[0] }}

      --- ROUTE SUMMARY ---
      {{ cisco_route_output.stdout[0] }}
    dest: "{{ output_dir }}/pre_check.txt"
  delegate_to: localhost

File : junos_checks.yml

- name: "[Junos] Gather BGP summary (structured)"
  junipernetworks.junos.junos_command:
    commands:
      - show bgp summary
    display: xml   # NETCONF returns structured XML
  register: junos_bgp_output

- name: "[Junos] Gather interface status"
  junipernetworks.junos.junos_command:
    commands:
      - show interfaces terse
  register: junos_intf_output

- name: "[Junos] Gather routing table"
  junipernetworks.junos.junos_command:
    commands:
      - show route summary
  register: junos_route_output

- name: "[Junos] Extract BGP peer count using XPath"
  set_fact:
    bgp_peer_count: >-
      {{ junos_bgp_output.output[0] | regex_findall('peer-count.*?</peer-count>')
         | length }}

- name: "[Junos] Save pre-check output"
  copy:
    content: |
      === JUNIPER vMX PRE-CHECK: {{ inventory_hostname }} ===
      Timestamp: {{ timestamp }}

      --- BGP SUMMARY ---
      {{ junos_bgp_output.stdout[0] | default(junos_bgp_output.output[0]) }}

      --- INTERFACES ---
      {{ junos_intf_output.stdout[0] }}

      --- ROUTING TABLE SUMMARY ---
      {{ junos_route_output.stdout[0] }}
    dest: "{{ output_dir }}/pre_check.txt"
  delegate_to: localhost

File : sros_checks.yml

- name: "[Nokia] Gather BGP summary"
  community.network.sros_command:
    commands:
      - show router bgp summary
      - show router bgp summary family ipv6
  register: sros_bgp_output

- name: "[Nokia] Gather interface status"
  community.network.sros_command:
    commands:
      - show router interface
      - show port
  register: sros_intf_output

- name: "[Nokia] Gather routing table"
  community.network.sros_command:
    commands:
      - show router route-table summary
  register: sros_route_output

- name: "[Nokia] Save pre-check output"
  copy:
    content: |
      === NOKIA SR OS PRE-CHECK: {{ inventory_hostname }} ===
      Timestamp: {{ timestamp }}

      --- BGP SUMMARY (IPv4) ---
      {{ sros_bgp_output.stdout[0] }}

      --- BGP SUMMARY (IPv6) ---
      {{ sros_bgp_output.stdout[1] }}

      --- INTERFACES ---
      {{ sros_intf_output.stdout[0] }}

      --- ROUTE TABLE SUMMARY ---
      {{ sros_route_output.stdout[0] }}
    dest: "{{ output_dir }}/pre_check.txt"
  delegate_to: localhost

File : group_vars/cisco_ixr.yml

ansible_network_os: cisco.iosxr.iosxr
ansible_connection: ansible.netcommon.network_cli
ansible_user: "<username>"
ansible_password: "<password>"
💡
The nokia sros and juniper are similar to the above

Run the Playbook

Output :

ansible-playbook -i inventory.yml precheck.yml

PLAY [PRE-CHANGE CHECKS] ***************************************************************

TASK [Debug output path] ***************************************************************
ok: [rtr-cisco-01 -> localhost] => {
    "msg": "Will create: reports/rtr-cisco-01/20260407_103940"
}
ok: [rtr-junos-01 -> localhost] => {
    "msg": "Will create: reports/rtr-junos-01/20260407_103940"
}
ok: [rtr-nokia-01 -> localhost] => {
    "msg": "Will create: reports/rtr-nokia-01/20260407_103940"
}
ok: [rtr-nokia-02 -> localhost] => {
    "msg": "Will create: reports/rtr-nokia-02/20260407_103940"
}

TASK [Create output directory] ***************************************************************
changed: [rtr-nokia-02 -> localhost]
changed: [rtr-nokia-01 -> localhost]
changed: [rtr-cisco-01 -> localhost]
changed: [rtr-junos-01 -> localhost]

TASK [Set output_dir fact per host] ***************************************************************
ok: [rtr-junos-01]
ok: [rtr-cisco-01]
ok: [rtr-nokia-01]
ok: [rtr-nokia-02]

TASK [Run Cisco IOS-XR checks] ***************************************************************
skipping: [rtr-junos-01]
skipping: [rtr-nokia-01]
skipping: [rtr-nokia-02]
included: /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/cisco_checks.yml for rtr-cisco-01

TASK [[Cisco] Gather BGP summary] ***************************************************************
ok: [rtr-cisco-01]

TASK [[Cisco] Gather interface status] ***************************************************************
ok: [rtr-cisco-01]

TASK [[Cisco] Gather routing table summary] ***************************************************************
ok: [rtr-cisco-01]

TASK [[Cisco] Debug output_dir] ***************************************************************
ok: [rtr-cisco-01 -> localhost] => {
    "msg": "output_dir is: /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/reports/rtr-cisco-01/20260407_103940"
}

TASK [[Cisco] Save pre-check output to file] ***************************************************************
changed: [rtr-cisco-01 -> localhost]

TASK [Run Juniper Junos checks] ***************************************************************
skipping: [rtr-cisco-01]
skipping: [rtr-nokia-01]
skipping: [rtr-nokia-02]
included: /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/junos_checks.yml for rtr-junos-01

TASK [[Junos] Gather BGP summary (structured)] ***************************************************************
ok: [rtr-junos-01]

TASK [[Junos] Gather interface status] ***************************************************************
ok: [rtr-junos-01]

TASK [[Junos] Gather routing table] ***************************************************************
ok: [rtr-junos-01]

TASK [[Junos] Extract BGP peer count using XPath] ***************************************************************
ok: [rtr-junos-01]

TASK [[Junos] Save pre-check output] ***************************************************************
changed: [rtr-junos-01 -> localhost]

TASK [Run Nokia SR OS checks] ***************************************************************
skipping: [rtr-cisco-01]
skipping: [rtr-junos-01]
included: /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/sros_checks.yml for rtr-nokia-01, rtr-nokia-02

TASK [[Nokia] Gather BGP summary] ***************************************************************
ok: [rtr-nokia-01]
ok: [rtr-nokia-02]

TASK [[Nokia] Gather interface status] ***************************************************************
ok: [rtr-nokia-02]
ok: [rtr-nokia-01]

TASK [[Nokia] Gather routing table] ***************************************************************
ok: [rtr-nokia-02]
ok: [rtr-nokia-01]

TASK [[Nokia] Save pre-check output] ***************************************************************
changed: [rtr-nokia-01 -> localhost]
changed: [rtr-nokia-02 -> localhost]

PLAY RECAP ***************************************************************
rtr-cisco-01               : ok=9    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
rtr-junos-01               : ok=9    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
rtr-nokia-01               : ok=8    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
rtr-nokia-02               : ok=8    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0

File : reports/rtr-cisco-01/20260407_103940/precheck.txt

=== CISCO IOS-XR PRE-CHECK: rtr-cisco-01 ===
Timestamp: 20260407_103948

--- BGP SUMMARY (IPv4) ---
BGP router identifier 10.10.10.3, local AS number 65000
BGP generic scan interval 60 secs
Non-stop routing is enabled
BGP table state: Active
Table ID: 0xe0000000   RD version: 2
BGP table nexthop route policy:
BGP main routing table version 2
BGP NSR Initial initsync version 2 (Reached)
BGP NSR/ISSU Sync-Group versions 0/0
BGP scan interval 60 secs

BGP is operating in STANDALONE mode.


Process       RcvTblVer   bRIB/RIB   LabelVer  ImportVer  SendTblVer  StandbyVer
Speaker               2          2          2          2           2           0

Neighbor        Spk    AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down  St/PfxRcd
10.10.10.1        0 65000  239205  239192        2    0    0     2w3d          0

--- BGP SUMMARY (IPv6) ---
% None of the requested address families are configured for instance 'default'(36210)

--- INTERFACE STATUS ---
Intf       Intf        LineP              Encap  MTU        BW
               Name       State       State               Type (byte)    (Kbps)
--------------------------------------------------------------------------------
                Lo0          up          up           Loopback  1500          0
              Lo100          up          up           Loopback  1500          0
                Nu0          up          up               Null  1500          0
     Mg0/RP0/CPU0/0          up          up               ARPA  1514    1000000
          Gi0/0/0/0          up          up               ARPA  9212    1000000
          Gi0/0/0/1  admin-down  admin-down               ARPA  1514    1000000
          Gi0/0/0/2          up          up               ARPA  1514    1000000

--- ROUTE SUMMARY ---
Route Source                     Routes     Backup     Deleted     Memory(bytes)
local                            2          0          0           416
connected                        1          1          0           416
application fib_mgr              0          0          0           0
vxlan                            0          0          0           0
static                           0          0          0           0
dagr                             0          0          0           0
bgp 65000                        0          0          0           0
isis 0                           6          1          0           1456
Total                            9          2          0           2288

Post-check Playbook

For this playbook we are still using the same construct as the pre-check playbook the only changes done are below

# change the check_phase to post

- name: "POST-CHANGE CHECKS"
  hosts: core_routers
  gather_facts: false
  vars:
    check_phase: "post"

# change the sros_checks.yml like below 

- name: "[Nokia] Save post-check output"
  copy:
    content: |
      === NOKIA SR OS POST-CHECK: {{ inventory_hostname }} ===
      Timestamp: {{ timestamp }}

      --- BGP SUMMARY (IPv4) ---
      {{ sros_bgp_output.stdout[0] }}

      --- BGP SUMMARY (IPv6) ---
      {{ sros_bgp_output.stdout[1] }}

      --- INTERFACES ---
      {{ sros_intf_output.stdout[0] }}

      --- ROUTE TABLE SUMMARY ---
      {{ sros_route_output.stdout[0] }}
    dest: "{{ output_dir }}/post_check.txt"
  delegate_to: localhost

# Run the playbook 

ansible-playbook -i inventory.yml postcheck.yml

Comparison Playbook

The compare.yml playbook finds the most recent pre and post check files for every device — regardless of which timestamp folder they live in — diffs them, and saves a full validation report.

- name: "COMPARE PRE/POST CHECKS"
  hosts: localhost
  gather_facts: false

  tasks:
    - name: Find all device directories
      find:
        paths: "{{ playbook_dir }}/reports/"
        file_type: directory
        recurse: no
      register: device_dirs

    - name: Find latest pre-check for each device
      shell: |
        find "{{ item.path }}" -name "pre_check.txt" | sort | tail -1
      loop: "{{ device_dirs.files }}"
      register: latest_pre
      delegate_to: localhost

    - name: Find latest post-check for each device
      shell: |
        find "{{ item.path }}" -name "post_check.txt" | sort | tail -1
      loop: "{{ device_dirs.files }}"
      register: latest_post
      delegate_to: localhost

............. [truncated]
💡
Step 1 : Find devices -- Step 2: Find latest pre/post -- Step 3: Diff pre/post -- Step 4 : Build Report -- Step 5 : Save and print

Sample diff output after the change window:

}
ok: [localhost] => (item=rtr-cisco-01) => {
    "msg": [
        "============================================================",
        "Device     : rtr-cisco-01",
        "Pre-check  : /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/reports/rtr-cisco-01/20260407_103940/pre_check.txt",
        "Post-check : /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/reports/rtr-cisco-01/20260407_111244/post_check.txt",
        "============================================================",
        "--- DIFF ---",
        "1,2c1,2",
        "< === CISCO IOS-XR PRE-CHECK: rtr-cisco-01 ===",
        "< Timestamp: 20260407_103948",
        "---",
        "> === CISCO IOS-XR POST-CHECK: rtr-cisco-01 ===",
        "> Timestamp: 20260407_111252",
        "23c23",
        "< 10.10.10.1        0 65000  239205  239192        2    0    0     2w3d          0",
        "---",
        "> 10.10.10.1        0 65000  239272  239258        2    0    0     2w3d          0"
    ]
}

Key Takeaways

Benefit Detail
Speed 45 min of manual CLI work runs in under minutes across all vendors simultaneously.
Consistency Every engineer captures the exact same data points — no more missed checks.
Auditability All outputs are timestamped, stored, and diff-able. Perfect for change management.
Security Ansible Vault keeps credentials out of your codebase, satisfying compliance requirements.

Download Code

All YAML files are here: codednetwork-week-8