Ansible for Network Automation
Infrastructure as Code with Ansible

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/
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>"
nokia sros and juniper are similar to the aboveRun 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]
Find devices -- Step 2: Find latest pre/post -- Step 3: Diff pre/post -- Step 4 : Build Report -- Step 5 : Save and printSample 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



