Files
oxipy/oxi/interfaces/base.py
IluaAir 41c4cc48e9 Update project description and enhance documentation for clarity
- Revised the project description in `pyproject.toml` to better reflect the functionality of the `oxipy` client.
- Improved the README.md by adding detailed explanations of the project structure, installation instructions, and usage examples.
- Updated documentation files to enhance clarity and organization, including sections on extending models and writing TTP templates.
- Adjusted various TTP templates to ensure consistency and accuracy in the parsing of device configurations.
2026-05-25 16:01:38 +03:00

144 lines
5.0 KiB
Python

from abc import ABC, abstractmethod
from pathlib import Path
from ttp import ttp
from oxi.exception import OxiAPIError
from oxi.interfaces.contract import Device
import xml.etree.ElementTree as ET
from oxi.interfaces.contract import Interfaces, System, Vlans
class BaseDevice(ABC):
_REQUIRED_SECTIONS: frozenset[str] = frozenset({"system", "interfaces"})
_OPTIONAL_SECTIONS: frozenset[str] = frozenset({"vlans"})
def __init__(self, config: str, name: str | None = None):
self.config: str = config
self.name = name
self._loaded_template = self._load_template()
self._declared_sections = None
self._validate_template_groups()
self.raw: dict = self._run_ttp()
@property
@abstractmethod
def template(self) -> str:
"""
Name of the TTP template file used by this device parser.
"""
def vlans(self) -> list[dict]:
"""
Parse VLAN configuration from self.raw['vlans'].
Expected structure:
[{"vlan_id": 10, "description": "MGMT"}, {"vlan_id": 15, "name": "SSH"}, ...]
Returns:
list[Vlans]: VLANs from the vlans section, or an empty list
when the section is absent.
Raises:
ValueError: if raw data cannot be validated by the contract.
"""
return self.raw.get("vlans", [])
def interfaces(self) -> list[dict]:
"""
Parse Interface configuration from self.raw['interfaces'].
Expected raw structure:
[{"interface": "GEthernet1/0/1", "ip_address": "192.168.1.1", "mask": "24", "description": "IPBB interface"}]
Raises:
ValueError: if raw data cannot be validated by the contract.
"""
return self.raw.get("interfaces", [])
def system(self) -> dict:
"""
Parse System configuration from self.raw['system'].
Expected raw structure:
{"model":"RB951Ui-2nD", serial_number: "B88C0B31117B", "version": "7.12.1"}
Raises:
ValueError: if raw data cannot be validated by the contract.
"""
return self.raw.get("system", None)
def _validate_contract(self) -> dict:
if self.raw is None:
msg = (
f"Node {self.name} not found"
if self.name
else "Node not found"
)
raise OxiAPIError(msg, status_code=404)
system_data = self.system()
interfaces_data = self.interfaces() or []
result = {
"system": System(**system_data),
"interfaces": [Interfaces(**item) for item in interfaces_data],
"vlans": [],
}
if "vlans" in self._declared_sections:
if "vlans" not in self.raw:
raise ValueError(
f"{self.__class__.__name__}: template '{self.template}' declares optional group "
f"'vlans', but TTP did not return it."
)
vlans_data = self.vlans() or []
result["vlans"] = [Vlans(**item) for item in vlans_data]
return result
def _load_template(self):
"""Load the device TTP template from models/templates."""
path = Path(__file__).parent / "models" / "templates" / self.template
if not path.exists():
raise FileNotFoundError(f"Template {self.template} not found")
return path.read_text(encoding="utf-8")
def _validate_template_groups(self) -> None:
"""Validate that the template declares all required groups."""
try:
root = ET.fromstring(self._loaded_template)
except ET.ParseError:
root = ET.fromstring(f"<template>{self._loaded_template}</template>")
declared = {g.get("name") for g in root.iter("group") if g.get("name")}
self._declared_sections = declared
missing_required = self._REQUIRED_SECTIONS - declared
if missing_required:
raise ValueError(
f"{self.__class__.__name__}: template '{self.template}' "
f"missing required groups: {sorted(missing_required)}. "
f"Declared groups: {sorted(declared)}"
)
def _run_ttp(self) -> dict:
"""Run the node-not-found check and then parse the config with TTP."""
pattern = """node not {{found}}"""
parser = ttp(data=self.config, template=pattern)
parser.parse()
res = parser.result()
if res[0][0]:
# raise OxiAPIError(f"Node {self.name} not found", status_code=404)
return None
p = ttp(data=self.config, template=self._loaded_template)
p.parse()
raw: dict = p.result()[0][0]
missing = self._REQUIRED_SECTIONS - raw.keys()
if missing:
raise ValueError(
f"{self.__class__.__name__}: TTP template '{self.template}' "
f"did not produce required groups: {sorted(missing)}. "
f"Return only: {(raw.keys())}"
)
return raw
def parse(self) -> Device:
return Device(**self._validate_contract())