Python Functions for Network Engineers

As network infrastructure grows increasingly complex, Python has become an essential tool for network engineers seeking to automate repetitive tasks and improve efficiency. One of the fundamental building blocks of Python programming is the function - a reusable block of code designed to perform a specific task.

Why Functions Matter in Network Automation

Network engineers often find themselves performing the same operations across multiple devices or configurations. Functions allow you to:

  1. Encapsulate common operations into reusable code
  2. Reduce errors through standardized processes
  3. Improve readability and maintainability of automation scripts
  4. Create modular code that can be combined into larger systems

Essential Network Functions

CIDR to Subnet Mask Conversion

One common task in networking is converting between CIDR notation (like /24) and subnet masks (like 255.255.255.0). Let's create a simple function to handle this conversion:

def cidr_to_subnet_mask(cidr):
    """
    Convert CIDR notation to a subnet mask
    
    Args:
        cidr (int): CIDR prefix length (0-32)
    
    Returns:
        str: Subnet mask in dotted decimal format
    """
    # Validate input
    if not isinstance(cidr, int) or cidr < 0 or cidr > 32:
        raise ValueError("CIDR must be an integer between 0 and 32")
    
    # Create a 32-bit binary string with 1's for the network portion
    binary = ('1' * cidr) + ('0' * (32 - cidr))
    
    # Split into 4 octets and convert to decimal
    octets = [binary[i:i+8] for i in range(0, 32, 8)]
    decimal_octets = [int(octet, 2) for octet in octets]
    
    # Return the subnet mask in dotted decimal notation
    return '.'.join(str(octet) for octet in decimal_octets)

Subnet Mask to CIDR Conversion

The complementary function to convert a subnet mask back to CIDR notation:

def subnet_mask_to_cidr(subnet_mask):
    """
    Convert a subnet mask to CIDR notation
    
    Args:
        subnet_mask (str): Subnet mask in dotted decimal format (e.g., '255.255.255.0')
    
    Returns:
        int: CIDR prefix length
    """
    # Validate input format
    octets = subnet_mask.split('.')
    if len(octets) != 4:
        raise ValueError("Subnet mask must be in dotted decimal format (e.g., '255.255.255.0')")
    
    # Convert to binary and count 1's
    try:
        binary = ''.join([bin(int(octet))[2:].zfill(8) for octet in octets])
        
        # Validate that the mask is contiguous (all 1's followed by all 0's)
        if '01' in binary:
            raise ValueError("Invalid subnet mask: non-contiguous mask detected")
            
        return binary.count('1')
    except ValueError:
        raise ValueError("Invalid subnet mask: each octet must be a number between 0 and 255")

IP Address in Subnet Checker

A function to check if an IP address belongs to a specific subnet:

def is_ip_in_subnet(ip_address, subnet_cidr):
    """
    Check if an IP address belongs to a subnet
    
    Args:
        ip_address (str): IP address to check (e.g., '192.168.1.5')
        subnet_cidr (str): Subnet in CIDR notation (e.g., '192.168.1.0/24')
    
    Returns:
        bool: True if the IP is in the subnet, False otherwise
    """
    import ipaddress
    
    try:
        network = ipaddress.IPv4Network(subnet_cidr, strict=False)
        ip = ipaddress.IPv4Address(ip_address)
        return ip in network
    except ValueError as e:
        raise ValueError(f"Invalid input: {e}")

Example Usage and Output

Let's see these functions in action:

# CIDR to subnet mask examples
print("CIDR to Subnet Mask Examples:")
print(f"/24 -> {cidr_to_subnet_mask(24)}")
print(f"/16 -> {cidr_to_subnet_mask(16)}")
print(f"/28 -> {cidr_to_subnet_mask(28)}")

# Subnet mask to CIDR examples
print("\nSubnet Mask to CIDR Examples:")
print(f"255.255.255.0 -> /{subnet_mask_to_cidr('255.255.255.0')}")
print(f"255.255.0.0 -> /{subnet_mask_to_cidr('255.255.0.0')}")
print(f"255.255.255.240 -> /{subnet_mask_to_cidr('255.255.255.240')}")

