Documentation, comments and small changes
This commit is contained in:
+264
-98
@@ -3,18 +3,20 @@
|
||||
This module contains the in-memory semantic model used by the rest of the
|
||||
pipeline and the parser that converts a raw JSON dictionary into that model.
|
||||
|
||||
The parser is intentionally independent of Mininet. Its job is only to turn
|
||||
JSON-shaped data into typed Python objects. Cross-reference and semantic
|
||||
The parser is intentionally independent of Mininet. Its job is only to turn
|
||||
JSON-shaped data into typed Python objects. Cross-reference and semantic
|
||||
validation belong to the validation stage that follows parsing.
|
||||
|
||||
Author: Christos Choutouridis <cchoutou@ece.auth.gr>
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union
|
||||
|
||||
|
||||
# ======================= Semantic model and errors =======================
|
||||
|
||||
JSONMapping = Mapping[str, Any]
|
||||
|
||||
|
||||
@@ -25,7 +27,7 @@ class TopologyParseError(ValueError):
|
||||
class NodeType(Enum):
|
||||
"""Supported logical node types."""
|
||||
|
||||
HOST = "host"
|
||||
HOST = "host"
|
||||
SWITCH = "switch"
|
||||
ROUTER = "router"
|
||||
|
||||
@@ -33,9 +35,9 @@ class NodeType(Enum):
|
||||
class AddressingMode(Enum):
|
||||
"""Supported Layer-3 addressing modes for an interface."""
|
||||
|
||||
NONE = "none"
|
||||
NONE = "none"
|
||||
STATIC = "static"
|
||||
DHCP = "dhcp"
|
||||
DHCP = "dhcp"
|
||||
|
||||
|
||||
class LinkInterfaceSelector(Enum):
|
||||
@@ -48,7 +50,7 @@ class ServiceInterfaceSelector(Enum):
|
||||
"""Special interface selectors that are valid for a service binding."""
|
||||
|
||||
AUTO = "auto"
|
||||
ALL = "all"
|
||||
ALL = "all"
|
||||
|
||||
|
||||
class ServiceType(Enum):
|
||||
@@ -62,29 +64,33 @@ class Interface:
|
||||
"""Describe one logical network interface owned by a node.
|
||||
|
||||
Args:
|
||||
name: Logical interface tag used by the topology description.
|
||||
addressing: Layer-3 addressing strategy. Omitting addressing in JSON
|
||||
is represented as AddressingMode.NONE.
|
||||
address: Static CIDR address, valid when addressing is STATIC.
|
||||
gateway: Optional default gateway, valid when addressing is STATIC.
|
||||
mac: Optional explicit MAC address. Reserved for current/future use.
|
||||
mtu: Optional MTU value. Reserved for current/future use.
|
||||
name: Logical interface tag used by the topology description.
|
||||
addressing: Layer-3 addressing strategy.
|
||||
Omitting addressing in JSON is represented as AddressingMode.NONE.
|
||||
address: Static CIDR address, valid when addressing is STATIC.
|
||||
gateway: [Optional] default gateway, valid when addressing is STATIC.
|
||||
mac: [Optional] explicit MAC address applied to the concrete interface.
|
||||
mtu: [Optional] MTU in bytes applied to the concrete interface.
|
||||
|
||||
Notes:
|
||||
MAC and MTU are independent of the addressing mode.
|
||||
The generated program applies them using Linux ip link commands.
|
||||
"""
|
||||
|
||||
name: str
|
||||
name: str
|
||||
addressing: AddressingMode = AddressingMode.NONE
|
||||
address: Optional[str] = None
|
||||
gateway: Optional[str] = None
|
||||
mac: Optional[str] = None
|
||||
mtu: Optional[int] = None
|
||||
address: Optional[str] = None
|
||||
gateway: Optional[str] = None
|
||||
mac: Optional[str] = None
|
||||
mtu: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class HostSettings:
|
||||
"""Contain host-specific settings.
|
||||
|
||||
The current grammar does not require host-specific settings yet. Keeping
|
||||
a dedicated type gives the model a stable extension point.
|
||||
The current grammar does not require host-specific settings yet.
|
||||
Keeping a dedicated type gives the model a stable extension point.
|
||||
"""
|
||||
|
||||
|
||||
@@ -92,8 +98,8 @@ class HostSettings:
|
||||
class SwitchSettings:
|
||||
"""Contain switch-specific settings.
|
||||
|
||||
The current grammar does not require switch-specific settings yet. Keeping
|
||||
a dedicated type gives the model a stable extension point.
|
||||
The current grammar does not require switch-specific settings yet.
|
||||
Keping a dedicated type gives the model a stable extension point.
|
||||
"""
|
||||
|
||||
|
||||
@@ -107,9 +113,9 @@ class RouterSettings:
|
||||
"""
|
||||
|
||||
ip_forward: Optional[bool] = None
|
||||
rp_filter: Optional[bool] = None
|
||||
|
||||
rp_filter: Optional[bool] = None
|
||||
|
||||
# Mutable global
|
||||
NodeSettings = Union[HostSettings, SwitchSettings, RouterSettings]
|
||||
|
||||
|
||||
@@ -118,19 +124,21 @@ class Node:
|
||||
"""Describe one logical topology node.
|
||||
|
||||
Args:
|
||||
name: Unique logical node name.
|
||||
type: Semantic node type.
|
||||
interfaces: Declared logical interfaces. None means that the node did
|
||||
not declare an interface set and therefore allows dynamic interface
|
||||
allocation. An empty dictionary means that an explicit, fixed
|
||||
interface set was declared and contains zero interfaces.
|
||||
name: Unique logical node name.
|
||||
type: Semantic node type.
|
||||
interfaces:
|
||||
Declared logical interfaces.
|
||||
None means that the node did not declare an interface set and therefore
|
||||
allows dynamic interface allocation.
|
||||
An empty dictionary means that an explicit, fixed interface set was declared
|
||||
and contains zero interfaces.
|
||||
settings: Type-specific node settings.
|
||||
"""
|
||||
|
||||
name: str
|
||||
type: NodeType
|
||||
name: str
|
||||
type: NodeType
|
||||
interfaces: Optional[Dict[str, Interface]]
|
||||
settings: NodeSettings
|
||||
settings: NodeSettings
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -138,11 +146,11 @@ class LinkEndpoint:
|
||||
"""Describe one endpoint of a point-to-point logical link.
|
||||
|
||||
Args:
|
||||
node: Name of the node referenced by the endpoint.
|
||||
node: Name of the node referenced by the endpoint.
|
||||
interface: Explicit logical interface tag or the AUTO selector.
|
||||
"""
|
||||
|
||||
node: str
|
||||
node: str
|
||||
interface: Union[str, LinkInterfaceSelector]
|
||||
|
||||
|
||||
@@ -151,22 +159,23 @@ class LinkSettings:
|
||||
"""Contain optional link-specific provisioning settings.
|
||||
|
||||
Args:
|
||||
bandwidth_mbps: Optional link bandwidth limit in megabits per second.
|
||||
delay_ms: Optional one-way propagation delay in milliseconds.
|
||||
loss_percent: Optional packet-loss percentage.
|
||||
jitter_ms: Optional delay variation in milliseconds.
|
||||
bandwidth_mbps: [Optional] link bandwidth limit in megabits per second.
|
||||
delay_ms: [Optional] one-way propagation delay in milliseconds.
|
||||
loss_percent: [Optional] packet-loss percentage.
|
||||
jitter_ms: [Optional] delay variation in milliseconds.
|
||||
|
||||
Notes:
|
||||
These fields are reserved as extension points for later Mininet link
|
||||
provisioning, for example through traffic-controlled link classes.
|
||||
They are parsed now so the semantic model already has a stable place
|
||||
for link-level configuration.
|
||||
All four settings are implemented through Mininet TCLink parameters
|
||||
in the generated program.
|
||||
Bandwidth maps to bw, loss to loss, and delay and jitter to millisecond strings.
|
||||
Additional provisioning settings require corresponding parser,
|
||||
validator, planner, and renderer support before they can be used.
|
||||
"""
|
||||
|
||||
bandwidth_mbps: Optional[float] = None
|
||||
delay_ms: Optional[float] = None
|
||||
loss_percent: Optional[float] = None
|
||||
jitter_ms: Optional[float] = None
|
||||
delay_ms: Optional[float] = None
|
||||
loss_percent: Optional[float] = None
|
||||
jitter_ms: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -175,13 +184,13 @@ class Link:
|
||||
|
||||
Args:
|
||||
endpoints: Ordered pair of link endpoints.
|
||||
name: Optional logical link name used for diagnostics or extensions.
|
||||
settings: Link-specific provisioning settings.
|
||||
name: [ Optional] logical link name used for diagnostics or extensions.
|
||||
settings: Link-specific provisioning settings.
|
||||
"""
|
||||
|
||||
endpoints: Tuple[LinkEndpoint, LinkEndpoint]
|
||||
name: Optional[str] = None
|
||||
settings: LinkSettings = field(default_factory=LinkSettings)
|
||||
name: Optional[str] = None
|
||||
settings: LinkSettings = field(default_factory=LinkSettings)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -190,11 +199,11 @@ class DHCPRange:
|
||||
|
||||
Args:
|
||||
start: First address in the DHCP allocation range.
|
||||
end: Last address in the DHCP allocation range.
|
||||
end: Last address in the DHCP allocation range.
|
||||
"""
|
||||
|
||||
start: str
|
||||
end: str
|
||||
end: str
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -203,13 +212,14 @@ class DHCPServerSettings:
|
||||
|
||||
Args:
|
||||
address_range: Address range offered to DHCP clients.
|
||||
gateway: Optional default gateway advertised to DHCP clients.
|
||||
gateway: [Optional] default gateway advertised to DHCP clients.
|
||||
"""
|
||||
|
||||
address_range: DHCPRange
|
||||
gateway: Optional[str] = None
|
||||
gateway: Optional[str] = None
|
||||
|
||||
|
||||
# Mutable global
|
||||
ServiceSettings = DHCPServerSettings
|
||||
|
||||
|
||||
@@ -218,44 +228,48 @@ class Service:
|
||||
"""Describe a service attached to a topology node.
|
||||
|
||||
Args:
|
||||
type: Service implementation type.
|
||||
node: Name of the node on which the service runs.
|
||||
type: Service implementation type.
|
||||
node: Name of the node on which the service runs.
|
||||
interface: Explicit logical interface tag or a supported special
|
||||
selector such as AUTO or ALL.
|
||||
settings: Type-specific service settings.
|
||||
settings: Type-specific service settings.
|
||||
"""
|
||||
|
||||
type: ServiceType
|
||||
node: str
|
||||
interface: Union[str, ServiceInterfaceSelector]
|
||||
settings: ServiceSettings
|
||||
settings: ServiceSettings
|
||||
|
||||
|
||||
@dataclass
|
||||
class Topology:
|
||||
"""Contain the complete parsed topology model.
|
||||
|
||||
Args:
|
||||
nodes: Nodes in declaration order. Keeping nodes as a list preserves
|
||||
the parsed source faithfully, including duplicate names, so that
|
||||
the separate validation stage can diagnose them instead of losing
|
||||
information during parsing.
|
||||
links: Links in declaration order. Order is preserved because it can
|
||||
make automatic interface allocation deterministic.
|
||||
services: Services in declaration order. Order may later be useful for
|
||||
deterministic startup and shutdown.
|
||||
nodes:
|
||||
Nodes in declaration order. Keeping nodes as a list preserves
|
||||
the parsed source faithfully, including duplicate names, so that
|
||||
the separate validation stage can diagnose them instead of losing
|
||||
information during parsing.
|
||||
links:
|
||||
Links in declaration order. Order is preserved because it can
|
||||
make automatic interface allocation deterministic.
|
||||
services:
|
||||
Services in declaration order. Order may later be useful for
|
||||
deterministic startup and shutdown.
|
||||
"""
|
||||
|
||||
nodes: List[Node] = field(default_factory=list)
|
||||
links: List[Link] = field(default_factory=list)
|
||||
nodes: List[Node] = field(default_factory=list)
|
||||
links: List[Link] = field(default_factory=list)
|
||||
services: List[Service] = field(default_factory=list)
|
||||
|
||||
|
||||
# ============================ Parsing filters ============================
|
||||
|
||||
def _mapping(value: Any, context: str) -> JSONMapping:
|
||||
"""Return a value as a mapping or raise a parse error.
|
||||
|
||||
Args:
|
||||
value: Value expected to be a JSON object.
|
||||
value: Value expected to be a JSON object.
|
||||
context: Human-readable location used in the error message.
|
||||
|
||||
Returns:
|
||||
@@ -278,7 +292,7 @@ def _reject_unknown_fields(
|
||||
"""Reject object members that are not part of the grammar.
|
||||
|
||||
Args:
|
||||
data: Parsed JSON object whose member names are checked.
|
||||
data: Parsed JSON object whose member names are checked.
|
||||
allowed: Set of field names accepted at this grammar location.
|
||||
context: Human-readable location used in the error message.
|
||||
|
||||
@@ -292,7 +306,7 @@ def _reject_unknown_fields(
|
||||
"%s contains unknown field%s: %s"
|
||||
% (
|
||||
context,
|
||||
"s" if len(unknown) != 1 else "",
|
||||
"s" if len(unknown) != 1 else "", # don't forget your English grammar ;)
|
||||
", ".join(repr(name) for name in unknown),
|
||||
)
|
||||
)
|
||||
@@ -302,7 +316,7 @@ def _list(value: Any, context: str) -> List[Any]:
|
||||
"""Return a value as a list or raise a parse error.
|
||||
|
||||
Args:
|
||||
value: Value expected to be a JSON array.
|
||||
value: Value expected to be a JSON array.
|
||||
context: Human-readable location used in the error message.
|
||||
|
||||
Returns:
|
||||
@@ -321,7 +335,7 @@ def _string(value: Any, context: str) -> str:
|
||||
"""Return a value as a string or raise a parse error.
|
||||
|
||||
Args:
|
||||
value: Value expected to be a string.
|
||||
value: Value expected to be a string.
|
||||
context: Human-readable location used in the error message.
|
||||
|
||||
Returns:
|
||||
@@ -340,7 +354,7 @@ def _optional_string(value: Any, context: str) -> Optional[str]:
|
||||
"""Return an optional string or raise a parse error.
|
||||
|
||||
Args:
|
||||
value: None or a value expected to be a string.
|
||||
value: None or a value expected to be a string.
|
||||
context: Human-readable location used in the error message.
|
||||
|
||||
Returns:
|
||||
@@ -359,7 +373,7 @@ def _optional_bool(value: Any, context: str) -> Optional[bool]:
|
||||
"""Return an optional boolean or raise a parse error.
|
||||
|
||||
Args:
|
||||
value: None or a value expected to be a boolean.
|
||||
value: None or a value expected to be a boolean.
|
||||
context: Human-readable location used in the error message.
|
||||
|
||||
Returns:
|
||||
@@ -380,7 +394,7 @@ def _optional_int(value: Any, context: str) -> Optional[int]:
|
||||
"""Return an optional integer or raise a parse error.
|
||||
|
||||
Args:
|
||||
value: None or a value expected to be an integer.
|
||||
value: None or a value expected to be an integer.
|
||||
context: Human-readable location used in the error message.
|
||||
|
||||
Returns:
|
||||
@@ -392,6 +406,7 @@ def _optional_int(value: Any, context: str) -> Optional[int]:
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
# bool is a subclass of int, but JSON booleans are not numeric settings.
|
||||
if isinstance(value, bool) or not isinstance(value, int):
|
||||
raise TopologyParseError("%s must be an integer" % context)
|
||||
return value
|
||||
@@ -401,8 +416,7 @@ def _optional_float(value: Any, context: str) -> Optional[float]:
|
||||
"""Return an optional numeric value as float or raise a parse error.
|
||||
|
||||
Args:
|
||||
value: None or a value expected to be an integer or floating-point
|
||||
number.
|
||||
value: None or a value expected to be an integer or floating-point number.
|
||||
context: Human-readable location used in the error message.
|
||||
|
||||
Returns:
|
||||
@@ -414,22 +428,42 @@ def _optional_float(value: Any, context: str) -> Optional[float]:
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
# Reject booleans before accepting Python's integer and float types.
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
raise TopologyParseError("%s must be a number" % context)
|
||||
return float(value)
|
||||
|
||||
# ============================= Entity parsing =============================
|
||||
|
||||
def _parse_interface(name: str, raw: Any, context: str) -> Interface:
|
||||
"""Parse one interface definition.
|
||||
|
||||
Args:
|
||||
name: Logical interface tag taken from the JSON object key.
|
||||
raw: Raw JSON value containing interface properties.
|
||||
name: Logical interface tag taken from the JSON object key.
|
||||
raw: Raw JSON value containing interface properties.
|
||||
context: Human-readable location used in error messages.
|
||||
|
||||
Returns:
|
||||
Parsed Interface object.
|
||||
|
||||
Example:
|
||||
Input (diagnostic context omitted):
|
||||
name = "lan"
|
||||
raw = {
|
||||
"addressing": "static",
|
||||
"address": "192.168.1.1/24",
|
||||
}
|
||||
|
||||
Output:
|
||||
Interface(
|
||||
name="lan",
|
||||
addressing=AddressingMode.STATIC,
|
||||
address="192.168.1.1/24",
|
||||
gateway=None,
|
||||
mac=None,
|
||||
mtu=None,
|
||||
)
|
||||
|
||||
Raises:
|
||||
TopologyParseError: If the interface cannot be represented.
|
||||
"""
|
||||
@@ -464,16 +498,34 @@ def _parse_interfaces(raw_node: JSONMapping, context: str) -> Optional[Dict[str,
|
||||
|
||||
Args:
|
||||
raw_node: Raw JSON node object.
|
||||
context: Human-readable node location used in error messages.
|
||||
context: Human-readable node location used in error messages.
|
||||
|
||||
Returns:
|
||||
None when the interfaces field is omitted, otherwise a dictionary of
|
||||
logical interface tags to Interface objects.
|
||||
- None when the interfaces field is omitted (dynamic), otherwise
|
||||
- a dictionary of logical interface tags to Interface objects.
|
||||
|
||||
Example:
|
||||
Each case shows the value passed as raw_node and the returned value,
|
||||
the diagnostic context argument is omitted.
|
||||
raw_node is the node object, not just its interfaces member.
|
||||
Unrelated node fields are omitted from these examples.
|
||||
|
||||
Interface set raw_node["interfaces"] return
|
||||
--------------------------------------------------------
|
||||
Omitted: {} None
|
||||
Explicitly empty: {"interfaces": {}} {}
|
||||
One declared: {"interfaces": {"lan": {}}} {"lan": Interface(name="lan")}
|
||||
|
||||
Omission preserves a dynamic interface set. An empty object preserves
|
||||
a fixed pool containing zero interfaces.
|
||||
Interface(name="lan") uses the model defaults, including NONE
|
||||
addressing and no address, gateway, MAC, or MTU.
|
||||
|
||||
Raises:
|
||||
TopologyParseError: If the interfaces field has an invalid shape.
|
||||
"""
|
||||
|
||||
# Preserve the distinction between dynamic allocation and a fixed empty pool.
|
||||
if "interfaces" not in raw_node:
|
||||
return None
|
||||
|
||||
@@ -496,12 +548,23 @@ def _parse_node_settings(node_type: NodeType, raw: Any, context: str) -> NodeSet
|
||||
|
||||
Args:
|
||||
node_type: Parsed semantic node type.
|
||||
raw: Raw JSON settings object or None.
|
||||
context: Human-readable node location used in error messages.
|
||||
raw: Raw JSON settings object or None.
|
||||
context: Human-readable node location used in error messages.
|
||||
|
||||
Returns:
|
||||
Type-specific node settings object.
|
||||
|
||||
Example:
|
||||
Input (Python values after JSON loading, diagnostic context omitted):
|
||||
node_type = NodeType.ROUTER
|
||||
raw = {"ip_forward": True, "rp_filter": False}
|
||||
|
||||
Output:
|
||||
RouterSettings(ip_forward=True, rp_filter=False)
|
||||
|
||||
For node_type = NodeType.HOST and raw = {}, the output is
|
||||
HostSettings().
|
||||
|
||||
Raises:
|
||||
TopologyParseError: If the settings object has an invalid shape or
|
||||
contains values that cannot be represented by the selected type.
|
||||
@@ -538,12 +601,34 @@ def _parse_node(raw: Any, index: int) -> Node:
|
||||
"""Parse one node declaration.
|
||||
|
||||
Args:
|
||||
raw: Raw JSON node object.
|
||||
raw: Raw JSON node object.
|
||||
index: Node index in the source array, used in diagnostics.
|
||||
|
||||
Returns:
|
||||
Parsed Node object.
|
||||
|
||||
Example:
|
||||
Input raw (diagnostic index omitted):
|
||||
{
|
||||
"name": "h1",
|
||||
"type": "host",
|
||||
"interfaces": {"net0": {"addressing": "dhcp"}},
|
||||
}
|
||||
|
||||
Output:
|
||||
Node(
|
||||
name="h1",
|
||||
type=NodeType.HOST,
|
||||
interfaces={
|
||||
"net0": Interface(
|
||||
name="net0", addressing=AddressingMode.DHCP,
|
||||
),
|
||||
},
|
||||
settings=HostSettings(),
|
||||
)
|
||||
|
||||
Unspecified Interface fields use their model defaults.
|
||||
|
||||
Raises:
|
||||
TopologyParseError: If the node cannot be represented.
|
||||
"""
|
||||
@@ -578,7 +663,7 @@ def _parse_link_interface(value: Any, context: str) -> Union[str, LinkInterfaceS
|
||||
"""Parse a link endpoint interface reference.
|
||||
|
||||
Args:
|
||||
value: Raw interface reference.
|
||||
value: Raw interface reference.
|
||||
context: Human-readable location used in error messages.
|
||||
|
||||
Returns:
|
||||
@@ -598,7 +683,7 @@ def _parse_link_endpoint(raw: Any, context: str) -> LinkEndpoint:
|
||||
"""Parse one link endpoint.
|
||||
|
||||
Args:
|
||||
raw: Raw JSON endpoint object.
|
||||
raw: Raw JSON endpoint object.
|
||||
context: Human-readable location used in error messages.
|
||||
|
||||
Returns:
|
||||
@@ -624,7 +709,7 @@ def _parse_link_settings(raw: Any, context: str) -> LinkSettings:
|
||||
"""Parse optional link-specific provisioning settings.
|
||||
|
||||
Args:
|
||||
raw: Raw JSON settings object or None.
|
||||
raw: Raw JSON settings object or None.
|
||||
context: Human-readable link location used in error messages.
|
||||
|
||||
Returns:
|
||||
@@ -665,12 +750,35 @@ def _parse_link(raw: Any, index: int) -> Link:
|
||||
"""Parse one link declaration.
|
||||
|
||||
Args:
|
||||
raw: Raw JSON link object.
|
||||
raw: Raw JSON link object.
|
||||
index: Link index in the source array, used in diagnostics.
|
||||
|
||||
Returns:
|
||||
Parsed Link object.
|
||||
|
||||
Example:
|
||||
Input raw (diagnostic index omitted):
|
||||
{
|
||||
"endpoints": [
|
||||
{"node": "h1", "interface": "net0"},
|
||||
{"node": "s1", "interface": "auto"},
|
||||
],
|
||||
}
|
||||
|
||||
Output:
|
||||
Link(
|
||||
endpoints=(
|
||||
LinkEndpoint(node="h1", interface="net0"),
|
||||
LinkEndpoint(
|
||||
node="s1", interface=LinkInterfaceSelector.AUTO,
|
||||
),
|
||||
),
|
||||
)
|
||||
Note:
|
||||
- Omitted name and settings use the Link defaults.
|
||||
- Parsing converts the endpoint list to a tuple and recognizes AUTO.
|
||||
- Validation checks whether the referenced nodes and interfaces exist.
|
||||
|
||||
Raises:
|
||||
TopologyParseError: If the link cannot be represented.
|
||||
"""
|
||||
@@ -712,7 +820,7 @@ def _parse_service_interface(
|
||||
"""Parse a service interface binding.
|
||||
|
||||
Args:
|
||||
value: Raw service interface reference.
|
||||
value: Raw service interface reference.
|
||||
context: Human-readable location used in error messages.
|
||||
|
||||
Returns:
|
||||
@@ -735,12 +843,32 @@ def _parse_dhcp_server_settings(raw: Any, context: str) -> DHCPServerSettings:
|
||||
"""Parse DHCP-server-specific settings.
|
||||
|
||||
Args:
|
||||
raw: Raw JSON DHCP server settings object.
|
||||
raw: Raw JSON DHCP server settings object.
|
||||
context: Human-readable location used in error messages.
|
||||
|
||||
Returns:
|
||||
Parsed DHCPServerSettings object.
|
||||
|
||||
Example:
|
||||
Input raw (diagnostic context omitted):
|
||||
{
|
||||
"range": {
|
||||
"start": "192.168.1.100",
|
||||
"end": "192.168.1.200",
|
||||
},
|
||||
"gateway": "192.168.1.1",
|
||||
}
|
||||
|
||||
Output:
|
||||
DHCPServerSettings(
|
||||
address_range=DHCPRange(
|
||||
start="192.168.1.100", end="192.168.1.200",
|
||||
),
|
||||
gateway="192.168.1.1",
|
||||
)
|
||||
|
||||
The JSON range object becomes the typed address_range field.
|
||||
|
||||
Raises:
|
||||
TopologyParseError: If required structural fields are missing or have
|
||||
incompatible JSON types.
|
||||
@@ -767,12 +895,43 @@ def _parse_service(raw: Any, index: int) -> Service:
|
||||
"""Parse one service declaration.
|
||||
|
||||
Args:
|
||||
raw: Raw JSON service object.
|
||||
raw: Raw JSON service object.
|
||||
index: Service index in the source array, used in diagnostics.
|
||||
|
||||
Returns:
|
||||
Parsed Service object.
|
||||
|
||||
Example:
|
||||
Input raw (diagnostic index omitted):
|
||||
{
|
||||
"type": "dhcp-server",
|
||||
"node": "r0",
|
||||
"interface": "lan",
|
||||
"settings": {
|
||||
"range": {
|
||||
"start": "192.168.1.100",
|
||||
"end": "192.168.1.200",
|
||||
},
|
||||
"gateway": "192.168.1.1",
|
||||
},
|
||||
}
|
||||
|
||||
Output:
|
||||
Service(
|
||||
type=ServiceType.DHCP_SERVER,
|
||||
node="r0",
|
||||
interface="lan",
|
||||
settings=DHCPServerSettings(
|
||||
address_range=DHCPRange(
|
||||
start="192.168.1.100", end="192.168.1.200",
|
||||
),
|
||||
gateway="192.168.1.1",
|
||||
),
|
||||
)
|
||||
|
||||
Binding fields remain separate from service settings.
|
||||
Parsing preserves the references. Validation checks their validity.
|
||||
|
||||
Raises:
|
||||
TopologyParseError: If the service cannot be represented.
|
||||
"""
|
||||
@@ -814,6 +973,10 @@ def _parse_service(raw: Any, index: int) -> Service:
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
# ============================ Public interface ============================
|
||||
|
||||
def parse_topology(raw: Mapping[str, Any]) -> Topology:
|
||||
"""Parse a raw JSON dictionary into the typed topology model.
|
||||
|
||||
@@ -828,8 +991,8 @@ def parse_topology(raw: Mapping[str, Any]) -> Topology:
|
||||
topology grammar.
|
||||
|
||||
Notes:
|
||||
This function intentionally does not validate cross references such as
|
||||
whether a link references an existing node. Those checks belong to
|
||||
This function intentionally does not validate cross-references such as
|
||||
whether a link references an existing node. Those checks belong to
|
||||
the separate validation stage.
|
||||
"""
|
||||
|
||||
@@ -858,6 +1021,9 @@ def parse_topology(raw: Mapping[str, Any]) -> Topology:
|
||||
return Topology(nodes=nodes, links=links, services=services)
|
||||
|
||||
|
||||
|
||||
# =============================== Self-test ===============================
|
||||
|
||||
def _self_test() -> None:
|
||||
"""Run a small smoke test against the public parse_topology() interface."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user