Idempotent Network Automation with Netpicker: Static Route Injection
Introduction
One of the core principles of reliable network automation is simple: only make a change when a change is actually needed.
This principle is called idempotency. An idempotent automation job can run once or many times and still produce the same desired end state without repeatedly pushing the same configuration.
In this article, we’ll look at a Netpicker automation job that adds a static route to Cisco IOS and Cisco IOS XE devices. The job follows a simple pattern:
check → act → verify
Why Idempotency Matters
Many scripts push configuration every time they run. For static routes, Cisco IOS may accept the same ip route command repeatedly, but that does not make it ideal.
Repeated configuration pushes can cause:
- Unnecessary
write memoryoperations - Noisy change logs
- Harder troubleshooting
- False confidence if the change is not verified
An idempotent job avoids this by checking the device first and making changes only when needed.
Where the Route Data Comes From
In this example, the route values are passed as job inputs:
destination_networksubnet_masknext_hop
In a real workflow, these values could also come from Slurpit or another source of truth. Slurpit can provide the route data, while Netpicker handles the device execution and verification.
The Netpicker Job
from comfy.automate import job
from ipaddress import IPv4Network
@job(platform=['cisco_ios', 'cisco_xe'])
def add_static_route(device, destination_network, subnet_mask, next_hop):
"""Add a static route and verify it exists. Rollback: remove_static_route."""
destination_network = str(destination_network).strip()
subnet_mask = str(subnet_mask).strip()
next_hop = str(next_hop).strip()
try:
IPv4Network((destination_network, subnet_mask), strict=True)
except ValueError as exc:
raise ValueError(
f"Invalid destination network/subnet mask combination: "
f"{destination_network} {subnet_mask}"
) from exc
existing = device.cli(f"show ip route {destination_network}")
if destination_network in existing and "directly connected" not in existing.lower():
print(f"[SKIP] Static route to {destination_network} already exists on {device.name}.")
return
print(f"[PRE-CHECK] Static route to {destination_network} does not exist. Proceeding...")
device.cli.send_config_set([f"ip route {destination_network} {subnet_mask} {next_hop}"])
device.cli("write memory")
print(f"[EXEC] Configuration saved on {device.name}.")
verification = device.cli(f"show ip route {destination_network}")
if destination_network not in verification:
raise RuntimeError(f"Static route to {destination_network} could not be verified on {device.name}.")
print(f"[SUCCESS] Static route to {destination_network} via {next_hop} created on {device.name}.")
How It Works
The job is scoped to Cisco IOS and Cisco IOS XE using the Netpicker @job decorator. This helps prevent the job from running against unsupported platforms.
The input values are normalized by trimming whitespace. Current Netpicker input handling can prevent required empty values from being submitted, so the job does not need separate empty-value checks for every field.
The destination network and subnet mask are validated using Python’s ipaddress module:
IPv4Network((destination_network, subnet_mask), strict=True)
The next_hop is intentionally not validated here because it may be either an IP address or an exit interface.
For example, both of these can be valid on Cisco IOS:
ip route 192.168.10.0 255.255.255.0 10.10.10.1
ip route 192.168.10.0 255.255.255.0 GigabitEthernet0/1
Before applying the route, the job checks the routing table:
existing = device.cli(f"show ip route {destination_network}")
If the route already exists and is not directly connected, the job exits without making a change. That means no repeated configuration push and no unnecessary write memory.
If the route is missing, Netpicker applies the static route and saves the configuration. After that, it checks the routing table again to verify that the route is present.
A Note on Next-Hop Matching
This example keeps the pre-check simple by checking whether the destination route already exists.
In a more advanced version, the job could also compare the expected next hop against the routing table. That would help detect cases where a route exists but points to a different next hop. This article keeps the focus on the basic idempotency pattern.
The Pattern
This job follows a reusable automation pattern:
| Step | Purpose |
|---|---|
| Normalize | Clean up the input values |
| Validate | Confirm the route format is valid |
| Check | Read the current device state |
| Act | Apply the change only if needed |
| Verify | Confirm the result |
This same approach can be used for VLANs, ACLs, interfaces, routing policies, and many other network changes.
Conclusion
Idempotency makes network automation safer and easier to trust.
This Netpicker static route job checks the current state before making a change, applies the route only when required, and verifies the result afterward. Whether the route data comes from manual input, Slurpit, or another source of truth, the important part is the same:
check first, change only when needed, and verify after execution.