Skip to content

5. Playbooks

Ansible
Git
Linux


Here I will create a static inventory, (for now), go over read-only show commands, fact gathering, configuration pushes, and create a structured config backup playbook.

What I Will Be Completing In This Part

  • Create a static YAML inventory file with wan-r1 and wan-r2
  • Verify connenctivity from Ansible using an ad-hoc ping
  • Write and run a read-only playbook using ios_command
  • Gather structured device facts using ios_facts
  • Push a configuration change using ios_config and observe idempotency
  • Save the running configuration to startup
  • Build a structured backup playbook that stores configs locally with timestamps

01 Static Inventory

I decided on YAML when creating the static inventory file. This file defines the hosts and groups that Ansible targets when running playbooks.

inventory/hosts.yml
1
2
3
4
5
6
7
8
9
---
all:
  children:
    ios:
      hosts:
        wan-r1:
          ansible_host: 172.20.20.11
        wan-r2:
          ansible_host: 172.20.20.12

Line 2: all is the top-level implicit group that contains every host.

Lines 4-5:
Any host here inherits the variables from inventory/group_vars/ios/vars.yml and inventory/group_vars/ios/vault.yml automatically.
Lines 7, 9:
ansible_host tells Ansible the actual IP address to connect to.
Info This static inventory is temporary. Once I setup NetBox I’ll also configure the dynamic inventory plugin, which generates the inventory automatically from NetBox’s device database.

I then verified Ansible can pars the inventory file before running playbooks:

Bash
(.venv) $ ansible-inventory --list -y

This will print the full inventory as Ansible sees it.


01a Connectivity Test

I confirmed that Ansible can reach both devices using an ad-hoc command.

Bash
(.venv) $ ansible ios -m cisco.ios.ios_command -a "commands='show version | include uptime'"
ios:
The target group.
-m cisco.ios.ios_command:
The module to run.
-a “commands=”:
The module arguments.
Expected Output
wan-r1 | SUCCESS => {
    "changed": false,
    "stdout": [
        "wan-r1 uptime is 2 hours, 15 minutes"
    ],
    "stdout_lines": [
        [
            "wan-r1 uptime is 2 hours, 15 minutes"
        ]
    ]
}
wan-r2 | SUCCESS => {
    "changed": false,
    "stdout": [
        "wan-r2 uptime is 2 hours, 14 minutes"
    ],
    "stdout_lines": [
        [
            "wan-r2 uptime is 2 hours, 14 minutes"
        ]
    ]
}

02 Playbook

02a Show Commands

I wrote a read-only playbook that runs several show commands and displays the output.

playbooks/ios_show.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
---
- name: IOS-XE show commands
  hosts: ios
  gather_facts: false

  tasks:
    - name: Run show commands
      cisco.ios.ios_command:
        commands:
          - show  version | include Software
          - show ip interface brief
          - show running-config | section line vty
      register: show_output

    - name: Display output
      ansible.builtin.debug:
        msg: "{{ show_output.stdout_lines }}"
Line 3:
Targets every device in the ios inventory group.
Line 4:
Disables Ansible’s default fact-gathering step.
Lines 9-12:
These commands run sequentially on each device and the output is collected in a list.
Line 13:
Captures the module’s return data into a variable.
Lines 15-17:
Prints the registered variable to the Ansible output.

Then I ran the playbook:

Bash
(.venv) $ ansible-playbook playbooks/ios_show.yml

The results will be formatted in YAML.


02b Gathering Facts

The ios_facts module collects structured data about devices. The data is returned as Ansible facts, which means it’s accessible as variables in subsequent tasks without needing to parse show command output manually.

playbooks/ios_facts.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
---
- name: Gather IOS-XE device facts
  hosts: ios
  gather_facts: false

  tasks:
    - name: Collect all facts
      cisco.ios.ios_facts:
        gather_subset:
          - all
      register: facts_output

    - name: Display hostname and version
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }}: {{ ansible_net_hostname }} running {{ ansible_net_version }}"

    - name: Display all interfaces
      ansible.builtin.debug:
        msg: "{{ ansible_net_interfaces | to_nice_yaml }}"
Lines 9-10:
This tells the module to collect every category of facts.
Line 15:
After ios_facts runs it populates Ansible facts as variables prefixed with ansible_net_.
Line 19:
Is a dictionary containing every interface on the device with its operational state, IP addresses, MTU, speed, and more.

Then tested the playbook:

Bash
(.venv) $ ansible-playbook playbooks/ios_facts.yml

03b Pushing Configuration

