In Part 4 of this series, we decoupled our network intent from the execution logic, using Jinja2 templates and model-driven YANG schemas to push VPLS configurations to our Nokia SR OS nodes via NETCONF.
But seeing a yellow CHANGED status in your terminal is only half the battle. The ultimate question isn’t “Did the configuration push successfully?”, it is “Is the service actually operational in the data plane?”
In this fifth and final part of our series, we close the loop. We will look at how to treat network operational states as structured data, execute automated Pre-Checks vs. Post-Checks, and build an immutable validation gate using Ansible assertions to guarantee compliance across our Containerlab fabric.
The Philosophy of Automated Verification
In this NetDevOps pipeline, we move from a model of reactive firefighting to proactive assertion.
Instead of treating verification as a manual afterthought, we write explicit test assertions that treat our operational state like unit tests in software engineering.
(Verify baseline is stable)"] B["2. EXECUTE CONFIG CHANGE
(Deploy VPLS Service)"] C["3. CAPTURE STATE POST-CHECK
(Query state namespace)"] D["4. RUN AUTOMATED ASSERTION
(Pass/Fail based on elements)"] %% Flow Layout A --> B B --> C C --> D %% Styling & Theme Customization classDef boxes fill:#1e1e2e,stroke:#313244,stroke-width:2px,color:#cdd6f4; classDef highlight fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4; class A,B,C boxes; class D highlight; %% Global link styling linkStyle default stroke:#6c7086,stroke-width:2px,linear;
By querying the operational state datastore (state branches in YANG models) instead of configuration datastore, the router returns data paths including packet counters, operational flags, physical link states, and so on.
1. The Strategy: Pre-Checks vs. Post-Checks
An automated validation playbook should run twice during an execution window:
- Pre-Check: Executed immediately before the configuration change to ensure the node is healthy, the transport tunnels are up, and the service ID we want to allocate isn’t already taken by another customer.
- Post-Check: Executed immediately after the configuration change. It compares the live state against our engineering definitions to confirm that the service transitioned smoothly into an operational state.
To fulfill both requirements elegantly without managing two completely separate scripts, we will build a single verification playbook that dynamically shifts its validation logic based on a runtime variable called validation_phase passed via CLI extra variables (--extra-vars).
2. Parsing Live Operational State via NETCONF
Because we are using model-driven architecture, we bypass human-readable CLI outputs entirely. We query the router using the ansible.netcommon.netconf_get module, pointing explicitly to the Nokia SR OS state datastore.
Create a file named verify_services.yml:
---
- name: "VPLS Service State Validation"
hosts: platforms_nokia_sros
gather_facts: false
tasks:
- name: "Query Operational VPLS State Namespace via NETCONF"
ansible.netcommon.netconf_get:
display: xml
filter: |
<state xmlns="urn:nokia.com:sros:ns:yang:sr:state">
<service>
<vpls>
<service-id>1</service-id>
</vpls>
</service>
</state>
register: vpls_live_state
- name: "Debug Live State Return Structure"
ansible.builtin.debug:
var: vpls_live_state.output
What happens here?
When this task runs, the Nokia SR OS node doesn’t spit out unparsed text. It returns an exact XML structure detailing every component of VPLS Service 1, mapping directly into our register variable vpls_live_state.
3. Building the Validation Gate: Ansible Assertions
With our operational data captured, we pass it into the ansible.builtin.assert module. This module evaluates boolean expressions. By introducing our validation_phase variable alongside logical or statements, we can toggle our validation gates seamlessly based on whether the pipeline is in the pre-change or post-change phase.
Let’s expand verify_services.yml to include a task for the conditional pass/fail thresholds:
- name: "Assert Service Operational Compliance"
ansible.builtin.assert:
that:
# POST-CHECK CONDITIONS: Evaluated only when validation_phase is 'post'
- "validation_phase == 'pre' or vpls_live_state.output.state.service.vpls['admin-state'] == 'enable'"
- "validation_phase == 'pre' or vpls_live_state.output.state.service.vpls['oper-state'] == 'up'"
# PRE-CHECK CONDITION: Evaluated only when validation_phase is 'pre'
- "validation_phase == 'post' or vpls_live_state.output.state.service.vpls is not defined"
fail_msg: "CRITICAL ALERT: Operational validation failed during {{ validation_phase }} check!"
success_msg: "SUCCESS: Fabric passed {{ validation_phase }} check requirements."
Deconstructing the Assertions:
The
validation_phase == 'pre' or ...Logic: In boolean operations, anorstatement stops evaluating as soon as it encounters a true value. During a Pre-Check, the first statement evaluates toTrue, meaning Ansible completely skips looking at theadmin-stateoroper-state(which wouldn’t exist yet, avoiding a playbook crash). During a Post-Check, the first block isFalse, forcing Ansible to verify the live states areenableandup.The Intent Leak Check (
is not defined): During our Pre-Check, we want to ensure we aren’t accidentally overwriting an active deployment. If VPLS 1 is completely unconfigured,vpls is not definedevaluates toTrueand the check passes. If the service already exists, it evaluates toFalseand safely halts the pipeline before a single line of configuration is changed.
Executing the Verification Engine
By passing the validation_phase parameter explicitly via runtime extra variables (-e), we can drive our entire pipeline cycle cleanly.
Phase 1: Running the Pre-Check Gate
Run the verification test runner before executing your configuration playbook to establish your safe baseline:
ansible-playbook -i netbox_inv.yml verify_services.yml -e "validation_phase=pre"
If VPLS Service 1 is completely clear, the assertions pass smoothly, indicating the fabric is completely ready for deployment.
Phase 2: Running the Post-Check Gate
After running your configuration deployment playbook from Part 4, execute the exact same validation playbook, updating the runtime flag to verify the data plane:
ansible-playbook -i netbox_inv.yml verify_services.yml -e "validation_phase=post"
Expected Output Log:
PLAY [VPLS Service State Validation] ***************************************************************
TASK [Query Operational VPLS State Namespace via NETCONF] ******************************************
ok: [PE1]
ok: [PE2]
TASK [Assert Service Operational Compliance] *******************************************************
ok: [PE1] => {
"changed": false,
"msg": "SUCCESS: Fabric passed post check requirements."
}
ok: [PE2] => {
"changed": false,
"msg": "SUCCESS: Fabric passed post check requirements."
}
PLAY RECAP *****************************************************************************************
PE1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
PE2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Because our post-conditions passed perfectly, the pipeline exits cleanly with zero failures. We have successfully verified our network state across both phases using structured telemetry loops.
Reflecting on the Complete NetDevOps Pipeline
Over this 5-part journey, we have focused on building a solid foundation for network automation, mapping out a clean and functional pipeline using a local lab environment:
Part 1 (NetBox Source of Truth): We moved out of static spreadsheets and defined our intended network state inside a structured data model.
Part 2 (Dynamic Ansible Handshake): We transitioned away from static host inventories, leveraging API integration to discover our lab layout dynamically.
Part 3 (Topology Orchestration with Containerlab): We treated our testing infrastructure like code, spinning up lightweight virtual networks instantly from declarative files.
Part 4 (Decoupling Logic with Jinja2/YANG): We separated our actual service variables from the deployment logic, pushing model-driven XML payloads over NETCONF.
Part 5 (State Validation): We established basic validation gates, showing how to check the actual state of the network before and after a change.
This cohesive loop represents a practical shift from manual, command-by-command administration toward systematic, repeatable automation patterns.
Alternative/Next-Step Validation Tooling
As your automation framework grows, you can extend this native Ansible/NETCONF model to incorporate deeper specialized systems:
pyATS / Genie Framework: A dedicated Python testing testbed ecosystem excellent for state profiling and generating strict network configuration “diffs” before and after changes.
gNMI / Streaming Telemetry: Swapping query-based polling loops (
netconf_get) for real-time, event-driven protobuf push notifications to monitor performance states dynamically.
Thank you for following along with this building journey! Keep automating!!