Source code for discovery_client.network.utils

"""
Network utility functions for netmask and network calculations.

Provides helpers for netmask-to-prefix conversion, computing network ranges,
and calculating broadcast addresses.
"""
import ipaddress
from typing import Union


[docs] def netmask_to_prefix(netmask: str) -> int: """ Convert netmask to CIDR prefix length. Converts a netmask in dotted decimal notation (e.g., "255.255.255.0") to a CIDR prefix length (e.g., 24). Args: netmask: Netmask as string in dotted decimal format (e.g., "255.255.255.0") Returns: CIDR prefix length as integer (0-32) Raises: ValueError: If netmask is invalid or non-contiguous Example: >>> netmask_to_prefix("255.255.255.0") 24 >>> netmask_to_prefix("255.0.0.0") 8 """ try: # Parse as IPv4Address to validate format mask_addr = ipaddress.IPv4Address(netmask) mask_int = int(mask_addr) # Validate it's a valid contiguous netmask # A valid netmask must be a sequence of 1s followed by 0s # We can check this by inverting and checking if (inverted + 1) is a power of 2 inverted = (~mask_int) & 0xFFFFFFFF if inverted == 0: # All 1s - /32 return 32 # Check if inverted + 1 is a power of 2 # For a valid netmask: inverted = 0...01...1, so inverted + 1 = 0...10...0 (power of 2) inverted_plus_one = inverted + 1 if (inverted_plus_one & (inverted_plus_one - 1)) != 0: raise ValueError(f"Non-contiguous netmask: {netmask}") # Count leading 1s by finding the position of the first 0 # The prefix length is 32 - number of trailing 0s in inverted # Which equals 32 - log2(inverted_plus_one) # Or we can count bits directly prefix = 0 for i in range(32): if mask_int & (1 << (31 - i)): prefix += 1 else: break return prefix except (ValueError, ipaddress.AddressValueError) as e: if "Non-contiguous" in str(e): raise raise ValueError(f"Invalid netmask format: {netmask}") from e
[docs] def prefix_to_netmask(prefix: int) -> str: """ Convert CIDR prefix length to netmask. Converts a CIDR prefix length (e.g., 24) to a netmask in dotted decimal notation (e.g., "255.255.255.0"). Args: prefix: CIDR prefix length (0-32) Returns: Netmask as string in dotted decimal format Raises: ValueError: If prefix is out of valid range (0-32) Example: >>> prefix_to_netmask(24) '255.255.255.0' >>> prefix_to_netmask(8) '255.0.0.0' """ if not (0 <= prefix <= 32): raise ValueError(f"Prefix length must be between 0 and 32, got {prefix}") # Calculate netmask: (2^prefix - 1) << (32 - prefix) # For prefix=24: (2^24 - 1) << 8 = 0xFFFFFF00 = 255.255.255.0 if prefix == 0: mask_int = 0 elif prefix == 32: mask_int = 0xFFFFFFFF else: mask_int = ((1 << prefix) - 1) << (32 - prefix) return str(ipaddress.IPv4Address(mask_int))
[docs] def network_from_ip_and_mask(ip: str, mask: Union[str, int]) -> ipaddress.IPv4Network: """ Create IPv4Network object from IP address and netmask. Args: ip: IPv4 address as string (e.g., "192.168.1.100") mask: Netmask as string (e.g., "255.255.255.0") or prefix length as int (e.g., 24) Returns: IPv4Network object representing the network Raises: ValueError: If IP or mask is invalid Example: >>> network = network_from_ip_and_mask("192.168.1.100", "255.255.255.0") >>> str(network) '192.168.1.0/24' >>> network = network_from_ip_and_mask("10.0.0.1", 8) >>> str(network) '10.0.0.0/8' """ try: # Normalize mask to prefix length if it's a string if isinstance(mask, str): # Check if it's already a prefix notation like "/24" if mask.startswith('/'): prefix = int(mask[1:]) else: # Convert netmask to prefix prefix = netmask_to_prefix(mask) elif isinstance(mask, int): prefix = mask else: raise TypeError(f"Mask must be str or int, got {type(mask).__name__}") # Validate prefix range if not (0 <= prefix <= 32): raise ValueError(f"Prefix length must be between 0 and 32, got {prefix}") # Create network object network = ipaddress.IPv4Network(f"{ip}/{prefix}", strict=False) return network except (ValueError, ipaddress.AddressValueError, ipaddress.NetmaskValueError) as e: raise ValueError(f"Invalid IP/mask combination: {ip}/{mask}") from e
[docs] def broadcast_from_ip_and_mask(ip: str, mask: Union[str, int]) -> str: """ Calculate broadcast address from IP address and netmask. Args: ip: IPv4 address as string (e.g., "192.168.1.100") mask: Netmask as string (e.g., "255.255.255.0") or prefix length as int (e.g., 24) Returns: Broadcast address as string Raises: ValueError: If IP or mask is invalid Example: >>> broadcast_from_ip_and_mask("192.168.1.100", "255.255.255.0") '192.168.1.255' >>> broadcast_from_ip_and_mask("10.0.0.1", 8) '10.255.255.255' """ network = network_from_ip_and_mask(ip, mask) return str(network.broadcast_address)