Domain 1 of 4 · Chapter 1 of 6

Network Automation with Ansible

Two ways to configure IOS: declarative or imperative

Two Ansible modules can add the same VLAN, and the difference between them is the spine of this whole page:

# Imperative: hand the device CLI lines
- cisco.ios.ios_config:
    parents: vlan 20
    lines: [name SERVERS]

# Declarative: hand the module the desired resource
- cisco.ios.ios_vlans:
    config: [{ vlan_id: 20, name: SERVERS }]
    state: merged

The ios_config[1] version is imperative: you type CLI lines and the module sends them, exactly as you would at a console. The ios_vlans[2] version is declarative: you describe the end state as structured data and the module makes the device match it. A resource module is that declarative kind, one per feature area (VLANs, ACLs, an OSPF process), and it runs the read-diff-push loop the diagram traces: it reads the running config off the device, computes the difference against your data, and pushes only that delta over a connection plugin (the transport, covered in "Reaching the device").

Two properties fall out of that loop. The module is declarative, so you state the goal rather than the ordered steps, and it is idempotent: applying the same input converges the device once and then reports no further change. Run ios_vlans with state: replaced and the running VLAN database is rewritten to match your list; run it again and it reports changed: false. That zero-change re-run is the signal the device already matches source control, which is why the guidance is one line: reach for the resource module when one models the feature, and keep ios_config for raw CLI that nothing else covers (network resource modules[3]).

Desired state(data)Read runningconfigComputethe diffPush onlythe deltaConverged(idempotent)
Reconciliation loop: a resource module reads the running config, diffs it against your data, and pushes only the delta to converge. Source: Ansible network resource modules guide.

Resource modules: the state keyword and module choice

One keyword, state, decides how much of the device a resource module is allowed to change, and getting it wrong is the difference between fixing one ACL and wiping every ACL on the box. Read the four write states as an axis of increasing scope:

  • merged (the default) adds or updates the attributes you list and removes nothing. Listing VLAN 10 with a new name changes that name and leaves every other VLAN alone.
  • replaced rewrites each resource you name to match your definition exactly, but only the resources you name. state: replaced on one named ACL[4] rewrites that ACL's entries and touches no other ACL.
  • overridden rewrites the whole class and deletes anything you did not list. state: overridden on ios_vlans reconciles every VLAN on the device to your list and removes the VLANs absent from it.
  • deleted removes the resources you name (or, with an empty config, the whole class).

The trap the exam leans on is replaced versus overridden. Both rewrite config, so they read as interchangeable, but replaced is per-resource and leaves unlisted resources alone, while overridden is device-wide and deletes them. To remove VLANs no longer in source control the answer is overridden; to fix one VLAN without disturbing the others it is replaced. The diagram draws these as nested boundaries: the inner box is the attributes merged touches, the middle box is the one resource replaced rewrites, and the outer box is the whole class overridden reconciles.

Read-only states never touch the device

Three more states transform config without changing anything. gathered returns the live config as structured facts (the read half of the loop), rendered turns your input into device CLI without connecting at all, and parsed converts a supplied running-config text into structured data for migration. Because rendered and parsed open no connection, they run in CI with no lab.

Choosing the right module

The naming rule is short: the modern declarative modules are plural, and the singular ones are their removed predecessors.

Module Manages
cisco.ios.ios_vlans The VLAN database (singular ios_vlan was removed)
cisco.ios.ios_acls Named/numbered IPv4/IPv6 ACLs as structured ACEs
cisco.ios.ios_ospfv2[5] The OSPFv2 process: areas, networks, redistribution, timers
cisco.ios.ios_ospf_interfaces Per-interface OSPF settings
cisco.ios.ios_interfaces[6] L1/L2 attributes: description, enabled, mtu, speed, duplex
cisco.ios.ios_l3_interfaces[7] L3 addressing: IPv4/IPv6 on an interface

Two naming traps recur. The singular cisco.ios.ios_vlan was deprecated and removed from the cisco.ios collection after 2022-06-01, and even before removal it could only add VLANs, never declaratively replace the set, so the plural ios_vlans is always today's call. And the interface split is real: ios_interfaces owns L1/L2 attributes while ios_l3_interfaces owns IP addressing, so trying to set an address with ios_interfaces puts it nowhere. Routing splits the same way, ios_ospfv2 for the process and ios_ospf_interfaces per interface. A stem that says manage the OSPF process declaratively wants ios_ospfv2, not ios_config; return the current config as structured data with no change wants state: gathered, the one read that hits the live device.