# IP in subnet examples
print("\nIP in Subnet Examples:")
print(f"Is 192.168.1.5 in 192.168.1.0/24? {is_ip_in_subnet('192.168.1.5', '192.168.1.0/24')}")
print(f"Is 10.0.0.5 in 192.168.1.0/24? {is_ip_in_subnet('10.0.0.5', '192.168.1.0/24')}")

The output of running this code would be:

CIDR to Subnet Mask Examples:
/24 -> 255.255.255.0
/16 -> 255.255.0.0
/28 -> 255.255.255.240

Subnet Mask to CIDR Examples:
255.255.255.0 -> /24
255.255.0.0 -> /16
255.255.255.240 -> /28

IP in Subnet Examples:
Is 192.168.1.5 in 192.168.1.0/24? True
Is 10.0.0.5 in 192.168.1.0/24? False

Error Handling Best Practices

Proper error handling is crucial in network automation scripts. Here's how to implement robust error handling with our functions:

def safely_check_subnet_membership(ip_address, subnet_cidr):
    """
    Safely check if an IP address belongs to a subnet with proper error handling
    
    Args:
        ip_address (str): IP address to check
        subnet_cidr (str): Subnet in CIDR notation
    
    Returns:
        tuple: (result, message) where result is a boolean or None if error occurred
    """
    try:
        result = is_ip_in_subnet(ip_address, subnet_cidr)
        return result, f"IP {ip_address} {'is' if result else 'is not'} in subnet {subnet_cidr}"
    except ValueError as e:
        return None, f"Error: {str(e)}"
    except Exception as e:
        return None, f"Unexpected error: {str(e)}"

Example usage with error handling:

# Test cases including error cases
test_cases = [
    ('192.168.1.5', '192.168.1.0/24'),
    ('10.0.0.5', '192.168.1.0/24'),
    ('invalid-ip', '192.168.1.0/24'),
    ('192.168.1.5', 'invalid-subnet')
]

for ip, subnet in test_cases:
    result, message = safely_check_subnet_membership(ip, subnet)
    print(f"Checking {ip} in {subnet}: {message}")

Output:

Checking 192.168.1.5 in 192.168.1.0/24: IP 192.168.1.5 is in subnet 192.168.1.0/24
Checking 10.0.0.5 in 192.168.1.0/24: IP 10.0.0.5 is not in subnet 192.168.1.0/24
Checking invalid-ip in 192.168.1.0/24: Error: Invalid input: 'invalid-ip' does not appear to be an IPv4 or IPv6 address
Checking 192.168.1.5 in invalid-subnet: Error: Invalid input: 'invalid-subnet' does not appear to be an IPv4 or IPv6 network

Practical Application: Network Segmentation Tool

Let's combine our functions to create a practical tool for network segmentation planning:

def plan_network_segmentation(base_network, num_subnets):
    """
    Plan network segmentation by dividing a base network into multiple subnets
    
    Args:
        base_network (str): Base network in CIDR notation (e.g., '10.0.0.0/24')
        num_subnets (int): Number of subnets to create (must be a power of 2)
    
    Returns:
        list: List of subnet CIDRs
    """
    import ipaddress
    import math
    
    # Validate input
    try:
        network = ipaddress.IPv4Network(base_network)
        
        # Calculate required subnet bits
        subnet_bits = math.ceil(math.log2(num_subnets))
        new_prefix_length = network.prefixlen + subnet_bits
        
        if new_prefix_length > 30:  # Leaving room for network and broadcast addresses
            raise ValueError(f"Cannot create {num_subnets} subnets from {base_network}. Resulting prefix would be too long.")
            
        # Generate subnets
        subnets = list(network.subnets(prefixlen_diff=subnet_bits))
        return [str(subnet) for subnet in subnets[:num_subnets]]
        
    except ValueError as e:
        raise ValueError(f"Invalid input: {e}")

Example usage:

