Actualización de la clase IP4Mask y Class_IP4 con nuevos métodos para conversiones, análisis de red y utilidades. Se añaden funcionalidades para manejar direcciones IP, incluyendo operaciones aritméticas y análisis de relaciones entre redes. Se mejora la documentación y se ajustan los métodos de autocompletado.

This commit is contained in:
Miguel 2025-06-02 20:23:37 +02:00
parent 036eeb4291
commit 03964d2ff5
1 changed files with 590 additions and 13 deletions

View File

@ -3,7 +3,7 @@ Clase híbrida para direcciones IPv4
"""
from class_base import ClassBase
from sympy_Base import SympyClassBase
from typing import Optional, Union
from typing import Optional, Union, List, Tuple
import re
import sympy
@ -99,26 +99,187 @@ class IP4Mask(ClassBase):
@staticmethod
def Helper(input_str):
if re.match(r"^\s*IP4Mask\\b", input_str, re.IGNORECASE):
return 'Máscara: IP4Mask[24], IP4Mask[255.255.255.0]. Funciones: get_prefix_int(), get_netmask_str(), hosts_count()'
if re.match(r"^\s*IP4Mask\b", input_str, re.IGNORECASE):
return 'Máscara: IP4Mask[24], IP4Mask[255.255.255.0]. Funciones: get_prefix_int(), get_mask_str(), hosts_count(), to_hex(), to_binary()'
return None
@staticmethod
def PopupFunctionList():
return [
("get_prefix_int", "Obtiene prefijo CIDR (ej: 24)"),
("get_netmask_str", "Obtiene máscara como string (ej: 255.255.255.0)"),
("get_mask_str", "Obtiene máscara como string (ej: 255.255.255.0)"),
("to_dotted", "Alias para get_mask_str()"),
("to_prefix", "Alias para get_prefix_int()"),
("hosts_count", "Número de hosts disponibles"),
("total_addresses", "Total de direcciones (incluyendo red/broadcast)"),
("to_decimal", "Convierte a entero decimal"),
("to_hex", "Convierte a hexadecimal"),
("to_binary", "Representación binaria"),
("to_wildcard", "Máscara wildcard (inversa)"),
("bit_representation", "Representación visual con bits"),
("octet_breakdown", "Desglose por octetos"),
("is_more_specific", "¿Es más específica que otra máscara?"),
("is_subnet_of", "¿Es subred de otra máscara?"),
("common_supernet", "Máscara común que contiene ambas"),
("next_smaller", "Siguiente máscara más pequeña"),
("next_larger", "Siguiente máscara más grande"),
("split", "Dividir en dos subredes"),
("is_valid", "¿Es una máscara válida?"),
("is_host_mask", "¿Es máscara de host (/32)?"),
("is_network_mask", "¿Es máscara de red (/0)?"),
("efficiency", "Eficiencia de uso de direcciones"),
("to_sympy", "Convierte a expresión SymPy para álgebra"),
]
def hosts_count(self):
"""Número de hosts disponibles"""
return 2**(32 - self._prefix) - 2 if self._prefix < 31 else 0
# ========== CONVERSIONES Y REPRESENTACIONES ==========
def to_decimal(self) -> int:
"""Convierte a entero decimal"""
return self._mask_int
def to_hex(self) -> str:
"""Convierte a hexadecimal"""
return f"0x{self._mask_int:08X}"
def to_binary(self) -> str:
"""Representación binaria de 32 bits"""
return f"{self._mask_int:032b}"
def to_dotted(self) -> str:
"""Alias para get_mask_str()"""
return self.get_mask_str()
def to_prefix(self) -> int:
"""Alias para get_prefix_int()"""
return self.get_prefix_int()
def to_wildcard(self) -> str:
"""Máscara wildcard (inversa)"""
wildcard_int = self._mask_int ^ 0xFFFFFFFF
return f"{(wildcard_int >> 24) & 0xFF}.{(wildcard_int >> 16) & 0xFF}.{(wildcard_int >> 8) & 0xFF}.{wildcard_int & 0xFF}"
def bit_representation(self) -> str:
"""Representación visual con bits"""
binary = self.to_binary()
return f"{binary[:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:32]}".replace('1', '').replace('0', '.')
def octet_breakdown(self) -> dict:
"""Desglose por octetos"""
return {
"oct1": (self._mask_int >> 24) & 0xFF,
"oct2": (self._mask_int >> 16) & 0xFF,
"oct3": (self._mask_int >> 8) & 0xFF,
"oct4": self._mask_int & 0xFF
}
# ========== CÁLCULOS DE RED ==========
def hosts_count(self) -> int:
"""Número de hosts disponibles (excluyendo red/broadcast)"""
if self._prefix >= 31:
return 0
return (2 ** (32 - self._prefix)) - 2
def total_addresses(self) -> int:
"""Total de direcciones (incluyendo red/broadcast)"""
return 2 ** (32 - self._prefix)
def subnet_count(self, base_mask: 'IP4Mask') -> int:
"""¿Cuántas subredes de este tamaño caben en la máscara base?"""
if not isinstance(base_mask, IP4Mask):
raise TypeError("base_mask must be IP4Mask instance")
if self._prefix < base_mask._prefix:
raise ValueError("Target mask must be more specific than base mask")
return 2 ** (self._prefix - base_mask._prefix)
def efficiency(self) -> float:
"""Eficiencia de uso de direcciones (0-1)"""
if self._prefix >= 31:
return 1.0 if self._prefix == 32 else 0.0
return self.hosts_count() / self.total_addresses()
# ========== COMPARACIONES Y ANÁLISIS ==========
def is_more_specific(self, other_mask: 'IP4Mask') -> bool:
"""¿Es más específica (mayor prefijo) que otra máscara?"""
if not isinstance(other_mask, IP4Mask):
raise TypeError("other_mask must be IP4Mask instance")
return self._prefix > other_mask._prefix
def is_subnet_of(self, other_mask: 'IP4Mask') -> bool:
"""¿Es subred (más específica) de otra máscara?"""
return self.is_more_specific(other_mask)
def common_supernet(self, other_mask: 'IP4Mask') -> 'IP4Mask':
"""Máscara común que contiene ambas"""
if not isinstance(other_mask, IP4Mask):
raise TypeError("other_mask must be IP4Mask instance")
common_prefix = min(self._prefix, other_mask._prefix)
return IP4Mask(common_prefix)
# ========== UTILIDADES DE PROGRAMADOR ==========
def is_valid(self) -> bool:
"""¿Es una máscara válida?"""
return 0 <= self._prefix <= 32
def is_host_mask(self) -> bool:
"""¿Es máscara de host (/32)?"""
return self._prefix == 32
def is_network_mask(self) -> bool:
"""¿Es máscara de red (/0)?"""
return self._prefix == 0
def next_smaller(self) -> 'IP4Mask':
"""Siguiente máscara más pequeña (más específica)"""
if self._prefix >= 32:
raise ValueError("Cannot make /32 more specific")
return IP4Mask(self._prefix + 1)
def next_larger(self) -> 'IP4Mask':
"""Siguiente máscara más grande (menos específica)"""
if self._prefix <= 0:
raise ValueError("Cannot make /0 less specific")
return IP4Mask(self._prefix - 1)
def split(self) -> Tuple['IP4Mask', 'IP4Mask']:
"""Divide en dos subredes del siguiente tamaño"""
if self._prefix >= 32:
raise ValueError("Cannot split /32 mask")
smaller_mask = self.next_smaller()
return (smaller_mask, smaller_mask)
def to_sympy(self):
"""Convierte a SymPy cuando se necesite álgebra"""
return sympy.sympify(self._prefix)
def hosts_expression(self):
"""Expresión SymPy para número de hosts"""
return 2**(32 - self._prefix)
# ========== MÉTODOS ESTÁTICOS ==========
@staticmethod
def optimal_for_hosts(host_count: int) -> 'IP4Mask':
"""¿Qué máscara para N hosts?"""
if host_count <= 0:
return IP4Mask(32)
if host_count == 1:
return IP4Mask(31)
# Encontrar el menor prefijo que acomode host_count + 2 (red y broadcast)
total_needed = host_count + 2
prefix = 32
while 2**(32 - prefix) < total_needed and prefix > 0:
prefix -= 1
return IP4Mask(prefix)
@staticmethod
def compare_efficiency(mask_list: List['IP4Mask']) -> List[Tuple['IP4Mask', float]]:
"""Eficiencia de uso de cada máscara"""
return [(mask, mask.efficiency()) for mask in mask_list]
class Class_IP4(SympyClassBase):
"""Clase híbrida para direcciones IPv4"""
@ -229,9 +390,8 @@ class Class_IP4(SympyClassBase):
@staticmethod
def Helper(input_str):
if re.match(r"^\s*IP4\\b", input_str, re.IGNORECASE):
return ('Ej: IP4[192.168.1.1/24], IP4[10.0.0.1;8], IP4[172.16.0.5;255.255.0.0]\\n'
'Funciones: NetworkAddress(), BroadcastAddress(), Nodes(), get_netmask_str(), get_prefix_length(), mask()')
if re.match(r"^\s*IP4\b", input_str, re.IGNORECASE):
return ('Ej: IP4[192.168.1.1/24], IP4[10.0.0.1;8]. Funciones: NetworkAddress(), BroadcastAddress(), add_hosts(), next_ip(), mask()')
return None
@staticmethod
@ -241,10 +401,42 @@ class Class_IP4(SympyClassBase):
("BroadcastAddress", "Dirección de broadcast"),
("Nodes", "Hosts disponibles"),
("mask", "Objeto máscara con sus métodos"),
("get_netmask_str", "Obtiene la máscara de red (ej: 255.255.255.0)"),
("get_prefix_length", "Obtiene la longitud del prefijo CIDR (ej: 24)"),
("add_hosts", "Sumar N hosts a la dirección"),
("subtract_hosts", "Restar N hosts a la dirección"),
("next_ip", "Siguiente dirección IP"),
("previous_ip", "Dirección IP anterior"),
("first_host", "Primera IP asignable"),
("last_host", "Última IP asignable"),
("is_network_address", "¿Es dirección de red?"),
("is_broadcast_address", "¿Es dirección de broadcast?"),
("is_host_address", "¿Es dirección de host válida?"),
("is_first_host", "¿Es la primera IP asignable?"),
("is_last_host", "¿Es la última IP asignable?"),
("to_integer", "Convierte a entero"),
("to_hex", "Convierte a hexadecimal"),
("to_binary", "Convierte a binario"),
("to_octets", "Convierte a lista de octetos"),
("bit_representation", "Representación visual de bits"),
("in_same_network", "¿Está en la misma red?"),
("is_subnet_of", "¿Está en esa subred?"),
("distance_to", "Distancia en direcciones"),
("hamming_distance", "Distancia de Hamming"),
("parent_network", "Red padre"),
("child_networks", "Subredes hijas"),
("sibling_networks", "Redes hermanas"),
("is_private", "¿Es IP privada?"),
("is_loopback", "¿Es loopback?"),
("is_multicast", "¿Es multicast?"),
("is_linklocal", "¿Es link-local?"),
("ip_class", "Clase de IP (A/B/C/D/E)"),
("reverse_dns_zone", "Zona DNS reversa"),
("ptr_record", "Registro PTR"),
("get_netmask_str", "Máscara como string"),
("get_prefix_length", "Longitud del prefijo CIDR"),
]
# ========== MÉTODOS BÁSICOS EXISTENTES ==========
def get_netmask_str(self) -> Optional[str]:
return self._mask_obj.get_mask_str() if self._mask_obj else None
@ -298,4 +490,389 @@ class Class_IP4(SympyClassBase):
"""Retorna objeto máscara para autocompletado"""
if not self._mask_obj:
raise ValueError("No mask defined for this IP4 object.")
return self._mask_obj
return self._mask_obj
# ========== ARITMÉTICA DE DIRECCIONES ==========
def add_hosts(self, n: int) -> 'Class_IP4':
"""Suma N hosts a la dirección"""
new_ip_int = (self._ip_int + n) & 0xFFFFFFFF
new_parts = [
(new_ip_int >> 24) & 0xFF,
(new_ip_int >> 16) & 0xFF,
(new_ip_int >> 8) & 0xFF,
new_ip_int & 0xFF
]
new_ip_str = '.'.join(str(x) for x in new_parts)
return Class_IP4(new_ip_str, self._mask_obj)
def subtract_hosts(self, n: int) -> 'Class_IP4':
"""Resta N hosts a la dirección"""
return self.add_hosts(-n)
def next_ip(self) -> 'Class_IP4':
"""Siguiente dirección IP"""
return self.add_hosts(1)
def previous_ip(self) -> 'Class_IP4':
"""Dirección IP anterior"""
return self.add_hosts(-1)
def increment_octet(self, octet_num: int, value: int) -> 'Class_IP4':
"""Incrementa un octeto específico (1-4)"""
if not 1 <= octet_num <= 4:
raise ValueError("octet_num must be between 1 and 4")
octets = self.to_octets()
octets[octet_num - 1] = (octets[octet_num - 1] + value) % 256
new_ip_str = '.'.join(str(x) for x in octets)
return Class_IP4(new_ip_str, self._mask_obj)
def next_network(self) -> 'Class_IP4':
"""Próxima red del mismo tamaño"""
if not self._mask_obj:
raise ValueError("No mask defined for network calculation")
network = self.NetworkAddress()
network_size = 2 ** (32 - self._mask_obj.get_prefix_int())
next_net = network.add_hosts(network_size)
return next_net.NetworkAddress()
def previous_network(self) -> 'Class_IP4':
"""Red anterior del mismo tamaño"""
if not self._mask_obj:
raise ValueError("No mask defined for network calculation")
network = self.NetworkAddress()
network_size = 2 ** (32 - self._mask_obj.get_prefix_int())
prev_net = network.subtract_hosts(network_size)
return prev_net.NetworkAddress()
# ========== ANÁLISIS DE RED ==========
def is_network_address(self) -> bool:
"""¿Es la dirección de red?"""
if not self._mask_obj:
return False
return self._ip_int == self.NetworkAddress()._ip_int
def is_broadcast_address(self) -> bool:
"""¿Es la dirección de broadcast?"""
if not self._mask_obj:
return False
return self._ip_int == self.BroadcastAddress()._ip_int
def is_host_address(self) -> bool:
"""¿Es una dirección de host válida?"""
if not self._mask_obj:
return True # Sin máscara, asumimos que es host
return not (self.is_network_address() or self.is_broadcast_address())
def is_first_host(self) -> bool:
"""¿Es la primera IP asignable?"""
if not self._mask_obj:
return False
return self._ip_int == self.first_host()._ip_int
def is_last_host(self) -> bool:
"""¿Es la última IP asignable?"""
if not self._mask_obj:
return False
return self._ip_int == self.last_host()._ip_int
def first_host(self) -> 'Class_IP4':
"""Primera IP asignable"""
if not self._mask_obj:
raise ValueError("No mask defined")
if self._mask_obj.get_prefix_int() >= 31:
return self # En /31 y /32, la IP es el host
return self.NetworkAddress().add_hosts(1)
def last_host(self) -> 'Class_IP4':
"""Última IP asignable"""
if not self._mask_obj:
raise ValueError("No mask defined")
if self._mask_obj.get_prefix_int() >= 31:
return self # En /31 y /32, la IP es el host
return self.BroadcastAddress().subtract_hosts(1)
def network_range(self) -> Tuple['Class_IP4', 'Class_IP4']:
"""Rango de red (primera, última) como tupla"""
return (self.first_host(), self.last_host())
def host_position(self) -> int:
"""¿En qué posición está dentro de la red?"""
if not self._mask_obj:
raise ValueError("No mask defined")
network = self.NetworkAddress()
return self._ip_int - network._ip_int
# ========== CONVERSIONES AVANZADAS ==========
def to_integer(self) -> int:
"""Convierte a entero"""
return self._ip_int
def to_hex(self) -> str:
"""Convierte a hexadecimal"""
return f"0x{self._ip_int:08X}"
def to_binary(self) -> str:
"""Convierte a binario"""
return f"{self._ip_int:032b}"
def to_octets(self) -> List[int]:
"""Convierte a lista de octetos"""
return [
(self._ip_int >> 24) & 0xFF,
(self._ip_int >> 16) & 0xFF,
(self._ip_int >> 8) & 0xFF,
self._ip_int & 0xFF
]
def bit_representation(self) -> str:
"""Representación visual de bits"""
binary = self.to_binary()
return f"{binary[:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:32]}".replace('1', '').replace('0', '.')
def hamming_distance(self, other_ip: 'Class_IP4') -> int:
"""Distancia de Hamming (diferencia en bits)"""
if not isinstance(other_ip, Class_IP4):
raise TypeError("other_ip must be Class_IP4 instance")
xor_result = self._ip_int ^ other_ip._ip_int
return bin(xor_result).count('1')
# ========== ANÁLISIS DE RELACIONES ==========
def in_same_network(self, other_ip: 'Class_IP4') -> bool:
"""¿Están en la misma red?"""
if not isinstance(other_ip, Class_IP4):
raise TypeError("other_ip must be Class_IP4 instance")
if not self._mask_obj:
raise ValueError("No mask defined for network comparison")
mask_int = IP4Mask._prefix_to_mask_int(self._mask_obj.get_prefix_int())
return (self._ip_int & mask_int) == (other_ip._ip_int & mask_int)
def is_subnet_of(self, network_ip: 'Class_IP4') -> bool:
"""¿Esta IP está en esa red?"""
if not isinstance(network_ip, Class_IP4):
raise TypeError("network_ip must be Class_IP4 instance")
if not network_ip._mask_obj:
raise ValueError("network_ip must have a mask defined")
mask_int = IP4Mask._prefix_to_mask_int(network_ip._mask_obj.get_prefix_int())
network_addr = network_ip._ip_int & mask_int
this_network = self._ip_int & mask_int
return network_addr == this_network
def distance_to(self, other_ip: 'Class_IP4') -> int:
"""Distancia en direcciones"""
if not isinstance(other_ip, Class_IP4):
raise TypeError("other_ip must be Class_IP4 instance")
return abs(self._ip_int - other_ip._ip_int)
def parent_network(self, larger_mask: IP4Mask) -> 'Class_IP4':
"""Red padre con máscara mayor"""
if not isinstance(larger_mask, IP4Mask):
raise TypeError("larger_mask must be IP4Mask instance")
if self._mask_obj and larger_mask.get_prefix_int() >= self._mask_obj.get_prefix_int():
raise ValueError("larger_mask must be less specific")
mask_int = IP4Mask._prefix_to_mask_int(larger_mask.get_prefix_int())
parent_int = self._ip_int & mask_int
parts = [
(parent_int >> 24) & 0xFF,
(parent_int >> 16) & 0xFF,
(parent_int >> 8) & 0xFF,
parent_int & 0xFF
]
parent_str = '.'.join(str(x) for x in parts)
return Class_IP4(parent_str, larger_mask)
def child_networks(self, smaller_mask: IP4Mask) -> List['Class_IP4']:
"""Subredes hijas"""
if not isinstance(smaller_mask, IP4Mask):
raise TypeError("smaller_mask must be IP4Mask instance")
if not self._mask_obj:
raise ValueError("IP must have a mask defined")
if smaller_mask.get_prefix_int() <= self._mask_obj.get_prefix_int():
raise ValueError("smaller_mask must be more specific")
network = self.NetworkAddress()
subnet_count = 2 ** (smaller_mask.get_prefix_int() - self._mask_obj.get_prefix_int())
subnet_size = 2 ** (32 - smaller_mask.get_prefix_int())
subnets = []
current_ip = network._ip_int
for i in range(subnet_count):
parts = [
(current_ip >> 24) & 0xFF,
(current_ip >> 16) & 0xFF,
(current_ip >> 8) & 0xFF,
current_ip & 0xFF
]
subnet_str = '.'.join(str(x) for x in parts)
subnets.append(Class_IP4(subnet_str, smaller_mask))
current_ip += subnet_size
return subnets
def sibling_networks(self) -> List['Class_IP4']:
"""Redes hermanas del mismo tamaño"""
if not self._mask_obj:
raise ValueError("IP must have a mask defined")
if self._mask_obj.get_prefix_int() == 0:
return [self] # /0 no tiene hermanas
parent_mask = IP4Mask(self._mask_obj.get_prefix_int() - 1)
parent = self.parent_network(parent_mask)
return parent.child_networks(self._mask_obj)
# ========== CLASIFICACIÓN DE IPS ==========
def is_private(self) -> bool:
"""¿Es IP privada? (RFC 1918)"""
# 10.0.0.0/8
if (self._ip_int & 0xFF000000) == 0x0A000000:
return True
# 172.16.0.0/12
if (self._ip_int & 0xFFF00000) == 0xAC100000:
return True
# 192.168.0.0/16
if (self._ip_int & 0xFFFF0000) == 0xC0A80000:
return True
return False
def is_loopback(self) -> bool:
"""¿Es loopback? (127.0.0.0/8)"""
return (self._ip_int & 0xFF000000) == 0x7F000000
def is_multicast(self) -> bool:
"""¿Es multicast? (224.0.0.0/4)"""
return (self._ip_int & 0xF0000000) == 0xE0000000
def is_linklocal(self) -> bool:
"""¿Es link-local? (169.254.0.0/16)"""
return (self._ip_int & 0xFFFF0000) == 0xA9FE0000
def ip_class(self) -> str:
"""Clase de IP (A/B/C/D/E) - legacy pero útil"""
first_octet = (self._ip_int >> 24) & 0xFF
if first_octet < 128:
return "A"
elif first_octet < 192:
return "B"
elif first_octet < 224:
return "C"
elif first_octet < 240:
return "D" # Multicast
else:
return "E" # Experimental
# ========== UTILIDADES DNS ==========
def reverse_dns_zone(self) -> str:
"""Zona DNS reversa"""
if not self._mask_obj:
raise ValueError("No mask defined for reverse DNS zone")
prefix = self._mask_obj.get_prefix_int()
octets = self.to_octets()
if prefix >= 24:
return f"{octets[2]}.{octets[1]}.{octets[0]}.in-addr.arpa"
elif prefix >= 16:
return f"{octets[1]}.{octets[0]}.in-addr.arpa"
elif prefix >= 8:
return f"{octets[0]}.in-addr.arpa"
else:
return "in-addr.arpa"
def ptr_record(self) -> str:
"""Registro PTR"""
octets = self.to_octets()
return f"{octets[3]}.{octets[2]}.{octets[1]}.{octets[0]}.in-addr.arpa"
def generate_host_list(self, count: int) -> List['Class_IP4']:
"""Genera lista de IPs consecutivas"""
if count <= 0:
return []
result = []
current = self
for i in range(count):
result.append(current)
current = current.next_ip()
return result
# ========== MÉTODOS ESTÁTICOS ==========
@staticmethod
def find_common_network(ip_list: List['Class_IP4']) -> 'Class_IP4':
"""Menor red que contiene todas las IPs"""
if not ip_list:
raise ValueError("ip_list cannot be empty")
if len(ip_list) == 1:
return ip_list[0]
# Encontrar el prefijo común más largo
min_ip = min(ip._ip_int for ip in ip_list)
max_ip = max(ip._ip_int for ip in ip_list)
# XOR para encontrar bits diferentes
xor_result = min_ip ^ max_ip
# Contar bits comunes desde la izquierda
common_bits = 0
for i in range(32):
if (xor_result >> (31 - i)) & 1:
break
common_bits += 1
# Crear la red común
mask = IP4Mask(common_bits)
network_int = min_ip & IP4Mask._prefix_to_mask_int(common_bits)
parts = [
(network_int >> 24) & 0xFF,
(network_int >> 16) & 0xFF,
(network_int >> 8) & 0xFF,
network_int & 0xFF
]
network_str = '.'.join(str(x) for x in parts)
return Class_IP4(network_str, mask)
@staticmethod
def detect_overlaps(network_list: List['Class_IP4']) -> List[Tuple['Class_IP4', 'Class_IP4']]:
"""Encontrar redes solapadas"""
overlaps = []
for i, net1 in enumerate(network_list):
for net2 in network_list[i+1:]:
if net1.is_subnet_of(net2) or net2.is_subnet_of(net1):
overlaps.append((net1, net2))
return overlaps
@staticmethod
def analyze_distribution(ip_list: List['Class_IP4']) -> dict:
"""Análisis estadístico de distribución"""
if not ip_list:
return {}
classes = {"A": 0, "B": 0, "C": 0, "D": 0, "E": 0}
private_count = 0
for ip in ip_list:
classes[ip.ip_class()] += 1
if ip.is_private():
private_count += 1
return {
"total": len(ip_list),
"classes": classes,
"private_count": private_count,
"public_count": len(ip_list) - private_count,
"common_network": Class_IP4.find_common_network(ip_list)
}