Documentation, comments and small changes

This commit is contained in:
2026-09-27 16:18:35 +03:00
parent 0e43b943c4
commit e1e164bd65
8 changed files with 653 additions and 211 deletions
+75 -10
View File
@@ -32,6 +32,8 @@ from topology_parser import (
)
# ========================== Execution plan model ==========================
class TopologyPlanError(ValueError):
"""Raised when a validated topology cannot be lowered into a plan."""
@@ -182,6 +184,12 @@ class PlannedService:
class ExecutionPlan:
"""Contain the complete, fully resolved execution plan.
Ordered tuples of frozen operation objects capture the result of planning.
They carry concrete names and configuration into the renderer without
exposing the mutable allocation context or the source model objects.
The renderer emits the operations in lifecycle order: construction,
configuration, and service startup.
Args:
nodes: Node creation operations in declaration order.
links: Link creation operations in declaration order.
@@ -201,21 +209,45 @@ class ExecutionPlan:
services: Tuple[PlannedService, ...]
# ======================== Mutable planning context ========================
@dataclass
class _PlanningContext:
"""Hold mutable state used while lowering a topology.
_build_context() initializes node lookup, reservations, free pools, and
counters from the validated topology.
During _plan_links(), allocation helpers consume pool entries, advance
counters, and record interface mappings.
Interface, node-setting, and service planning then read those mappings.
A logical interface is identified by (node_name, logical_name), since tags
need only be unique within their owning node.
interface_names and interface_models acquire the same keys on allocation:
one supplies the backend name, and the other supplies its configuration.
A reservation alone does not create an entry in either mapping.
Args:
nodes: Nodes indexed by logical name.
reserved_interfaces: Explicitly referenced logical interfaces per node.
free_interfaces: Remaining declared interfaces available to AUTO.
dynamic_interface_counters: Synthetic logical-name counters for nodes
whose interface sets were omitted.
physical_interface_counters: Concrete eth-index counters per node.
interface_names: Mapping from resolved logical interfaces to concrete
Linux/Mininet interface names.
interface_models: Mapping from resolved logical interfaces to their
source Interface objects.
nodes: Source nodes indexed by name, safe after uniqueness validation.
reserved_interfaces: All explicit link references per node, used to
construct free pools and retained unchanged during allocation.
free_interfaces: Ordered lists of unreserved declared tags for fixed
pools; AUTO removes the first entry.
An empty list means exhaustion; dynamic nodes have no entry.
dynamic_interface_counters: Next synthetic logical-tag indices, such
as auto0; advanced only for nodes with omitted interface sets.
physical_interface_counters: Next concrete eth-index per node;
advanced for every allocation, including explicit selections.
interface_names: Allocated (node, logical tag) pairs mapped to concrete
Linux/Mininet names, in interface-configuration order.
interface_models: The same allocated keys mapped to source Interface
objects or default models created for dynamic interfaces.
Example:
A fixed pool declared as p1, p2, uplink with an explicit uplink
reference starts with free_interfaces containing [p1, p2].
If the first endpoint for that node uses AUTO, it consumes p1 and
receives <node>-eth0, recording both mappings for (node, p1).
"""
nodes: Dict[str, Node]
@@ -227,9 +259,15 @@ class _PlanningContext:
interface_models: Dict[Tuple[str, str], Interface]
# ========================= Context initialization =========================
def _build_context(topology: Topology) -> _PlanningContext:
"""Create planning state and reserve all explicit link interfaces.
Scan every link before constructing the free-interface pools so an AUTO
endpoint cannot consume an interface explicitly referenced by a later link.
Preserve declaration order in each pool for deterministic allocation.
Args:
topology: Validated topology to lower.
@@ -267,6 +305,8 @@ def _build_context(topology: Topology) -> _PlanningContext:
)
# ========================= Node and link planning =========================
def _plan_nodes(topology: Topology) -> Tuple[PlannedNode, ...]:
"""Lower topology nodes into concrete node creation operations.
@@ -296,6 +336,11 @@ def _allocate_logical_interface(
) -> Tuple[str, Interface]:
"""Resolve one link endpoint to a concrete logical interface.
Explicit tags look up their declared models without consuming a free pool.
AUTO consumes the first available tag for a fixed pool, or creates a
synthetic tag and an unconfigured model for a dynamic pool.
Concrete backend names are assigned separately by physical allocation.
Args:
node: Node that owns the endpoint.
selector: Explicit interface tag or AUTO selector.
@@ -373,6 +418,7 @@ def _allocate_physical_interface(
context.physical_interface_counters[node_name] = index + 1
interface_name = "%s-eth%d" % (node_name, index)
# These mappings let later stages recover both the name and configuration.
context.interface_names[key] = interface_name
context.interface_models[key] = interface
@@ -385,6 +431,11 @@ def _plan_links(
) -> Tuple[PlannedLink, ...]:
"""Resolve all link endpoints and concrete interface names.
Traverse links and their endpoints in source order, resolving each logical
selection before assigning its backend name.
Allocation records mappings in the shared context for subsequent interface,
node-setting, and service planning, so traversal order determines naming.
Args:
topology: Validated topology to lower.
context: Mutable planning context.
@@ -436,6 +487,8 @@ def _plan_links(
return tuple(planned_links)
# ==================== Interface and node configuration ====================
def _interface_requires_materialization(interface: Interface) -> bool:
"""Return whether an unused declared interface carries configuration.
@@ -462,6 +515,12 @@ def _plan_interfaces(
) -> Tuple[PlannedInterface, ...]:
"""Create interface-configuration operations for materialized interfaces.
First reject configured declarations absent from the allocation mappings:
Mininet creates these interfaces through links, so there would be no
concrete interface on which to apply their configuration.
Empty unused declarations need no operation and may remain unallocated.
Then pair each allocated name with its model in allocation order.
Args:
topology: Validated topology to lower.
context: Planning context containing resolved interface mappings.
@@ -581,6 +640,8 @@ def _plan_node_settings(
return tuple(planned)
# ============================ Service planning ============================
def _resolve_service_logical_interface(
service: Service,
node: Node,
@@ -694,6 +755,8 @@ def _plan_services(
return tuple(planned)
# ============================ Public interface ============================
def plan(topology: Topology) -> ExecutionPlan:
"""Lower a validated topology into a fully resolved execution plan.
@@ -730,6 +793,8 @@ def plan(topology: Topology) -> ExecutionPlan:
)
# =============================== Self-test ===============================
def _self_test() -> None:
"""Run a smoke test against the public plan() interface."""