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:
- Encapsulate common operations into reusable code
- Reduce errors through standardized processes
- Improve readability and maintainability of automation scripts
- 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.