813 lines
24 KiB
Python
813 lines
24 KiB
Python
"""Lower a validated topology into a fully resolved execution plan.
|
|
|
|
This module is the planning stage of the topology pipeline.
|
|
|
|
The parser produces a typed description of the user's intent and the validator
|
|
proves that the description is semantically coherent. The planner then makes
|
|
all implementation choices that must be resolved before executable Mininet
|
|
Python can be generated.
|
|
|
|
In particular, the planner resolves logical and automatic interface references
|
|
into concrete Linux/Mininet interface names. It does not import Mininet,
|
|
execute commands, modify the network, or render Python source.
|
|
|
|
Author: Christos Choutouridis <cchoutou@ece.auth.gr>
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Optional, Set, Tuple, Union
|
|
|
|
from topology_parser import (
|
|
AddressingMode,
|
|
DHCPServerSettings,
|
|
Interface,
|
|
LinkInterfaceSelector,
|
|
Node,
|
|
NodeType,
|
|
RouterSettings,
|
|
Service,
|
|
ServiceInterfaceSelector,
|
|
ServiceType,
|
|
Topology,
|
|
)
|
|
|
|
|
|
class TopologyPlanError(ValueError):
|
|
"""Raised when a validated topology cannot be lowered into a plan."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PlannedNode:
|
|
"""Describe one concrete node creation operation.
|
|
|
|
Args:
|
|
name: Logical node name used by Mininet.
|
|
type: Concrete semantic node type to create.
|
|
"""
|
|
|
|
name: str
|
|
type: NodeType
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PlannedLinkEndpoint:
|
|
"""Describe one fully resolved endpoint of a planned link.
|
|
|
|
Args:
|
|
node: Logical node name that owns the endpoint.
|
|
logical_interface: Resolved logical interface identifier. For a node
|
|
with a dynamic interface set, the planner creates a deterministic
|
|
synthetic identifier for an automatically allocated interface.
|
|
interface_name: Concrete Linux/Mininet interface name.
|
|
"""
|
|
|
|
node: str
|
|
logical_interface: str
|
|
interface_name: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PlannedLinkSettings:
|
|
"""Contain resolved provisioning settings for one link.
|
|
|
|
Args:
|
|
bandwidth_mbps: Optional bandwidth limit in megabits per second.
|
|
delay_ms: Optional one-way delay in milliseconds.
|
|
loss_percent: Optional packet-loss percentage.
|
|
jitter_ms: Optional delay variation in milliseconds.
|
|
"""
|
|
|
|
bandwidth_mbps: Optional[float] = None
|
|
delay_ms: Optional[float] = None
|
|
loss_percent: Optional[float] = None
|
|
jitter_ms: Optional[float] = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PlannedLink:
|
|
"""Describe one fully resolved Mininet link creation operation.
|
|
|
|
Args:
|
|
endpoints: Ordered pair of fully resolved link endpoints.
|
|
name: Optional logical link name preserved for diagnostics.
|
|
settings: Concrete link provisioning settings.
|
|
"""
|
|
|
|
endpoints: Tuple[PlannedLinkEndpoint, PlannedLinkEndpoint]
|
|
name: Optional[str]
|
|
settings: PlannedLinkSettings
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PlannedInterface:
|
|
"""Describe post-link configuration for one concrete interface.
|
|
|
|
Args:
|
|
node: Logical node name that owns the interface.
|
|
logical_interface: Resolved logical interface identifier.
|
|
interface_name: Concrete Linux/Mininet interface name.
|
|
addressing: Layer-3 addressing strategy.
|
|
address: Optional static CIDR address.
|
|
gateway: Optional static default gateway.
|
|
mac: Optional explicit MAC address.
|
|
mtu: Optional MTU value.
|
|
"""
|
|
|
|
node: str
|
|
logical_interface: str
|
|
interface_name: str
|
|
addressing: AddressingMode
|
|
address: Optional[str]
|
|
gateway: Optional[str]
|
|
mac: Optional[str]
|
|
mtu: Optional[int]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PlannedSysctl:
|
|
"""Describe one namespace-scoped sysctl operation.
|
|
|
|
Args:
|
|
node: Logical node on which the sysctl is applied.
|
|
key: Fully resolved sysctl key.
|
|
value: Integer value written to the key.
|
|
"""
|
|
|
|
node: str
|
|
key: str
|
|
value: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PlannedDHCPServerSettings:
|
|
"""Contain fully resolved DHCP server settings.
|
|
|
|
Args:
|
|
range_start: First address offered to DHCP clients.
|
|
range_end: Last address offered to DHCP clients.
|
|
gateway: Optional default gateway advertised to DHCP clients.
|
|
"""
|
|
|
|
range_start: str
|
|
range_end: str
|
|
gateway: Optional[str]
|
|
|
|
|
|
PlannedServiceSettings = PlannedDHCPServerSettings
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PlannedService:
|
|
"""Describe one fully resolved service startup operation.
|
|
|
|
Args:
|
|
type: Service implementation type.
|
|
node: Logical node on which the service runs.
|
|
logical_interface: Resolved logical interface identifier.
|
|
interface_name: Concrete Linux/Mininet interface name to bind.
|
|
settings: Service-type-specific resolved settings.
|
|
"""
|
|
|
|
type: ServiceType
|
|
node: str
|
|
logical_interface: str
|
|
interface_name: str
|
|
settings: PlannedServiceSettings
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ExecutionPlan:
|
|
"""Contain the complete, fully resolved execution plan.
|
|
|
|
Args:
|
|
nodes: Node creation operations in declaration order.
|
|
links: Link creation operations in declaration order.
|
|
interfaces: Concrete interface configuration operations.
|
|
node_settings: Concrete namespace-level configuration operations.
|
|
services: Service startup operations in declaration order.
|
|
|
|
Notes:
|
|
The plan contains no AUTO or ALL selectors. All such decisions are
|
|
resolved before an ExecutionPlan is returned.
|
|
"""
|
|
|
|
nodes: Tuple[PlannedNode, ...]
|
|
links: Tuple[PlannedLink, ...]
|
|
interfaces: Tuple[PlannedInterface, ...]
|
|
node_settings: Tuple[PlannedSysctl, ...]
|
|
services: Tuple[PlannedService, ...]
|
|
|
|
|
|
@dataclass
|
|
class _PlanningContext:
|
|
"""Hold mutable state used while lowering a topology.
|
|
|
|
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: Dict[str, Node]
|
|
reserved_interfaces: Dict[str, Set[str]]
|
|
free_interfaces: Dict[str, List[str]]
|
|
dynamic_interface_counters: Dict[str, int]
|
|
physical_interface_counters: Dict[str, int]
|
|
interface_names: Dict[Tuple[str, str], str]
|
|
interface_models: Dict[Tuple[str, str], Interface]
|
|
|
|
|
|
def _build_context(topology: Topology) -> _PlanningContext:
|
|
"""Create planning state and reserve all explicit link interfaces.
|
|
|
|
Args:
|
|
topology: Validated topology to lower.
|
|
|
|
Returns:
|
|
Initialized mutable planning context.
|
|
"""
|
|
|
|
nodes = {node.name: node for node in topology.nodes}
|
|
reserved: Dict[str, Set[str]] = {node.name: set() for node in topology.nodes}
|
|
|
|
for link in topology.links:
|
|
for endpoint in link.endpoints:
|
|
if isinstance(endpoint.interface, str):
|
|
reserved[endpoint.node].add(endpoint.interface)
|
|
|
|
free_interfaces: Dict[str, List[str]] = {}
|
|
for node in topology.nodes:
|
|
if node.interfaces is None:
|
|
continue
|
|
|
|
free_interfaces[node.name] = [
|
|
name
|
|
for name in node.interfaces.keys()
|
|
if name not in reserved[node.name]
|
|
]
|
|
|
|
return _PlanningContext(
|
|
nodes=nodes,
|
|
reserved_interfaces=reserved,
|
|
free_interfaces=free_interfaces,
|
|
dynamic_interface_counters={node.name: 0 for node in topology.nodes},
|
|
physical_interface_counters={node.name: 0 for node in topology.nodes},
|
|
interface_names={},
|
|
interface_models={},
|
|
)
|
|
|
|
|
|
def _plan_nodes(topology: Topology) -> Tuple[PlannedNode, ...]:
|
|
"""Lower topology nodes into concrete node creation operations.
|
|
|
|
Args:
|
|
topology: Validated topology to lower.
|
|
|
|
Returns:
|
|
Planned nodes in source declaration order.
|
|
"""
|
|
|
|
return tuple(
|
|
PlannedNode(name=node.name, type=node.type)
|
|
for node in topology.nodes
|
|
)
|
|
|
|
|
|
def _allocate_logical_interface(
|
|
node: Node,
|
|
selector: Union[str, LinkInterfaceSelector],
|
|
context: _PlanningContext,
|
|
) -> Tuple[str, Interface]:
|
|
"""Resolve one link endpoint to a concrete logical interface.
|
|
|
|
Args:
|
|
node: Node that owns the endpoint.
|
|
selector: Explicit interface tag or AUTO selector.
|
|
context: Mutable planning context.
|
|
|
|
Returns:
|
|
Pair containing the resolved logical interface name and its interface
|
|
configuration model.
|
|
|
|
Raises:
|
|
TopologyPlanError: If the topology violates a planner precondition.
|
|
"""
|
|
|
|
if isinstance(selector, str):
|
|
if node.interfaces is None or selector not in node.interfaces:
|
|
raise TopologyPlanError(
|
|
"explicit interface %r cannot be resolved on node %r"
|
|
% (selector, node.name)
|
|
)
|
|
|
|
return selector, node.interfaces[selector]
|
|
|
|
if selector is not LinkInterfaceSelector.AUTO:
|
|
raise TopologyPlanError(
|
|
"unsupported link interface selector %r" % selector
|
|
)
|
|
|
|
if node.interfaces is not None:
|
|
free = context.free_interfaces[node.name]
|
|
if not free:
|
|
raise TopologyPlanError(
|
|
"node %r has no declared interface available for AUTO"
|
|
% node.name
|
|
)
|
|
|
|
logical_name = free.pop(0)
|
|
return logical_name, node.interfaces[logical_name]
|
|
|
|
counter = context.dynamic_interface_counters[node.name]
|
|
context.dynamic_interface_counters[node.name] = counter + 1
|
|
|
|
logical_name = "auto%d" % counter
|
|
return logical_name, Interface(name=logical_name)
|
|
|
|
|
|
def _allocate_physical_interface(
|
|
node_name: str,
|
|
logical_name: str,
|
|
interface: Interface,
|
|
context: _PlanningContext,
|
|
) -> str:
|
|
"""Assign one deterministic concrete interface name.
|
|
|
|
Args:
|
|
node_name: Logical node that owns the interface.
|
|
logical_name: Resolved logical interface identifier.
|
|
interface: Interface configuration model.
|
|
context: Mutable planning context.
|
|
|
|
Returns:
|
|
Concrete Linux/Mininet interface name.
|
|
|
|
Raises:
|
|
TopologyPlanError: If the same logical interface is allocated twice.
|
|
"""
|
|
|
|
key = (node_name, logical_name)
|
|
if key in context.interface_names:
|
|
raise TopologyPlanError(
|
|
"interface %s:%s was allocated more than once"
|
|
% (node_name, logical_name)
|
|
)
|
|
|
|
index = context.physical_interface_counters[node_name]
|
|
context.physical_interface_counters[node_name] = index + 1
|
|
|
|
interface_name = "%s-eth%d" % (node_name, index)
|
|
context.interface_names[key] = interface_name
|
|
context.interface_models[key] = interface
|
|
|
|
return interface_name
|
|
|
|
|
|
def _plan_links(
|
|
topology: Topology,
|
|
context: _PlanningContext,
|
|
) -> Tuple[PlannedLink, ...]:
|
|
"""Resolve all link endpoints and concrete interface names.
|
|
|
|
Args:
|
|
topology: Validated topology to lower.
|
|
context: Mutable planning context.
|
|
|
|
Returns:
|
|
Planned links in source declaration order.
|
|
"""
|
|
|
|
planned_links: List[PlannedLink] = []
|
|
|
|
for link in topology.links:
|
|
planned_endpoints: List[PlannedLinkEndpoint] = []
|
|
|
|
for endpoint in link.endpoints:
|
|
node = context.nodes[endpoint.node]
|
|
logical_name, interface = _allocate_logical_interface(
|
|
node,
|
|
endpoint.interface,
|
|
context,
|
|
)
|
|
interface_name = _allocate_physical_interface(
|
|
node.name,
|
|
logical_name,
|
|
interface,
|
|
context,
|
|
)
|
|
|
|
planned_endpoints.append(
|
|
PlannedLinkEndpoint(
|
|
node=node.name,
|
|
logical_interface=logical_name,
|
|
interface_name=interface_name,
|
|
)
|
|
)
|
|
|
|
planned_links.append(
|
|
PlannedLink(
|
|
endpoints=(planned_endpoints[0], planned_endpoints[1]),
|
|
name=link.name,
|
|
settings=PlannedLinkSettings(
|
|
bandwidth_mbps=link.settings.bandwidth_mbps,
|
|
delay_ms=link.settings.delay_ms,
|
|
loss_percent=link.settings.loss_percent,
|
|
jitter_ms=link.settings.jitter_ms,
|
|
),
|
|
)
|
|
)
|
|
|
|
return tuple(planned_links)
|
|
|
|
|
|
def _interface_requires_materialization(interface: Interface) -> bool:
|
|
"""Return whether an unused declared interface carries configuration.
|
|
|
|
Args:
|
|
interface: Declared interface to inspect.
|
|
|
|
Returns:
|
|
True when silently leaving the interface unmaterialized would discard
|
|
requested configuration.
|
|
"""
|
|
|
|
return (
|
|
interface.addressing is not AddressingMode.NONE
|
|
or interface.address is not None
|
|
or interface.gateway is not None
|
|
or interface.mac is not None
|
|
or interface.mtu is not None
|
|
)
|
|
|
|
|
|
def _plan_interfaces(
|
|
topology: Topology,
|
|
context: _PlanningContext,
|
|
) -> Tuple[PlannedInterface, ...]:
|
|
"""Create interface-configuration operations for materialized interfaces.
|
|
|
|
Args:
|
|
topology: Validated topology to lower.
|
|
context: Planning context containing resolved interface mappings.
|
|
|
|
Returns:
|
|
Planned interface configuration operations.
|
|
|
|
Raises:
|
|
TopologyPlanError: If a configured declared interface is not attached to
|
|
any link and therefore has no concrete Mininet interface to
|
|
configure.
|
|
"""
|
|
|
|
for node in topology.nodes:
|
|
if node.interfaces is None:
|
|
continue
|
|
|
|
for logical_name, interface in node.interfaces.items():
|
|
key = (node.name, logical_name)
|
|
if (
|
|
key not in context.interface_names
|
|
and _interface_requires_materialization(interface)
|
|
):
|
|
raise TopologyPlanError(
|
|
"configured interface %s:%s is not attached to any link"
|
|
% (node.name, logical_name)
|
|
)
|
|
|
|
planned: List[PlannedInterface] = []
|
|
|
|
# The mapping insertion order is the exact allocation order from
|
|
# _plan_links(), which is deterministic on supported Python versions.
|
|
for key, interface_name in context.interface_names.items():
|
|
node_name, logical_name = key
|
|
interface = context.interface_models[key]
|
|
|
|
planned.append(
|
|
PlannedInterface(
|
|
node=node_name,
|
|
logical_interface=logical_name,
|
|
interface_name=interface_name,
|
|
addressing=interface.addressing,
|
|
address=interface.address,
|
|
gateway=interface.gateway,
|
|
mac=interface.mac,
|
|
mtu=interface.mtu,
|
|
)
|
|
)
|
|
|
|
return tuple(planned)
|
|
|
|
|
|
def _plan_node_settings(
|
|
topology: Topology,
|
|
context: _PlanningContext,
|
|
) -> Tuple[PlannedSysctl, ...]:
|
|
"""Lower node settings into concrete namespace sysctl operations.
|
|
|
|
Args:
|
|
topology: Validated topology to lower.
|
|
context: Planning context containing concrete interface names.
|
|
|
|
Returns:
|
|
Planned sysctl operations in deterministic order.
|
|
"""
|
|
|
|
planned: List[PlannedSysctl] = []
|
|
|
|
for node in topology.nodes:
|
|
if node.type is not NodeType.ROUTER:
|
|
continue
|
|
|
|
if not isinstance(node.settings, RouterSettings):
|
|
raise TopologyPlanError(
|
|
"router %r does not carry RouterSettings" % node.name
|
|
)
|
|
|
|
if node.settings.ip_forward is not None:
|
|
planned.append(
|
|
PlannedSysctl(
|
|
node=node.name,
|
|
key="net.ipv4.ip_forward",
|
|
value=1 if node.settings.ip_forward else 0,
|
|
)
|
|
)
|
|
|
|
if node.settings.rp_filter is not None:
|
|
value = 1 if node.settings.rp_filter else 0
|
|
|
|
planned.append(
|
|
PlannedSysctl(
|
|
node=node.name,
|
|
key="net.ipv4.conf.all.rp_filter",
|
|
value=value,
|
|
)
|
|
)
|
|
planned.append(
|
|
PlannedSysctl(
|
|
node=node.name,
|
|
key="net.ipv4.conf.default.rp_filter",
|
|
value=value,
|
|
)
|
|
)
|
|
|
|
for (owner, _), interface_name in context.interface_names.items():
|
|
if owner != node.name:
|
|
continue
|
|
|
|
planned.append(
|
|
PlannedSysctl(
|
|
node=node.name,
|
|
key="net.ipv4.conf.%s.rp_filter" % interface_name,
|
|
value=value,
|
|
)
|
|
)
|
|
|
|
return tuple(planned)
|
|
|
|
|
|
def _resolve_service_logical_interface(
|
|
service: Service,
|
|
node: Node,
|
|
) -> str:
|
|
"""Resolve one validated service interface selector.
|
|
|
|
Args:
|
|
service: Service whose interface binding is resolved.
|
|
node: Node on which the service runs.
|
|
|
|
Returns:
|
|
Concrete logical interface tag.
|
|
|
|
Raises:
|
|
TopologyPlanError: If the selector cannot be lowered uniquely.
|
|
"""
|
|
|
|
if isinstance(service.interface, str):
|
|
return service.interface
|
|
|
|
if service.interface is ServiceInterfaceSelector.AUTO:
|
|
if node.interfaces is None:
|
|
raise TopologyPlanError(
|
|
"automatic service binding on node %r requires declared "
|
|
"interfaces" % node.name
|
|
)
|
|
|
|
candidates = [
|
|
name
|
|
for name, interface in node.interfaces.items()
|
|
if interface.addressing is AddressingMode.STATIC
|
|
]
|
|
|
|
if len(candidates) != 1:
|
|
raise TopologyPlanError(
|
|
"automatic service binding on node %r is not uniquely "
|
|
"resolvable" % node.name
|
|
)
|
|
|
|
return candidates[0]
|
|
|
|
if service.interface is ServiceInterfaceSelector.ALL:
|
|
raise TopologyPlanError(
|
|
"service type %r cannot be lowered from the ALL selector"
|
|
% service.type.value
|
|
)
|
|
|
|
raise TopologyPlanError(
|
|
"unsupported service interface selector %r" % service.interface
|
|
)
|
|
|
|
|
|
def _plan_services(
|
|
topology: Topology,
|
|
context: _PlanningContext,
|
|
) -> Tuple[PlannedService, ...]:
|
|
"""Resolve service bindings and lower service-specific settings.
|
|
|
|
Args:
|
|
topology: Validated topology to lower.
|
|
context: Planning context containing concrete interface names.
|
|
|
|
Returns:
|
|
Planned service startup operations in declaration order.
|
|
|
|
Raises:
|
|
TopologyPlanError: If a service cannot be mapped to a materialized
|
|
interface or has unsupported settings.
|
|
"""
|
|
|
|
planned: List[PlannedService] = []
|
|
|
|
for service in topology.services:
|
|
node = context.nodes[service.node]
|
|
logical_name = _resolve_service_logical_interface(service, node)
|
|
key = (node.name, logical_name)
|
|
|
|
if key not in context.interface_names:
|
|
raise TopologyPlanError(
|
|
"service %r binds to interface %s:%s, but that interface is "
|
|
"not attached to any link"
|
|
% (service.type.value, node.name, logical_name)
|
|
)
|
|
|
|
if service.type is ServiceType.DHCP_SERVER:
|
|
if not isinstance(service.settings, DHCPServerSettings):
|
|
raise TopologyPlanError(
|
|
"DHCP service on %r has incompatible settings" % node.name
|
|
)
|
|
|
|
settings = PlannedDHCPServerSettings(
|
|
range_start=service.settings.address_range.start,
|
|
range_end=service.settings.address_range.end,
|
|
gateway=service.settings.gateway,
|
|
)
|
|
else:
|
|
raise TopologyPlanError(
|
|
"unsupported service type %r" % service.type.value
|
|
)
|
|
|
|
planned.append(
|
|
PlannedService(
|
|
type=service.type,
|
|
node=node.name,
|
|
logical_interface=logical_name,
|
|
interface_name=context.interface_names[key],
|
|
settings=settings,
|
|
)
|
|
)
|
|
|
|
return tuple(planned)
|
|
|
|
|
|
def plan(topology: Topology) -> ExecutionPlan:
|
|
"""Lower a validated topology into a fully resolved execution plan.
|
|
|
|
Args:
|
|
topology: Topology that has already passed semantic validation.
|
|
|
|
Returns:
|
|
Fully resolved execution plan suitable for source-code rendering.
|
|
|
|
Raises:
|
|
TopologyPlanError: If a planner precondition is violated or if a
|
|
validated construct cannot be lowered by the current backend.
|
|
|
|
Notes:
|
|
This function deliberately does not call validate(). Validation and
|
|
planning remain separate pipeline stages, and callers are expected to
|
|
validate the topology before invoking the planner.
|
|
"""
|
|
|
|
context = _build_context(topology)
|
|
|
|
nodes = _plan_nodes(topology)
|
|
links = _plan_links(topology, context)
|
|
interfaces = _plan_interfaces(topology, context)
|
|
node_settings = _plan_node_settings(topology, context)
|
|
services = _plan_services(topology, context)
|
|
|
|
return ExecutionPlan(
|
|
nodes=nodes,
|
|
links=links,
|
|
interfaces=interfaces,
|
|
node_settings=node_settings,
|
|
services=services,
|
|
)
|
|
|
|
|
|
def _self_test() -> None:
|
|
"""Run a smoke test against the public plan() interface."""
|
|
|
|
from topology_parser import parse_topology
|
|
from topology_validator import validate
|
|
|
|
raw = {
|
|
"nodes": [
|
|
{
|
|
"name": "h1",
|
|
"type": "host",
|
|
"interfaces": {
|
|
"net0": {
|
|
"addressing": "dhcp",
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"name": "s1",
|
|
"type": "switch",
|
|
"interfaces": {
|
|
"client": {},
|
|
"uplink": {},
|
|
},
|
|
},
|
|
{
|
|
"name": "r0",
|
|
"type": "router",
|
|
"interfaces": {
|
|
"lan": {
|
|
"addressing": "static",
|
|
"address": "192.168.1.1/24",
|
|
}
|
|
},
|
|
"settings": {
|
|
"ip_forward": True,
|
|
"rp_filter": False,
|
|
},
|
|
},
|
|
],
|
|
"links": [
|
|
{
|
|
"endpoints": [
|
|
{"node": "h1", "interface": "net0"},
|
|
{"node": "s1", "interface": "auto"},
|
|
]
|
|
},
|
|
{
|
|
"endpoints": [
|
|
{"node": "r0", "interface": "lan"},
|
|
{"node": "s1", "interface": "uplink"},
|
|
]
|
|
},
|
|
],
|
|
"services": [
|
|
{
|
|
"type": "dhcp-server",
|
|
"node": "r0",
|
|
"interface": "lan",
|
|
"settings": {
|
|
"range": {
|
|
"start": "192.168.1.100",
|
|
"end": "192.168.1.200",
|
|
},
|
|
"gateway": "192.168.1.1",
|
|
},
|
|
}
|
|
],
|
|
}
|
|
|
|
topology = parse_topology(raw)
|
|
validate(topology)
|
|
execution_plan = plan(topology)
|
|
|
|
assert len(execution_plan.nodes) == 3
|
|
assert len(execution_plan.links) == 2
|
|
assert execution_plan.links[0].endpoints[1].logical_interface == "client"
|
|
assert execution_plan.links[0].endpoints[0].interface_name == "h1-eth0"
|
|
assert execution_plan.links[1].endpoints[0].interface_name == "r0-eth0"
|
|
assert execution_plan.services[0].interface_name == "r0-eth0"
|
|
assert any(
|
|
setting.key == "net.ipv4.ip_forward" and setting.value == 1
|
|
for setting in execution_plan.node_settings
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_self_test()
|
|
print("topology_plan: self-test passed")
|