try:
    # Divide a /24 network into 4 equal subnets
    subnets = plan_network_segmentation('192.168.1.0/24', 4)
    
    print("Network Segmentation Plan:")
    for i, subnet in enumerate(subnets, 1):
        cidr = int(subnet.split('/')[1])
        mask = cidr_to_subnet_mask(cidr)
        print(f"Subnet {i}: {subnet} (Mask: {mask})")
        
        # Show some usable IPs in this subnet
        network = ipaddress.IPv4Network(subnet)
        usable_ips = list(network.hosts())
        print(f"  Usable IPs: {usable_ips[0]} - {usable_ips[-1]} ({len(usable_ips)} addresses)")
        
except ValueError as e:
    print(f"Error: {e}")

Output:

Network Segmentation Plan:
Subnet 1: 192.168.1.0/26 (Mask: 255.255.255.192)
  Usable IPs: 192.168.1.1 - 192.168.1.62 (62 addresses)
Subnet 2: 192.168.1.64/26 (Mask: 255.255.255.192)
  Usable IPs: 192.168.1.65 - 192.168.1.126 (62 addresses)
Subnet 3: 192.168.1.128/26 (Mask: 255.255.255.192)
  Usable IPs: 192.168.1.129 - 192.168.1.190 (62 addresses)
Subnet 4: 192.168.1.192/26 (Mask: 255.255.255.192)
  Usable IPs: 192.168.1.193 - 192.168.1.254 (62 addresses)

Integration with Network Automation Libraries

These functions become even more powerful when integrated with network automation libraries. Here's an example using Netmiko to verify subnet configurations on network devices:

def verify_subnet_configuration(device_ip, username, password, subnet_cidr):
    """
    Verify if a subnet is properly configured on a Cisco device
    
    Args:
        device_ip (str): IP address of the network device
        username (str): SSH username
        password (str): SSH password
        subnet_cidr (str): Subnet in CIDR notation to verify
    
    Returns:
        bool: True if subnet is found in device configuration
    """
    from netmiko import ConnectHandler
    import re
    
    # Convert CIDR to subnet mask for comparison
    subnet_ip = subnet_cidr.split('/')[0]
    prefix_length = int(subnet_cidr.split('/')[1])
    subnet_mask = cidr_to_subnet_mask(prefix_length)
    
    # Device connection parameters
    device = {
        'device_type': 'cisco_ios',
        'host': device_ip,
        'username': username,
        'password': password,
    }
    
    try:
        # Connect to device and get IP interface information
        with ConnectHandler(**device) as conn:
            output = conn.send_command('show ip interface brief | include up')
            
            # Get detailed information for each up interface
            for line in output.splitlines():
                interface = line.split()[0]
                interface_config = conn.send_command(f'show running-config interface {interface}')
                
                # Look for IP address/subnet configuration
                ip_match = re.search(r'ip address (\d+\.\d+\.\d+\.\d+) (\d+\.\d+\.\d+\.\d+)', interface_config)
                if ip_match:
                    configured_ip = ip_match.group(1)
                    configured_mask = ip_match.group(2)
                    
                    # Check if this interface has our target subnet
                    if configured_mask == subnet_mask:
                        # Verify the network address matches
                        from ipaddress import IPv4Network, IPv4Address
                        configured_network = str(IPv4Network(f"{configured_ip}/{prefix_length}", strict=False).network_address)
                        target_network = str(IPv4Network(subnet_cidr, strict=False).network_address)
                        
                        if configured_network == target_network:
                            return True
            
            return False
            
    except Exception as e:
        print(f"Error connecting to device: {e}")
        return False

Testing Network Functions

Writing tests for your network functions ensures they work correctly across different scenarios. Here's how to test our CIDR conversion functions using pytest:

# test_network_functions.py
import pytest
from network_functions import cidr_to_subnet_mask, subnet_mask_to_cidr

def test_cidr_to_subnet_mask():
    # Test normal cases
    assert cidr_to_subnet_mask(24) == "255.255.255.0"
    assert cidr_to_subnet_mask(16) == "255.255.0.0"
    assert cidr_to_subnet_mask(8) == "255.0.0.0"
    assert cidr_to_subnet_mask(30) == "255.255.255.252"
    
    # Test edge cases
    assert cidr_to_subnet_mask(0) == "0.0.0.0"
    assert cidr_to_subnet_mask(32) == "255.255.255.255"
    
    # Test error cases
    with pytest.raises(ValueError):
        cidr_to_subnet_mask(-1)
    with pytest.raises(ValueError):
        cidr_to_subnet_mask(33)
    with pytest.raises(ValueError):
        cidr_to_subnet_mask("24")