I used ios_config to push a simple change: adding a login banner and disabling DNS lookups.

playbooks/ios_base_config.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
---
- name: Apply base confiugration to IOS-XE devices
  hosts: ios
  gather_facts: false

  tasks:
    - name: Disable DNS lookups
      cisco.ios.ios_config:
        lines:
          - no ip domain-lookup

    - name: Set login banner
      cisco.ios.ios_config:
        lines:
          - banner motd ^Unauthorized access is prohibited.^
      
    - name: Set logging and service timestamps
      cisco.ios.ios_config:
        lines:
          - service timestamps debug datetime msec localtime
          - service timestamps log datetime msec localtime
          - logging buffered 16384 informational

Then ran the playbook:

Bash
(.venv) $ ansible-playbook playbooks/ios_base_config.yml

The 1st run should show changed=3 for each device. To check idempotency I ran the playbook again, which it should show changed=0.


03c Saving Configuration

Next, I wrote a playbook that saves the running configuration to startup.

playbooks/ios_save.yml
1
2
3
4
5
6
7
8
9
---
- name: Save running config to startup
  hosts: ios
  gather_facts: false

  tasks:
    - name: Save configuration
      cisco.ios.ios_config:
        save_when: always
Line 9:
This triggers a write memory every time this task runs.

I then tested the playbook:

Bash
(.venv) $ ansible-playbook playbooks/ios_save.yml

03d Structured Backup

I then made a playbook that retrieves the full running configuration from each device and saves it to a local file on the control node, with a timestamp. This is the manual way of doing it, later on I’ll setup Oxidized which does it automatically.

playbooks/ios_backup.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
---
- name: Backup IOS-XE running configuration
  hosts: ios
  gather_facts: false

  vars:
    backup_root: "{{ playbook_dir }}/../backups"
    timestamp: "{{ lookup('pipe', 'date +%Y%m%d-%H%M%S') }}"

  tasks:
    - name: Create backup directory
      ansible.builtin.file:
        path: "{{ backup_root }}/{{ inventory_hostname }}"
        state: directory
        mode: "0755"
      delegate_to: localhost
      run_once: false

    - name: Retrieve running configuration
      cisco.ios.ios_command:
        commands:
          - show running-config
      register: running_config

    - name: Save configuration to file
      ansible.builtin.copy:
        content: "{{ running_config.stdout[0] }}"
        dest: "{{ backup_root }}/{{ inventory_hostname }}/{{ inventory_hostname }}_{{ timestamp }}.cfg"
        mode: "0644"
      delegate_to: localhost

    - name: Save a latest copy (overwritten each run)
      ansible.builtin.copy:
        content: "{{ running_config.stdout[0] }}"
        dest: "{{ backup_root }}/{{ inventory_hostname }}/{{ inventory_hostname }}_latest.cfg"
        mode: "0644"
      delegate_to: localhost
Line 7:
backup_root resolves to a backups/ directory in the project root.
Line 8:
This plugin runs a shell command on the control node and captures the output.
Lines 11-17:
Creates a per-device subdirectory under backups/.
Lines 19-23:
Retrieves the full running config.
Lines 25-30:
Writes the timestamped backup file.
Lines 32-37:
Writes a _latest.cfg copy that is overwritten on every run.

Then ran the playbook:

Bash
(.venv) $ ansible-playbook playbooks/ios_backup.yml

04 Updating .gitignore

The backups/ directory contains device running configurations which may include sensitive information. I added it to the .gitignore so backup files are not committed to the repo.

.gitignore
backups/

05 Commit and Push

Lastly, I committed the inventory file, all four playbooks, and updated the .gitignore using the feature branch workflow:

Bash
(.venv) $ git checkout -b feat/first-playbooks
(.venv) $ git add -A
(.venv) $ git status
Expected Output
modified:   .gitignore
new file:   inventory/hosts.yml
new file:   playbooks/ios_show.yml
new file:   playbooks/ios_facts.yml
new file:   playbooks/ios_base_config.yml
new file:   playbooks/ios_save.yml
new file:   playbooks/ios_backup.yml

I committed and pushed:

Bash
(.venv) $ git commit -m "feat: add static inventory and first IOS-XE playbooks"
(.venv) $ git push -u origin feat/first-playbooks

I then went to Gitea and approved and merged.

After merging:

Bash
(.venv) $ git checkout main
(.venv) $ git pull origin main

I now have a working static inventory with 2 Cat9kv nodes in the ios group, 5 playbooks covering the core Ansible network automation.

Last updated on • Ernesto Diaz