Spaces:
Sleeping
Sleeping
| """ | |
| Schema Validation for Network Models | |
| Uses Pydantic for type checking and data validation | |
| """ | |
| from typing import List, Optional, Dict, Any, Literal | |
| from pydantic import BaseModel, Field, validator, IPvAnyAddress, IPvAnyNetwork | |
| from ipaddress import ip_network, ip_address | |
| import re | |
| class VLANModel(BaseModel): | |
| """VLAN configuration schema""" | |
| id: int = Field(..., ge=1, le=4094, description="VLAN ID (1-4094)") | |
| name: str = Field(..., min_length=1, max_length=32, description="VLAN name") | |
| purpose: Optional[str] = Field(None, description="VLAN purpose/description") | |
| subnet: Optional[str] = Field(None, description="Associated subnet in CIDR notation") | |
| def validate_vlan_name(cls, v): | |
| """Ensure VLAN name follows naming conventions""" | |
| # No spaces, special characters | |
| if not re.match(r'^[a-zA-Z0-9_-]+$', v): | |
| raise ValueError("VLAN name must contain only letters, numbers, underscore, or hyphen") | |
| return v | |
| def validate_subnet_format(cls, v): | |
| """Validate CIDR notation""" | |
| if v: | |
| try: | |
| ip_network(v, strict=False) | |
| except ValueError as e: | |
| raise ValueError(f"Invalid subnet format: {e}") | |
| return v | |
| class Config: | |
| schema_extra = { | |
| "example": { | |
| "id": 10, | |
| "name": "Management", | |
| "purpose": "Network management and monitoring", | |
| "subnet": "10.0.10.0/24" | |
| } | |
| } | |
| class SubnetModel(BaseModel): | |
| """IP Subnet schema""" | |
| network: str = Field(..., description="Network address in CIDR notation") | |
| gateway: str = Field(..., description="Default gateway IP address") | |
| vlan: Optional[int] = Field(None, ge=1, le=4094, description="Associated VLAN ID") | |
| purpose: Optional[str] = Field(None, description="Subnet purpose") | |
| dhcp_enabled: bool = Field(False, description="DHCP server enabled") | |
| dhcp_range_start: Optional[str] = Field(None, description="DHCP pool start IP") | |
| dhcp_range_end: Optional[str] = Field(None, description="DHCP pool end IP") | |
| def validate_network(cls, v): | |
| """Ensure valid CIDR notation""" | |
| try: | |
| net = ip_network(v, strict=False) | |
| # Ensure it's a private network (RFC1918) unless explicitly public | |
| # if not net.is_private: | |
| # raise ValueError("Use RFC1918 private addressing (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)") | |
| return str(net) | |
| except ValueError as e: | |
| raise ValueError(f"Invalid network: {e}") | |
| def validate_gateway(cls, v, values): | |
| """Ensure gateway is within the network""" | |
| try: | |
| gw = ip_address(v) | |
| if 'network' in values: | |
| net = ip_network(values['network'], strict=False) | |
| if gw not in net: | |
| raise ValueError(f"Gateway {v} not in network {values['network']}") | |
| except ValueError as e: | |
| raise ValueError(f"Invalid gateway: {e}") | |
| return v | |
| def validate_dhcp_range(cls, v, values): | |
| """Ensure DHCP range is valid""" | |
| if v and 'dhcp_range_start' in values and values['dhcp_range_start']: | |
| start = ip_address(values['dhcp_range_start']) | |
| end = ip_address(v) | |
| if end <= start: | |
| raise ValueError("DHCP range end must be greater than start") | |
| # Ensure both are in the network | |
| if 'network' in values: | |
| net = ip_network(values['network'], strict=False) | |
| if start not in net or end not in net: | |
| raise ValueError("DHCP range must be within subnet") | |
| return v | |
| class Config: | |
| schema_extra = { | |
| "example": { | |
| "network": "10.0.10.0/24", | |
| "gateway": "10.0.10.1", | |
| "vlan": 10, | |
| "purpose": "Management network", | |
| "dhcp_enabled": True, | |
| "dhcp_range_start": "10.0.10.100", | |
| "dhcp_range_end": "10.0.10.200" | |
| } | |
| } | |
| class InterfaceModel(BaseModel): | |
| """Network interface schema""" | |
| name: str = Field(..., description="Interface name (e.g., GigabitEthernet1/0/1)") | |
| type: Literal["ethernet", "management", "loopback", "vlan", "port-channel"] = Field(..., description="Interface type") | |
| speed: Optional[str] = Field(None, description="Interface speed (e.g., 1000, 10G)") | |
| mode: Optional[Literal["access", "trunk"]] = Field(None, description="Switchport mode") | |
| vlan: Optional[int] = Field(None, description="Access VLAN or native VLAN for trunk") | |
| allowed_vlans: Optional[List[int]] = Field(None, description="Allowed VLANs for trunk ports") | |
| ip_address: Optional[str] = Field(None, description="IP address if L3 interface") | |
| description: Optional[str] = Field(None, max_length=240, description="Interface description") | |
| enabled: bool = Field(True, description="Interface administratively up") | |
| def validate_allowed_vlans(cls, v): | |
| """Ensure VLAN IDs are valid""" | |
| if v: | |
| for vlan_id in v: | |
| if vlan_id < 1 or vlan_id > 4094: | |
| raise ValueError(f"Invalid VLAN ID {vlan_id} (must be 1-4094)") | |
| return v | |
| class Config: | |
| schema_extra = { | |
| "example": { | |
| "name": "GigabitEthernet1/0/1", | |
| "type": "ethernet", | |
| "speed": "1000", | |
| "mode": "trunk", | |
| "vlan": 1, | |
| "allowed_vlans": [10, 20, 30], | |
| "description": "Uplink to core switch", | |
| "enabled": True | |
| } | |
| } | |
| class DeviceModel(BaseModel): | |
| """Network device schema""" | |
| name: str = Field(..., min_length=1, max_length=64, description="Device hostname") | |
| role: Literal["core", "distribution", "access", "edge", "firewall", "router", "wireless"] = Field(..., description="Device role in network") | |
| model: str = Field(..., description="Hardware model") | |
| vendor: Literal["cisco", "juniper", "arista", "hp", "dell", "ubiquiti", "mikrotik", "other"] = Field(..., description="Vendor") | |
| mgmt_ip: str = Field(..., description="Management IP address") | |
| location: str = Field(..., description="Physical location") | |
| interfaces: List[InterfaceModel] = Field(default_factory=list, description="Network interfaces") | |
| os_version: Optional[str] = Field(None, description="OS version") | |
| serial_number: Optional[str] = Field(None, description="Device serial number") | |
| def validate_hostname(cls, v): | |
| """Ensure hostname follows RFC1123""" | |
| if not re.match(r'^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$', v): | |
| raise ValueError("Invalid hostname format (RFC1123)") | |
| return v | |
| def validate_mgmt_ip(cls, v): | |
| """Ensure valid IP address""" | |
| try: | |
| ip_address(v) | |
| except ValueError as e: | |
| raise ValueError(f"Invalid management IP: {e}") | |
| return v | |
| class Config: | |
| schema_extra = { | |
| "example": { | |
| "name": "core-sw-01", | |
| "role": "core", | |
| "model": "Catalyst 9300", | |
| "vendor": "cisco", | |
| "mgmt_ip": "10.0.10.10", | |
| "location": "Main Office - IDF1", | |
| "os_version": "17.9.4", | |
| "interfaces": [] | |
| } | |
| } | |
| class RoutingModel(BaseModel): | |
| """Routing protocol configuration""" | |
| protocol: Literal["static", "ospf", "bgp", "eigrp", "rip", "is-is"] = Field(..., description="Routing protocol") | |
| autonomous_system: Optional[int] = Field(None, ge=1, le=4294967295, description="AS number for BGP/EIGRP") | |
| process_id: Optional[int] = Field(None, ge=1, le=65535, description="Process ID for OSPF/EIGRP") | |
| router_id: Optional[str] = Field(None, description="Router ID") | |
| areas: Optional[List[str]] = Field(None, description="OSPF areas or IS-IS levels") | |
| networks: Optional[List[str]] = Field(None, description="Networks to advertise") | |
| neighbors: Optional[List[str]] = Field(None, description="BGP neighbors or static routes") | |
| def validate_router_id(cls, v): | |
| """Ensure router ID is valid IP format""" | |
| if v: | |
| try: | |
| ip_address(v) | |
| except ValueError as e: | |
| raise ValueError(f"Invalid router ID format: {e}") | |
| return v | |
| class Config: | |
| schema_extra = { | |
| "example": { | |
| "protocol": "ospf", | |
| "process_id": 1, | |
| "router_id": "10.0.0.1", | |
| "areas": ["0"], | |
| "networks": ["10.0.0.0/8"] | |
| } | |
| } | |
| class NetworkModelSchema(BaseModel): | |
| """Complete network data model with validation""" | |
| name: str = Field(..., min_length=1, max_length=64, description="Network name") | |
| version: str = Field(..., pattern=r'^\d+\.\d+\.\d+$', description="Schema version (semantic versioning)") | |
| description: Optional[str] = Field(None, description="Network description") | |
| business_requirements: Optional[List[str]] = Field(default_factory=list, description="Business requirements") | |
| constraints: Optional[List[str]] = Field(default_factory=list, description="Design constraints") | |
| intent: Optional[Dict[str, Any]] = Field(None, description="Original network intent") | |
| devices: List[DeviceModel] = Field(default_factory=list, description="Network devices") | |
| vlans: List[VLANModel] = Field(default_factory=list, description="VLANs") | |
| subnets: List[SubnetModel] = Field(default_factory=list, description="IP subnets") | |
| routing: Optional[Dict[str, Any]] = Field(None, description="Routing configuration") | |
| services: List[str] = Field(default_factory=lambda: ["DHCP", "DNS", "NTP"], description="Network services") | |
| def check_unique_vlan_ids(cls, v): | |
| """Ensure no duplicate VLAN IDs""" | |
| vlan_ids = [vlan.id for vlan in v] | |
| if len(vlan_ids) != len(set(vlan_ids)): | |
| duplicates = [vid for vid in vlan_ids if vlan_ids.count(vid) > 1] | |
| raise ValueError(f"Duplicate VLAN IDs found: {duplicates}") | |
| return v | |
| def check_unique_device_names(cls, v): | |
| """Ensure no duplicate device names""" | |
| device_names = [d.name for d in v] | |
| if len(device_names) != len(set(device_names)): | |
| duplicates = [name for name in device_names if device_names.count(name) > 1] | |
| raise ValueError(f"Duplicate device names found: {duplicates}") | |
| return v | |
| def check_unique_mgmt_ips(cls, v): | |
| """Ensure no duplicate management IPs""" | |
| mgmt_ips = [d.mgmt_ip for d in v] | |
| if len(mgmt_ips) != len(set(mgmt_ips)): | |
| duplicates = [ip for ip in mgmt_ips if mgmt_ips.count(ip) > 1] | |
| raise ValueError(f"Duplicate management IPs found: {duplicates}") | |
| return v | |
| def check_subnet_vlan_references(cls, v, values): | |
| """Ensure referenced VLANs exist""" | |
| if 'vlans' in values: | |
| valid_vlan_ids = {vlan.id for vlan in values['vlans']} | |
| for subnet in v: | |
| if subnet.vlan and subnet.vlan not in valid_vlan_ids: | |
| raise ValueError(f"Subnet {subnet.network} references non-existent VLAN {subnet.vlan}") | |
| return v | |
| class Config: | |
| schema_extra = { | |
| "example": { | |
| "name": "corporate-network", | |
| "version": "1.0.0", | |
| "description": "Corporate office network", | |
| "business_requirements": ["High availability", "Secure guest access"], | |
| "constraints": ["Budget under $10K", "Cisco preferred"], | |
| "devices": [], | |
| "vlans": [], | |
| "subnets": [], | |
| "routing": None, | |
| "services": ["DHCP", "DNS", "NTP"] | |
| } | |
| } | |
| def validate_network_model(data: Dict[str, Any]) -> NetworkModelSchema: | |
| """ | |
| Validate network model data against schema | |
| Raises ValidationError if invalid | |
| """ | |
| return NetworkModelSchema(**data) | |
| def get_validation_errors(data: Dict[str, Any]) -> List[str]: | |
| """ | |
| Get human-readable validation errors | |
| Returns empty list if valid | |
| """ | |
| try: | |
| validate_network_model(data) | |
| return [] | |
| except Exception as e: | |
| # Parse pydantic validation errors | |
| errors = [] | |
| if hasattr(e, 'errors'): | |
| for error in e.errors(): | |
| loc = ' -> '.join(str(l) for l in error['loc']) | |
| msg = error['msg'] | |
| errors.append(f"{loc}: {msg}") | |
| else: | |
| errors.append(str(e)) | |
| return errors | |