# src/idempotent_operations.pyfromdataclassesimportdataclassfromenumimportEnumclassOperationStatus(Enum):"""Status of an idempotent operation."""CREATED="created"ALREADY_EXISTS="already_exists"UPDATED="updated"UNCHANGED="unchanged"FAILED="failed"@dataclassclassOperationResult:"""Result of an idempotent operation."""status:OperationStatusmessage:strdetails:dict=NoneclassIdempotentOperations:"""Idempotent network operations (safe to repeat)."""@staticmethoddefensure_vlan_exists(device,vlan_id:int,vlan_name:str)->OperationResult:""" Ensure VLAN exists with given name (idempotent). Safe to call multiple times—won't duplicate VLAN. """# Check if VLAN already existsoutput=device.send_command(f"show vlan id {vlan_id}")iff"VLAN{vlan_id:04d}"inoutput:# VLAN exists, verify name matchesifvlan_nameinoutput:returnOperationResult(status=OperationStatus.UNCHANGED,message=f"VLAN {vlan_id} exists with correct name",details={"vlan_id":vlan_id,"vlan_name":vlan_name})else:# VLAN exists but name differs, update itdevice.send_command("configure terminal")device.send_command(f"vlan {vlan_id}")device.send_command(f"name {vlan_name}")device.send_command("exit")device.send_command("end")returnOperationResult(status=OperationStatus.UPDATED,message=f"VLAN {vlan_id} name updated to {vlan_name}",details={"vlan_id":vlan_id,"vlan_name":vlan_name})# VLAN doesn't exist, create itdevice.send_command("configure terminal")device.send_command(f"vlan {vlan_id}")device.send_command(f"name {vlan_name}")device.send_command("exit")device.send_command("end")returnOperationResult(status=OperationStatus.CREATED,message=f"VLAN {vlan_id} created with name {vlan_name}",details={"vlan_id":vlan_id,"vlan_name":vlan_name})@staticmethoddefensure_interface_config(device,interface:str,config_lines:list)->OperationResult:""" Ensure interface has specific configuration (idempotent). Args: device: Netmiko device interface: Interface name (e.g., "Gi0/0/1") config_lines: List of config commands to apply """# Get current interface configurationcurrent_config=device.send_command(f"show running-config interface {interface}")# Check if all config lines already existall_exist=all(lineincurrent_configforlineinconfig_lines)ifall_exist:returnOperationResult(status=OperationStatus.UNCHANGED,message=f"Interface {interface} already has required configuration",details={"interface":interface,"lines":config_lines})# Apply missing configurationdevice.send_command("configure terminal")device.send_command(f"interface {interface}")forconfig_lineinconfig_lines:device.send_command(config_line)device.send_command("exit")device.send_command("end")returnOperationResult(status=OperationStatus.UPDATED,message=f"Interface {interface} configuration applied",details={"interface":interface,"lines":config_lines})@staticmethoddefensure_bgp_neighbor(device,asn:int,neighbor_ip:str,neighbor_asn:int)->OperationResult:"""Ensure BGP neighbor is configured (idempotent)."""# Check if neighbor already existsoutput=device.send_command("show ip bgp summary")ifneighbor_ipinoutput:returnOperationResult(status=OperationStatus.UNCHANGED,message=f"BGP neighbor {neighbor_ip} already configured",details={"neighbor_ip":neighbor_ip})# Configure BGP neighbordevice.send_command("configure terminal")device.send_command(f"router bgp {asn}")device.send_command(f"neighbor {neighbor_ip} remote-as {neighbor_asn}")device.send_command("exit")device.send_command("end")returnOperationResult(status=OperationStatus.CREATED,message=f"BGP neighbor {neighbor_ip} configured",details={"neighbor_ip":neighbor_ip,"neighbor_asn":neighbor_asn})
fromnetmikoimportConnectHandlerfromidempotent_operationsimportIdempotentOperations,OperationStatusdevice=ConnectHandler(device_type="cisco_ios",host="10.0.0.1",username="admin",password="password")ops=IdempotentOperations()# Run 1: Creates VLAN 100result=ops.ensure_vlan_exists(device,100,"DATA")print(f"{result.status.value}: {result.message}")# Output: created: VLAN 100 created with name DATA# Run 2: Detects VLAN 100 already existsresult=ops.ensure_vlan_exists(device,100,"DATA")print(f"{result.status.value}: {result.message}")# Output: unchanged: VLAN 100 exists with correct name# Run 3: Safe to call again and againresult=ops.ensure_vlan_exists(device,100,"DATA")print(f"{result.status.value}: {result.message}")# Output: unchanged: VLAN 100 exists with correct namedevice.disconnect()
# ❌ IMPERATIVE - Order-dependentdevice.send_command("interface Gi0/0/1")device.send_command("ip address 10.0.0.1 255.255.255.0")# If someone manually removed the IP later, this script doesn't fix it
device=ConnectHandler(device_type="cisco_ios",host="10.0.0.1",username="admin",password="password")configurator=StateDrivenConfigurator(device)desired_state={"interfaces":{"Gi0/0/1":{"ip":"10.0.0.1","netmask":"255.255.255.0"}},"vlans":{"100":{"name":"DATA"},"101":{"name":"VOICE"}}}# Run 1: Applies all configurationconfigurator.apply_desired_state(desired_state)# Run 2: Checks state, skips what's already correctconfigurator.apply_desired_state(desired_state)# Run 3-N: Safe to run anytimeconfigurator.apply_desired_state(desired_state)
# src/nornir_idempotent_tasks.pyfromnornirimportInitNornirfromnornir.core.taskimportTask,Resultfromidempotent_operationsimportIdempotentOperationsdefensure_vlans(task:Task)->Result:"""Nornir task: Ensure VLANs exist (idempotent)."""device=task.host.get_connection("netmiko")ops=IdempotentOperations()# Get VLAN configuration from inventory or task datavlans=task.host.get("vlans",{})results=[]forvlan_id,vlan_configinvlans.items():result=ops.ensure_vlan_exists(device,vlan_id,vlan_config["name"])results.append(result)returnResult(host=task.host,result={"vlans":[{"vlan_id":r.details["vlan_id"],"status":r.status.value,"message":r.message}forrinresults]})# Usagenr=InitNornir(config_file="config.yaml")results=nr.run(task=ensure_vlans)# Results show what changedforhostname,resultinresults.items():forvlaninresult[0].result["vlans"]:print(f"{hostname}: VLAN {vlan['vlan_id']} - {vlan['status']}")
# ✅ GOODdefensure_vlan_exists(device,vlan_id):# 1. Checkifvlan_exists(device,vlan_id):return"already_exists"# 2. Create only if neededcreate_vlan(device,vlan_id)return"created"# ❌ BAD - Assumes creation is neededdefcreate_vlan(device,vlan_id):device.send_command(f"vlan {vlan_id}")# Fails if exists!
# ✅ GOOD - Single responsibilitydefensure_vlan_exists(device,vlan_id,vlan_name):# Only concerned with VLAN existence# ❌ BAD - Too much responsibilitydefconfigure_vlan_and_interfaces_and_bgp(device,vlan_id):# Does too many things, hard to reason about
deftest_operation_is_idempotent(mock_device):"""Verify running operation twice is safe."""# Run 1result1=ensure_vlan_exists(mock_device,100,"DATA")assertresult1.status==OperationStatus.CREATED# Run 2 - should be saferesult2=ensure_vlan_exists(mock_device,100,"DATA")assertresult2.status==OperationStatus.UNCHANGED# Run 3 - still saferesult3=ensure_vlan_exists(mock_device,100,"DATA")assertresult3.status==OperationStatus.UNCHANGED