def test_subnet_mask_to_cidr():
    # Test normal cases
    assert subnet_mask_to_cidr("255.255.255.0") == 24
    assert subnet_mask_to_cidr("255.255.0.0") == 16
    assert subnet_mask_to_cidr("255.0.0.0") == 8
    assert subnet_mask_to_cidr("255.255.255.252") == 30
    
    # Test edge cases
    assert subnet_mask_to_cidr("0.0.0.0") == 0
    assert subnet_mask_to_cidr("255.255.255.255") == 32
    
    # Test error cases
    with pytest.raises(ValueError):
        subnet_mask_to_cidr("256.255.255.0")  # Invalid octet
    with pytest.raises(ValueError):
        subnet_mask_to_cidr("255.255.255")    # Wrong format
    with pytest.raises(ValueError):
        subnet_mask_to_cidr("255.255.0.255")  # Non-contiguous mask

Performance Considerations

When working with large-scale network automation, performance becomes critical. Here's a comparison of different implementation approaches for our CIDR conversion function:

import time
import ipaddress

def benchmark_cidr_conversion(iterations=100000):
    """Compare performance of different CIDR conversion implementations"""
    
    # Our custom implementation
    def custom_cidr_to_mask(cidr):
        binary = ('1' * cidr) + ('0' * (32 - cidr))
        octets = [binary[i:i+8] for i in range(0, 32, 8)]
        decimal_octets = [int(octet, 2) for octet in octets]
        return '.'.join(str(octet) for octet in decimal_octets)
    
    # Using ipaddress module
    def ipaddress_cidr_to_mask(cidr):
        return str(ipaddress.IPv4Network(f"0.0.0.0/{cidr}").netmask)
    
    # Using bit manipulation
    def bitwise_cidr_to_mask(cidr):
        mask = ((1 << 32) - 1) ^ ((1 << (32 - cidr)) - 1)
        return f"{mask >> 24 & 0xFF}.{mask >> 16 & 0xFF}.{mask >> 8 & 0xFF}.{mask & 0xFF}"
    
    # Benchmark each implementation
    implementations = {
        "Custom": custom_cidr_to_mask,
        "ipaddress module": ipaddress_cidr_to_mask,
        "Bitwise": bitwise_cidr_to_mask
    }
    
    results = {}
    for name, func in implementations.items():
        start_time = time.time()
        for _ in range(iterations):
            func(24)
        elapsed = time.time() - start_time
        results[name] = elapsed
    
    return results

# Run benchmark and display results
results = benchmark_cidr_conversion()
print("Performance Comparison (lower is better):")
for name, elapsed in sorted(results.items(), key=lambda x: x[1]):
    print(f"{name}: {elapsed:.4f} seconds")

Typical output:

Performance Comparison (lower is better):
Bitwise: 0.0842 seconds
Custom: 0.3215 seconds
ipaddress module: 0.5731 seconds

Relevant RFCs and Standards

These functions implement concepts defined in several important networking standards:

  • RFC 1878: "Variable Length Subnet Table For IPv4"
  • RFC 1519: "Classless Inter-Domain Routing (CIDR): an Address Assignment and Aggregation Strategy"
  • RFC 4632: "Classless Inter-Domain Routing (CIDR): The Internet Address Assignment and Aggregation Plan"

Understanding these standards provides valuable context for why these conversions are necessary and how they relate to modern network design principles.

Conclusion

Python functions are powerful tools for network engineers, enabling automation of common tasks and improving operational efficiency. The examples in this article demonstrate how relatively simple functions can solve everyday networking challenges, from subnet calculations to configuration verification.

By creating reusable, well-tested functions, you can build a personal library of network automation tools that grow with your needs. As you develop more complex automation systems, these fundamental building blocks will serve as the foundation for more sophisticated solutions.

For your next network automation project, consider starting with a library of these essential functions - they'll save you time and reduce errors in your day-to-day network operations.