Refactor BaseDevice and Interfaces models for improved validation and structure
- Introduced `_declared_sections` in `BaseDevice` to track declared template groups. - Enhanced `_validate_contract` method to conditionally validate VLANs based on declared sections. - Updated docstrings in `BaseDevice` and `Interfaces` models for clarity on expected structures. - Refactored `Interfaces` and `Vlans` models to improve field definitions and aliases. - Commented out unused `vlans` method in `Keenetic` model for future reference.
This commit is contained in:
@@ -12,7 +12,9 @@ class BaseDevice(ABC):
|
|||||||
|
|
||||||
def __init__(self, config: str):
|
def __init__(self, config: str):
|
||||||
self.config: str = config
|
self.config: str = config
|
||||||
|
|
||||||
self._loaded_template = self._load_template()
|
self._loaded_template = self._load_template()
|
||||||
|
self._declared_sections = None
|
||||||
self._validate_template_groups()
|
self._validate_template_groups()
|
||||||
self.raw: dict = self._run_ttp()
|
self.raw: dict = self._run_ttp()
|
||||||
|
|
||||||
@@ -20,15 +22,17 @@ class BaseDevice(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def template(self) -> str:
|
def template(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns:
|
Expected structure:
|
||||||
Название файла с парсером ttp
|
Название файла с парсером ttp
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def vlans(self) -> list[dict]:
|
def vlans(self) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Parse VLAN configuration from self.raw['vlans'].
|
Parse VLAN configuration from self.raw['vlans'].
|
||||||
|
|
||||||
Expected raw structure:
|
Expected structure:
|
||||||
[{"id": 10, "description": "MGMT"}, {"id": 15, "name": "SSH"}, ...]
|
[{"id": 10, "description": "MGMT"}, {"id": 15, "name": "SSH"}, ...]
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -64,16 +68,20 @@ class BaseDevice(ABC):
|
|||||||
"""
|
"""
|
||||||
return self.raw.get("system", None)
|
return self.raw.get("system", None)
|
||||||
|
|
||||||
def _validate_contract(self):
|
def _validate_contract(self) -> dict:
|
||||||
optional_vlans = self.vlans()
|
system_data = self.system()
|
||||||
if optional_vlans:
|
interfaces_data = self.interfaces() or []
|
||||||
optional_vlans = [Vlans(**item) for item in optional_vlans]
|
result = {
|
||||||
return {
|
"system": System(**system_data),
|
||||||
"system": System(**self.system()),
|
"interfaces": [Interfaces(**item) for item in interfaces_data],
|
||||||
"interfaces": [Interfaces(**items) for items in self.interfaces()],
|
"vlans": [],
|
||||||
"vlans": optional_vlans,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if "vlans" in self._declared_sections:
|
||||||
|
vlans_data = self.vlans() or []
|
||||||
|
result["vlans"] = [Vlans(**item) for item in vlans_data]
|
||||||
|
return result
|
||||||
|
|
||||||
def _load_template(self):
|
def _load_template(self):
|
||||||
"""Подгрузка темплейтов из папки models/templates"""
|
"""Подгрузка темплейтов из папки models/templates"""
|
||||||
path = Path(__file__).parent / "models" / "templates" / self.template
|
path = Path(__file__).parent / "models" / "templates" / self.template
|
||||||
@@ -82,20 +90,20 @@ class BaseDevice(ABC):
|
|||||||
return path.read_text(encoding="utf-8")
|
return path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
def _validate_template_groups(self) -> None:
|
def _validate_template_groups(self) -> None:
|
||||||
"""Проверка что TTP темлпейт имеет декларированные группы для всех требуемых и опциональных секций"""
|
"""Проверяем только обязательные группы в template."""
|
||||||
try:
|
try:
|
||||||
root = ET.fromstring(self._loaded_template)
|
root = ET.fromstring(self._loaded_template)
|
||||||
except ET.ParseError:
|
except ET.ParseError:
|
||||||
root = ET.fromstring(f"<template>{self._loaded_template}</template>")
|
root = ET.fromstring(f"<template>{self._loaded_template}</template>")
|
||||||
|
|
||||||
declared = {g.get("name") for g in root.iter("group") if g.get("name")}
|
declared = {g.get("name") for g in root.iter("group") if g.get("name")}
|
||||||
expected = self._REQUIRED_SECTIONS | self._OPTIONAL_SECTIONS
|
self._declared_sections = declared
|
||||||
missing = expected - declared
|
|
||||||
|
|
||||||
if missing:
|
missing_required = self._REQUIRED_SECTIONS - declared
|
||||||
|
if missing_required:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"{self.__class__.__name__}: template '{self.template}' "
|
f"{self.__class__.__name__}: template '{self.template}' "
|
||||||
f"missing group declarations: {sorted(missing)}. "
|
f"missing required groups: {sorted(missing_required)}. "
|
||||||
f"Declared groups: {sorted(declared)}"
|
f"Declared groups: {sorted(declared)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,8 +116,8 @@ class BaseDevice(ABC):
|
|||||||
if missing:
|
if missing:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"{self.__class__.__name__}: TTP template '{self.template}' "
|
f"{self.__class__.__name__}: TTP template '{self.template}' "
|
||||||
f"did not produce required sections: {sorted(missing)}. "
|
f"did not produce required groups: {sorted(missing)}. "
|
||||||
f"Got: {(raw.keys())}"
|
f"Return only: {(raw.keys())}"
|
||||||
)
|
)
|
||||||
return raw
|
return raw
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,41 @@ from ipaddress import IPv4Address
|
|||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
class Interfaces(BaseModel):
|
class Base(BaseModel):
|
||||||
name: str
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
ip_address: IPv4Address | None = None
|
|
||||||
mask: int | None = None
|
|
||||||
description: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class System(BaseModel):
|
class System(BaseModel):
|
||||||
|
"""
|
||||||
|
Requred
|
||||||
|
"""
|
||||||
|
|
||||||
model: str
|
model: str
|
||||||
serial_number: str
|
serial_number: str
|
||||||
version: str
|
version: str
|
||||||
|
|
||||||
|
|
||||||
class Vlans(BaseModel):
|
class Interfaces(Base):
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
"""
|
||||||
|
Requred
|
||||||
|
"""
|
||||||
|
|
||||||
vlan_id: int
|
name: str = Field(alias="interface")
|
||||||
|
ip_address: IPv4Address | None = None
|
||||||
|
mask: int | None = None
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Vlans(Base):
|
||||||
|
"""
|
||||||
|
Optional
|
||||||
|
"""
|
||||||
|
|
||||||
|
vlan_id: int = Field(alias="id")
|
||||||
name: str | None = Field(default=None, alias="description")
|
name: str | None = Field(default=None, alias="description")
|
||||||
|
|
||||||
|
|
||||||
class Device(BaseModel):
|
class Device(BaseModel):
|
||||||
system: System
|
system: System
|
||||||
interfaces: list[Interfaces] = []
|
interfaces: list[Interfaces]
|
||||||
vlans: list[Vlans] = []
|
vlans: list[Vlans] = []
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from ipaddress import ip_interface
|
from ipaddress import ip_interface
|
||||||
from oxi.interfaces import register_parser
|
from oxi.interfaces import register_parser
|
||||||
from oxi.interfaces.base import BaseDevice
|
from oxi.interfaces.base import BaseDevice
|
||||||
from oxi.interfaces.contract import Interfaces, Vlans
|
|
||||||
|
|
||||||
|
|
||||||
@register_parser(["NDMS", "keenetic", "KeeneticOS"])
|
@register_parser(["NDMS", "keenetic", "KeeneticOS"])
|
||||||
@@ -34,13 +33,13 @@ class Keenetic(BaseDevice):
|
|||||||
item["description"] = decoded
|
item["description"] = decoded
|
||||||
return interfaces
|
return interfaces
|
||||||
|
|
||||||
def vlans(self):
|
# def vlans(self):
|
||||||
vlans = self.raw["vlans"]
|
# vlans = self.raw["vlans"]
|
||||||
for item in vlans:
|
# for item in vlans:
|
||||||
if item.get("description"):
|
# if item.get("description"):
|
||||||
decoded = self._decode_utf(item.get("description", ""))
|
# decoded = self._decode_utf(item.get("description", ""))
|
||||||
item["description"] = decoded
|
# item["description"] = decoded
|
||||||
return vlans
|
# return vlans
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user