All resources of this typestate: overridden (deletes unlisted)One listed resourcestate: replacedListed attributesstate: merged (adds or updates)
Scope by state: merged touches listed attributes, replaced one resource, overridden the whole class and deletes unlisted. Source: cisco.ios docs.

ios_config for raw CLI: parents, match, save_when

ios_config is the right tool for raw CLI that no resource module models, banners, one-off global commands, service tweaks, and it earns three options worth knowing cold. A single task is CLI with a little structure around it:

- cisco.ios.ios_config:
    parents: interface GigabitEthernet1/0/1
    lines:
      - ip access-group ACL_IN in
      - description UPLINK-TO-CORE
    match: line
    save_when: changed

lines (alias commands) is the ordered CLI to send. parents is the ordered configuration-mode context the module enters first, so the two lines land inside interface GigabitEthernet1/0/1 rather than at the global level. That placement is what ios_config compares against show running-config, so the text must match what the device stores, including full non-abbreviated forms: int Gi1/0/1 will not match the stored interface GigabitEthernet1/0/1 and re-sends every run.

match decides whether the task is idempotent

Whether a second run reports ok or changed comes down to match, which defaults to line. The diagram splits on it. With match: line the module reads the running config, compares each candidate line, and sends only the missing ones, so a converged device receives nothing, that is idempotency, and it is on by default; strict and exact are the other diff-aware values (they tighten how ordering and completeness are compared) and behave the same way for this purpose, diffing against the running config and sending only what is missing. match: none alone skips the comparison and re-sends the full lines list on every run, reporting changed forever. This is the single most tested fact about the module: idempotent by default, but match: none throws idempotency away. Reserve it for a genuine unconditional replay, never as a way to "make sure it applies."

save_when persists to startup without churn

An ios_config change lands in running-config and stops there; save_when decides when it is copied to startup so it survives a reload. The default is never.

save_when Copies running to startup when Idempotent
never (default) never yes, but the change is not persisted
modified running and startup differ yes
changed this task changed running-config yes
always every run no, reports changed each time

The favourite detail is modified versus changed: modified looks at the device and saves whenever running and startup differ (including an out-of-band change someone else made), while changed looks at the task and saves only when this run altered config. Both keep a converged play green; a stem that mentions an out-of-band change points at modified. save_when: always is the usual reason a playbook never turns green, because it saves and reports changed on every run. When a structured feature (VLANs, OSPFv2, an ACL, an interface address) is in play, the maintainable answer is the matching resource module, not a hand-built ios_config block, and ios_command[8] is a trap that only runs show/exec output and cannot change config at all.

candidate lines (your task)match value?line / strict / exactnonediff vs running-configsend only missing linesidempotentno diffsend every line each runnot idempotent
match: line/strict/exact diff against running-config and send only what is missing; match: none skips the diff and re-sends every line.

Reaching the device: connection plugins and inventory

A switch runs no on-box Python, so Ansible keeps the module on the control node, runs it locally, and reaches the device through a connection plugin that speaks the device's own protocol. Two inventory variables steer that: ansible_connection picks the transport, and ansible_network_os: cisco.ios.ios names the platform so the plugin loads the right command set and parser (set network_os wrong and you get a wall of parser errors). The three network transports live in the ansible.netcommon collection, and the diagram traces one task from the control node through whichever you pick.

Aspect network_cli netconf httpapi
ansible_connection ansible.netcommon.network_cli ansible.netcommon.netconf ansible.netcommon.httpapi
Transport / port SSH, TCP 22 NETCONF over SSH, TCP 830 HTTP(S), TCP 443 with SSL
On the wire CLI commands NETCONF RPC (XML) JSON over REST
Typical target IOS/IOS-XE via cisco.ios YANG config datastores RESTCONF, Catalyst Center

network_cli[9] is the SSH command line and the workhorse for ios_config and the resource modules. netconf[10] exchanges XML RPCs on port 830 and needs the ncclient library on the control node, which is itself the tell that a scenario means NETCONF. httpapi[11] carries RESTCONF and controller APIs such as Cisco Catalyst Center[12] as JSON over HTTPS; turn on TLS with ansible_httpapi_use_ssl: true and the port defaults to 443. An API transport reads and writes structured data instead of screen-scraping a terminal.

