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 memory operations
  • 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_network
  • subnet_mask
  • next_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.