> ## Content Index
> Fetch the complete content index at: https://www.pinglabz.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Ansible Gathered, Rendered and Parsed on Cisco IOS XE: Read State Without Changing Anything
- URL: https://www.pinglabz.com/ansible-gathered-rendered-parsed-ios/
- Published: 2026-10-31T19:58:31.000Z
- Updated: 2026-10-31T20:11:19.000Z
- Description: Three read only states of the cisco.ios resource modules on IOS XE 17.18: gathered pulls live state into YAML, rendered turns YAML into CLI with no device, parsed reads a saved config. Plus the stale backup and leftover secondary address they caught.
- Author: Jaime
- Tags: Network Automation, cisco-ios, Cisco

This is part 8 of the [Ansible for network engineers series](https://www.pinglabz.com/ansible-network-automation/). [Part 3](https://www.pinglabz.com/ansible-resource-module-states-merged-replaced-overridden-deleted/) covered the states that change a device: merged, replaced, overridden and deleted. The same resource modules have three more states that never push a single command. `gathered` reads live config into structured data. `rendered` turns structured data into CLI lines without touching a device. `parsed` reads a saved config file and returns the same structure `gathered` would.

That makes them the safe way to start automating a network you didn't build. You can read everything, store it as YAML, and compare it before any playbook is allowed to change anything.

Built in my home lab with ansible-core 2.21.5, cisco.ios 11.6.0, ansible.netcommon 8.7.1 and Python 3.14, against hq-core1 running IOS XE 17.18.2\. The inventory is the one from [part 2](https://www.pinglabz.com/ansible-network-inventory-site-role/).

[![Lab diagram: Three read-only states of the cisco.ios resource modules, left to right. Gathered: hq-core1 (IOS XE 17.18.2) is read over SSH with ios_l3_interfaces / ios_interfaces state: gathered, saved to build/gathered/hq-core1.yml, where Ethernet0/2 has description 'CORE: hq-edge1 Et0/1'. Rendered: hq-core1.yml goes through state: rendered on localhost with no device contacted, giving CLI lines such as interface Ethernet0/1, ip address 10.66.101.1 255.255.255.252 and interface loopback0. Parsed: backups/hq-core1-interfaces.cfg goes through ios_interfaces state: parsed on localhost with no device contacted, giving name and description pairs such as Ethernet0/2 CORE: hq-edge1 Et0/2. A drift card compares gathered (live) with parsed (backup): Ethernet0/2 description is live 'CORE: hq-edge1 Et0/1' and backup 'CORE: hq-edge1 Et0/2', so the backup is stale.](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/10/ansible-gathered-rendered-parsed-states.png)](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/10/ansible-gathered-rendered-parsed-states.png?ref=pinglabz.com)

Gathered, rendered and parsed, and the drift they exposed on hq-core1

## gathered: live state as YAML

```yaml
# playbooks/80-gathered.yml
---
- name: Read device state as structured data
  hosts: hq-core1
  gather_facts: false
  tasks:
    - name: Gather L3 interfaces
      cisco.ios.ios_l3_interfaces:
        state: gathered
      register: l3

    - name: Gather interfaces
      cisco.ios.ios_interfaces:
        state: gathered
      register: intf

    - name: Save both as YAML
      ansible.builtin.copy:
        content: "{{ {'l3_interfaces': l3.gathered, 'interfaces': intf.gathered} | to_nice_yaml(indent=2) }}"
        dest: "{{ playbook_dir }}/../build/gathered/{{ inventory_hostname }}.yml"
        mode: "0644"
      delegate_to: localhost

```

The only `changed` in the run is the file write on the control node. The router reports `ok`:

```text
TASK [Gather L3 interfaces] ****************************************************
ok: [hq-core1]

TASK [Gather interfaces] *******************************************************
ok: [hq-core1]

TASK [Save both as YAML] *******************************************************
changed: [hq-core1 -> localhost]

PLAY RECAP *********************************************************************
hq-core1                   : ok=3    changed=1    unreachable=0    failed=0    skipped=0

```

Here's the file it wrote:

```yaml
interfaces:
- description: OOB management
  enabled: true
  name: Ethernet0/0
- description: 'CORE: hq-core2 Et0/1'
  enabled: true
  name: Ethernet0/1
- description: 'CORE: hq-edge1 Et0/1'
  enabled: true
  name: Ethernet0/2
- description: TEMP lab test - remove
  enabled: false
  name: Ethernet0/3
- description: HQ core loopback
  enabled: true
  name: Loopback0
l3_interfaces:
- ipv4:
  - address: 10.66.10.11/24
  name: Ethernet0/0
- ipv4:
  - address: 10.66.101.1/30
  name: Ethernet0/1
- ipv4:
  - address: 10.66.199.1/24
    secondary: true
  - address: 10.66.102.1/30
  name: Ethernet0/2
- name: Ethernet0/3
- ipv4:
  - address: 10.66.255.11/32
  name: Loopback0

```

Reading it as data showed two things I'd forgotten on this router: Et0/3 is shut with a "TEMP lab test - remove" description, and Et0/2 still has a 10.66.199.1/24 secondary from an old test. Both were in `show run` the whole time. I just never looked. In YAML they're one `grep` away, and on a real network you can loop the same play over every host and search the lot.

Masks come back as prefix lengths (`/30`, not `255.255.255.252`). That's the format the module expects in `config:`, so this output can go straight back into a `replaced` task later.

## rendered: YAML to CLI with no device

`rendered` takes the same `config:` you'd give `merged` or `replaced` and returns the commands it would generate. It never opens a session, so it runs on localhost.

```yaml
# playbooks/81-rendered.yml
---
- name: Render config from data, no device needed
  hosts: localhost
  gather_facts: false
  vars:
    ansible_connection: ansible.netcommon.network_cli
    ansible_network_os: cisco.ios.ios
    want: "{{ lookup('file', playbook_dir ~ '/../build/gathered/hq-core1.yml') | from_yaml }}"
  tasks:
    - name: Render L3 interfaces
      cisco.ios.ios_l3_interfaces:
        state: rendered
        config: "{{ want.l3_interfaces }}"
      register: r

    - name: Show it
      ansible.builtin.debug:
        var: r.rendered

```

The two connection vars look odd for a play that connects to nothing, but my first run without them failed:

```text
Connection type local is not valid for this module

```

Implicit localhost gets `ansible_connection: local`, and the module refuses to run under it even though it won't use the connection. A play level `connection:` keyword didn't help, because the host var wins. Setting both as play vars did.

```text
ok: [localhost] => {
    "r.rendered": [
        "interface Ethernet0/0",
        "ip address 10.66.10.11 255.255.255.0",
        "interface Ethernet0/1",
        "ip address 10.66.101.1 255.255.255.252",
        "interface Ethernet0/2",
        "ip address 10.66.102.1 255.255.255.252",
        "ip address 10.66.199.1 255.255.255.0 secondary",
        "interface loopback0",
        "ip address 10.66.255.11 255.255.255.255"
    ]
}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0

```

Two details. The prefixes are back to dotted masks, which is what IOS wants. And `Loopback0` came out as `interface loopback0` in lowercase. IOS accepts that, but if you diff rendered output against `show run` text, normalize case first or every loopback shows up as a difference.

I use `rendered` for code review: put the generated lines in the merge request so whoever approves the change reads CLI, not YAML.

## parsed: a saved config file, offline

[Part 4](https://www.pinglabz.com/ansible-config-backup-git/) saved every router's running config to `backups/`. `parsed` reads that text and returns structured data, again with no device.

```yaml
# playbooks/82-parsed.yml
---
- name: Parse saved config text, no device needed
  hosts: localhost
  gather_facts: false
  vars:
    ansible_connection: ansible.netcommon.network_cli
    ansible_network_os: cisco.ios.ios
  tasks:
    - name: Parse interfaces from a backup file
      cisco.ios.ios_interfaces:
        state: parsed
        running_config: "{{ lookup('file', playbook_dir ~ '/../backups/hq-core1-interfaces.cfg') }}"
      register: p

    - name: Show name and description
      ansible.builtin.debug:
        msg: "{{ p.parsed | map(attribute='name') | zip(p.parsed | map(attribute='description', default='')) | list }}"

```

My first attempt pointed it at the full backup file and failed:

```text
Action failed: 'name' is undefined

```

I cut the file down until it passed. The problem was a VRF definition near the top of the config (name changed here):

```text
vrf definition MGMT
 description management VRF

```

The interfaces parser picked up that indented `description` line as if it belonged to an interface, found no interface name to attach it to, and failed. Feeding it only the `interface` sections fixed it:

```bash
awk '/^interface /{p=1} /^!/{p=0} p' backups/hq-core1.cfg > backups/hq-core1-interfaces.cfg

```

```text
# output compacted to one pair per line
ok: [localhost] => {
    "msg": [
        ["Ethernet0/0", "OOB management"],
        ["Ethernet0/1", "CORE: hq-core2 Et0/1"],
        ["Ethernet0/2", "CORE: hq-edge1 Et0/2"],
        ["Ethernet0/3", "TEMP lab test - remove"],
        ["Loopback0", "HQ core loopback"]
    ]
}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0

```

Compare that with the gathered file. Live Et0/2 says `CORE: hq-edge1 Et0/1`. The backup says `CORE: hq-edge1 Et0/2`. The description was corrected on the router after that backup was taken, so the backup is stale. Nothing in `ansible-playbook` output warned me; the two data sets only disagreed once they were side by side.

## Which one to use

| State    | Talks to device | Input           | Output          |
| -------- | --------------- | --------------- | --------------- |
| gathered | yes, read only  | live config     | structured data |
| rendered | no              | structured data | CLI lines       |
| parsed   | no              | config text     | structured data |

All three report `changed=0` for the device, so they're safe in a scheduled job or against production on day one.

## What I'd do on a production network

Start with `gathered` across the whole inventory and commit the YAML to git next to the backups from part 4\. Read it. You'll find leftovers like my Et0/3 and that secondary address. Use `parsed` on old backups when the device is gone or you can't log in, and strip non interface sections first if the parser chokes. Use `rendered` to show reviewers the exact commands before anything is pushed. Only after the data looks right should it become the `config:` of a `replaced` task.

Next in the series: [Ansible for Cisco ISE: Network Devices, Device Groups and Endpoints with cisco.ise](https://www.pinglabz.com/ansible-cisco-ise-network-devices-endpoints/), managing network devices, device groups and endpoints over the ERS API.