Log in, then elevate with become: enable

On network_cli, authenticating and reaching config mode are two steps, and forgetting the second is the classic privilege error. ansible_user and ansible_password open the SSH session and land you at user EXEC (switch>); configuration lives behind privileged EXEC (switch#), so you elevate with become: true and become_method: enable (plus ansible_become_password when an enable secret is set). Read this carefully: become here is not sudo. The meaningful become_method on a network device is enable, which maps to the IOS enable command, so an answer using become_method: sudo is wrong by construction. A read-only ios_command running show stays at user EXEC and needs no elevation.

Scope variables with group_vars and host_vars

Credentials belong in inventory, not playbooks. Put settings shared by a class of devices in group_vars/<group>.yml and per-device exceptions in host_vars/<host>.yml; where both set the same variable, host_vars outranks group_vars[13]. So set the norm once at the group and override only the exception per host. Two guardrails: group_vars/host_vars are plain YAML on disk, so any secret in them must be encrypted with Ansible Vault (next section), and a host in many groups inherits from all of them, which makes a value's origin hard to trace.

Control noderuns the modulenetwork_cliSSH CLI, TCP 22netconfNETCONF, TCP 830httpapiRESTCONF JSON, TCP 443Network deviceno on-box Python
One task runs on the control node and reaches the device through the selected connection plugin. Source: Ansible network connection options guide.

Protecting secrets with Ansible Vault

A network-automation repo fills with values you cannot commit as clear text: the enable secret, a Catalyst Center API token, a TACACS password. Ansible Vault[14] keeps them in the same Git repo as your playbooks by storing them as ciphertext. The mental model is one sentence: ansible-vault applies symmetric AES256 encryption to content at rest, and the vault password is the only key. At rest is load-bearing, Vault protects files on disk, not the SSH or TLS session to the device, which is secured separately. Debunk the common confusion on sight: the ansible-vault CLI baked into Ansible and the HashiCorp Vault server (a network-reachable secrets manager) share a word and nothing else; an ansible-vault encrypt on a .yml file always means the Ansible tool.

The diagram follows one secret end to end. You run ansible-vault encrypt (or encrypt_string) once, turning plaintext into ciphertext; that ciphertext is what you commit, so the repo never holds the plaintext. At run time you supply the vault password, Ansible decrypts the value in memory for that run, and never writes plaintext back to disk.

Encrypt a whole file, or a single value

ansible-vault encrypt group_vars/all/vault.yml replaces a file's whole contents with one $ANSIBLE_VAULT;1.1;AES256 blob, unreadable in a diff, which suits a dedicated secrets file. You still work with it through ansible-vault edit, view, and rekey (which re-encrypts under a new password in one step, with no plaintext-on-disk window). ansible-vault encrypt_string instead encrypts one value[15] and prints a !vault | YAML snippet you paste into an otherwise plaintext vars file, so variable names and structure stay readable in review while only the secret is opaque.

The password is the whole trust boundary

Because the AES key is derived from the vault password, protecting that password is the entire job. Supply it only at run time: --ask-vault-pass prompts interactively (fine for a human, useless in a pipeline that has no terminal to answer), and --vault-password-file ~/.vault_pass reads it from a file or an executable script, which is how a CI job injects it from its own protected store. When one play mixes secrets under different passwords, --vault-id label@source[16] names each (--vault-id prod@prompt --vault-id lab@lab-pass.txt) and is also the supported bridge to an external secrets manager through a client script.

The headline anti-pattern the exam tests: encrypting a vars file but committing its vault password file next to it gives no real protection, because anyone who clones the repo then holds both halves. The password must reach the run from outside source control, a prompt, a CI secret variable, or a secrets manager, so repository access alone never yields plaintext. And "commit it in a private repo" is not the fix: private is access control, not encryption.

Plaintextsecretansible-vaultencrypt (AES256)Ciphertextin GitRun playbook+ vault passwordDecryptedin memory
One secret's lifecycle: encrypt once with AES256, commit the ciphertext, decrypt in memory at run time with the vault password. Source: Ansible Vault guide.

Playbooks, roles, idempotency, and safe change

Run a fully converged playbook a second time and the recap reads changed=0 on every host, that zero is the whole point. Idempotency (introduced in "Two ways to configure IOS") shows up operationally in the PLAY RECAP[17]: a task reports changed only when it actually altered the device, so a green changed=0 run tells you the running config still matches source control, and a surprise changed=2 on a play you did not edit tells you something drifted. The crucial caveat is that idempotency is a property of the module, not of Ansible: a resource module diffs and converges, but a raw command module (ios_command, shell, raw) cannot know its effect was already present and reports changed every run, so a changed=0 recap means nothing for a play built out of those.

Preview safely with check mode and diff

ansible-playbook --check --diff is the dry run before production. Check mode[18] has each module compute the change it would make and report changed/ok without sending anything, and --diff prints the exact configuration delta line by line. Check mode is per-module, the network resource modules support it while a raw command module cannot predict its own effect, and --diff also works on a real apply, turning an ordinary run into an audit log of what moved.

Roles package automation; handlers fire once

A role[19] gives reusable automation a fixed layout, tasks/ handlers/ templates/ vars/ defaults/ meta/, and Ansible auto-loads main.yml from each. The defaults/ versus vars/ split is the part exams probe: defaults/main.yml holds the lowest-precedence variables a caller is meant to override, while vars/main.yml holds high-precedence values that an ordinary playbook variable will not. A handler[20] is a task that runs only when another task notifys it, and only if that task reported changed; all notified handlers run once, at the end of the play, in definition order, exactly the semantics for a single save config after a batch of changes and never on a converged run.

One template for a fleet

Jinja2[21] is how one playbook fits devices that are not identical. Ansible evaluates {{ ... }} from inventory, host_vars, and group_vars; the template module renders a .j2 file per host; loop (the modern replacement for with_items) repeats a task once per list element for a variable-length set of VLANs or ACLs; and when gates a task on an expression such as a device fact, so a single play branches across a heterogeneous fleet. Inventory data plus one template equals per-device config.

Failure handling and rollback

Internalize the rule first: Ansible does not roll back. When a task fails the play stops for that host, but earlier changes stay applied, so rollback is something you write, using the block[22] construct the diagram traces. block groups the change tasks; rescue runs only if a block task fails (put undo/restore steps here); always runs on both paths for cleanup or a final notify. Two task keywords decide whether the rescue path is reached, and they are the pair the exam most often swaps: failed_when[23] redefines what counts as a failure (a command can exit 0 yet be a failure because its output contains Invalid), while ignore_errors: true changes what happens after a failure by letting the play continue. They are not interchangeable, and an answer that uses ignore_errors to mean redefine-failure is wrong.

block:change tasksA block taskfailed?rescue:undo / restorealways: notify handlers, cleanupruns whether or not rescue ranYes / failedNo / ok
block/rescue/always: rescue runs only on a block failure, always runs cleanup on both paths. Source: Ansible blocks guide.

Reading device facts and operational state

SSH into a switch and run show version: it prints the model, chassis serial, and running image. Open show running-config and none of those three appear. That gap is this section. A managed device holds two kinds of data. Configuration is the intended settings you push, interfaces, routing, ACLs, VLANs, what show running-config returns and NETCONF calls a configuration datastore (RFC 6241[10]). Operational state is the live, read-only data the device computes about itself: model, serial number, software version, oper-status and counters, neighbor and route tables, uptime. You cannot set operational state; the device reports it.

Device facts and asset inventory are operational state, so a task that says "build a CMDB keyed on serial number" is asking for state, never configuration. The diagram fixes that split, and every tool below exposes a config-read beside a state-read; every trap in this area is a config-read offered where the goal needs the state-read.

The three surfaces, one split

  • Ansible. cisco.ios.ios_facts[24] gathers a device into ansible_net_* variables (ansible_net_model, ansible_net_serialnum, ansible_net_version, ansible_net_hostname), the standard asset feed; gather_subset (all, min, hardware, interfaces, config) tunes cost, and hardware is usually right for inventory. The trap: a resource module's state: gathered returns that one feature's structured configuration, not a serial or model, so when the stem asks for hardware/software inventory, ios_facts is the answer and gathered is the distractor.
  • RESTCONF turns the split into a query knob: GET with content=nonconfig returns operational state only, content=config returns configuration only, and omitting content returns both (the default is all) (RFC 8040 §4.8.1[11]). So the common misread, that a plain RESTCONF GET returns just configuration, is wrong; read state alone with content=nonconfig.
  • NETCONF makes it two operations: get-config returns configuration only from a named datastore, while get returns configuration and operational state together. To read counters or neighbor state over NETCONF you issue get, never get-config.

RESTCONF content=config lines up with NETCONF get-config, and RESTCONF's default all with NETCONF get; the one asymmetry is that NETCONF has no single state-only verb.

Python: retrieve, then parse

In Python you read live state with a purpose-built call, then parse the reply, never regex raw text. ncclient[25] issues the same RPCs, m.get(...) for config-plus-state and m.get_config(source='running', ...) for config only, returning XML you parse with xmltodict or lxml. netmiko runs send_command("show version") and returns raw text whose column positions shift between releases, so scraping it with regular expressions is brittle. Turn the reply into a dictionary instead: Cisco pyATS/Genie[26] parses hundreds of show commands (device.parse("show version")["version"]["chassis_sn"]), and netmiko can apply a TextFSM template with use_textfsm=True. Either way you key on named fields, so a cosmetic output change does not break the collector. One rule collapses the whole section: choose the read whose result set includes operational state, and treat every config-only read (gathered, content=config, get-config) as the trap whenever the question asks for device facts or asset inventory.

Network deviceConfiguration (what you push)Interfaces, IP, routingACLs, VLANs, QoS policySet via edit-config / PATCHReturned by show running-configOperational state (read-only facts)Model, serial number, versionOper-status, counters, uptimeNeighbors, ARP, route tablesNever in running-configAsset inventory = operational state
A device's data splits into configuration (what you push) and operational state (read-only facts); asset inventory lives in operational state.

Ansible resource-module state values

Behaviormergedreplacedoverriddendeleted
Default stateYesNoNoNo
Pushes config to deviceYesYesYesYes
Scope of changeListed attributesListed resourcesWhole classListed resources
Deletes unlisted resourcesNoNoYesNo
Typical useAdd or adjustEnforce one resourceGolden configDecommission

Decision tree

Configure the device,or read its state?Read stateios_facts, RESTCONF content=nonconfig,NETCONF getResource module modelsthis feature?read stateconfigureios_config (raw CLI)match: linesave_when: changedEnforce exact set,delete extras?no modulemodeledResource modulestate: merged / replacedadditive / per-resourceResource modulestate: overriddendeletes unlistedadditiveexact set

Sharp facts the exam loves — give these one last read before exam day.

Cheat sheet

Sharp facts the exam loves — scan these before test day.

cisco.ios.ios_vlans with state replaced converges the VLAN database

The cisco.ios.ios_vlans resource module is the modern declarative module for VLANs; with state: replaced it overwrites the config of the listed VLANs so the running VLAN entries match the playbook exactly, giving idempotent convergence rather than blind CLI pushes.

Trap Distractor uses the deprecated singular cisco.ios.ios_vlan which only adds VLANs and cannot declaratively replace the set.

4 questions test this
Resource-module states: merged, replaced, overridden, deleted

Ansible network resource modules accept state values merged (default, additive), replaced (replace only the specified resources), overridden (replace and remove every resource not listed), and deleted. For VLANs, overridden deletes all VLANs absent from the playbook while replaced touches only the listed ones.

Trap Confusing replaced (per-resource) with overridden (whole-list) — overridden will wipe VLANs you did not list.

7 questions test this
gathered, rendered, and parsed states never touch the device

The gathered state returns current config as structured facts, rendered produces device CLI from the module input without connecting, and parsed converts a supplied running-config text into structured data. None of these three push configuration to the device.

3 questions test this
The singular ios_vlan module was deprecated and removed

The legacy cisco.ios.ios_vlan (singular) module was deprecated and removed from the cisco.ios collection after 2022-06-01; playbooks should use the plural cisco.ios.ios_vlans resource module for declarative, idempotent VLAN management.

cisco.ios.ios_ospfv2 manages OSPF process config declaratively

cisco.ios.ios_ospfv2 manages the OSPFv2 process (areas, networks, redistribution, timers) as structured data, while cisco.ios.ios_ospf_interfaces manages per-interface OSPF settings; state: merged adds, replaced rewrites one process, overridden rewrites all.

Trap Using the generic ios_config module for OSPF instead of the declarative ios_ospfv2 resource module.

4 questions test this
cisco.ios.ios_acls manages ACLs as structured ACEs

cisco.ios.ios_acls manages named and numbered IPv4/IPv6 ACLs as a structured list of ACEs; state: replaced rewrites a single named ACL to the given ACEs, while state: overridden reconciles ALL ACLs on the device to the module's definition.

Trap Assuming state: replaced touches every ACL on the device (it is scoped to the named ACL; overridden is the device-wide one).

3 questions test this
ios_interfaces vs ios_l3_interfaces split interface concerns

cisco.ios.ios_interfaces manages L1/L2 interface attributes (description, enabled, mtu, speed, duplex); cisco.ios.ios_l3_interfaces manages the L3 addressing (IPv4/IPv6) on those interfaces — the correct module depends on which layer of interface setting is being automated.

Trap Trying to set an IP address with ios_interfaces (addressing lives in ios_l3_interfaces).

4 questions test this
ios_config parents builds a hierarchical config block

cisco.ios.ios_config pushes raw CLI imperatively; the parents list places the lines/commands into a config hierarchy, e.g. parents: interface GigabitEthernet1/0/1 with lines: ip access-group ACL_IN in enters that interface context before applying the lines.

5 questions test this
ios_config match none disables idempotent diffing

ios_config defaults to match: line, comparing each candidate line against the running-config and sending only missing lines; setting match: none disables that comparison so every command is pushed on every run, destroying idempotency.

Trap Assuming ios_config is always idempotent — with match: none (or no diff-aware model) it re-pushes lines every run.

5 questions test this
save_when: modified writes startup-config only on change

ios_config's save_when: modified copies running-config to startup-config only when the task actually changed the running-config, preserving idempotency; save_when: always writes every run and reports changed each time.

5 questions test this
ios_config is imperative; resource modules are declarative

For structured features (VLANs, OSPF, interfaces, ACLs) prefer the declarative resource modules (ios_vlans, ios_ospfv2, ios_l3_interfaces, ios_acls) which model and converge state; reserve ios_config for raw CLI that no resource module covers.

1 question tests this
network_cli, netconf, and httpapi connection plugins

Network devices set ansible_connection to ansible.netcommon.network_cli (SSH CLI), ansible.netcommon.netconf (NETCONF/830), or ansible.netcommon.httpapi (REST/RESTCONF), and ansible_network_os: cisco.ios.ios to select the platform parser and command set.

8 questions test this
become_method enable reaches privileged config mode

For network_cli, ansible_user/ansible_password authenticate the SSH session and become: true with become_method: enable (optionally ansible_become_password) elevates to privileged EXEC so configuration tasks can run.

2 questions test this
group_vars and host_vars scope connection variables

Inventory groups let group_vars/<group>.yml set shared connection variables while host_vars/<host>.yml overrides per device; this keeps credentials and ansible_network_os out of playbooks and scoped correctly.

2 questions test this
httpapi connection drives RESTCONF and controller APIs

The httpapi connection plugin (with ansible_httpapi_use_ssl: true and ansible_httpapi_port: 443) is used for RESTCONF and controller APIs such as Cisco Catalyst Center, where modules exchange JSON over HTTPS rather than screen-scraping CLI.

3 questions test this
ansible-vault encrypts secrets at rest with AES256

ansible-vault encrypt protects a var file with AES256 so credentials stay ciphertext in Git; ansible-vault encrypt_string embeds a single encrypted value inline in an otherwise plaintext vars file for selective secrecy.

4 questions test this
Vault password is supplied only at runtime

The decryption key is provided at run time via --ask-vault-pass or --vault-password-file, and --vault-id label@source supports multiple vaults; the plaintext secret never lives on disk or in source control.

6 questions test this
Storing the vault key in the repo defeats the purpose

Committing the vault password (or a symmetric key) alongside the encrypted file gives no real protection because anyone with repo access can decrypt; the key must come from an out-of-band source or a secrets manager at runtime.

Trap Encrypting the vars file but committing the vault password file next to it is a common secret-management anti-pattern.

Idempotency means re-runs report zero changes

An idempotent task reports changed only when it actually altered device state; re-running a fully converged playbook against the same devices yields ok with changed=0, which is the signal that the desired state already matches.

3 questions test this
Check mode previews changes without applying them

Running a playbook with --check (check mode) has resource modules compute and report the changes they would make without pushing anything, and --diff shows the exact config delta; together they give a safe dry-run before a real apply.

1 question tests this
Roles standardize layout; handlers run once when notified

A role uses the standard tasks/ handlers/ templates/ vars/ defaults/ meta/ layout for reuse; a handler runs once at the end of the play and only when a task that notifys it reported changed, ideal for a single save or service reload.

2 questions test this
Jinja2 templates and loops render per-device config

Jinja2 templates plus loop/with_items render device-specific values (VLAN IDs, ACL entries, OSPF areas) from inventory variables, and when conditionals gate tasks so one playbook adapts across a heterogeneous fleet.

1 question tests this
Ansible block/rescue/always enables change rollback

In Ansible, block groups tasks; rescue runs its tasks only if a task in the block fails (place restore/undo steps here); always runs regardless — together they give a change task automated rollback when a push or post-change verify fails, instead of leaving the device half-configured.

Trap Believing a failed task automatically rolls back prior changes without a rescue block.

1 question tests this
failed_when and ignore_errors shape failure handling

failed_when defines a custom failure condition on a task (e.g. fail when command output contains 'Invalid'), and ignore_errors: true lets the play continue past a failure so a later cleanup or rescue can run — both control when the recovery path triggers.

Trap Confusing ignore_errors (continue past failure) with failed_when (redefine what counts as failure).

2 questions test this
cisco.ios.ios_facts collects hardware/software inventory

cisco.ios.ios_facts gathers device facts into ansible_net_* variables (ansible_net_model, ansible_net_serialnum, ansible_net_version, ansible_net_hostname) — the standard way to collect hardware/software inventory for an asset database; gather_subset tunes what is collected.

Trap Expecting a resource module's 'gathered' state to return hardware serial/model — it returns per-feature CONFIG, not asset inventory.

2 questions test this
RESTCONF content=nonconfig returns operational state

A RESTCONF GET with query content=nonconfig returns operational (read-only) state such as interface oper-status and counters, while content=config returns only configuration and omitting content returns both — how live device state is read without changing config.

Trap Assuming a plain RESTCONF GET on a data node returns only configuration data.

3 questions test this
NETCONF returns config+state, config only

NETCONF returns configuration AND operational state, whereas returns configuration only from a named datastore — use to read operational/asset data such as counters or neighbor state.

Trap Using to read operational counters (it returns configuration only).

2 questions test this
Python retrieves and parses live device state

Python reads live device state by issuing ncclient get() RPCs or netmiko send_command show output, then parsing it into structured data with a Genie/pyATS parser or TextFSM for programmatic asset/state processing rather than screen-scraping raw text.

Trap Screen-scraping raw show output instead of parsing it into structured data.

4 questions test this

References

  1. https://docs.ansible.com/ansible/latest/collections/cisco/ios/ios_config_module.html
  2. https://docs.ansible.com/ansible/latest/collections/cisco/ios/ios_vlans_module.html
  3. https://docs.ansible.com/ansible/latest/network/user_guide/network_resource_modules.html
  4. https://docs.ansible.com/ansible/latest/collections/cisco/ios/ios_acls_module.html
  5. https://docs.ansible.com/ansible/latest/collections/cisco/ios/ios_ospfv2_module.html
  6. https://docs.ansible.com/ansible/latest/collections/cisco/ios/ios_interfaces_module.html
  7. https://docs.ansible.com/ansible/latest/collections/cisco/ios/ios_l3_interfaces_module.html
  8. https://docs.ansible.com/ansible/latest/collections/cisco/ios/ios_command_module.html
  9. https://docs.ansible.com/ansible/latest/network/user_guide/platform_ios.html
  10. https://www.rfc-editor.org/rfc/rfc6241
  11. https://www.rfc-editor.org/rfc/rfc8040
  12. https://developer.cisco.com/docs/dna-center/
  13. https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html
  14. https://docs.ansible.com/ansible/latest/vault_guide/vault.html
  15. https://docs.ansible.com/ansible/latest/vault_guide/vault_encrypting_content.html
  16. https://docs.ansible.com/ansible/latest/vault_guide/vault_managing_passwords.html
  17. https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html
  18. https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_checkmode.html
  19. https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html
  20. https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_handlers.html
  21. https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating.html
  22. https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_blocks.html
  23. https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_error_handling.html
  24. https://docs.ansible.com/ansible/latest/collections/cisco/ios/ios_facts_module.html
  25. https://ncclient.readthedocs.io/en/latest/manager.html
  26. https://developer.cisco.com/docs/pyats/