Documentation, comments and small changes
This commit is contained in:
+268
-80
@@ -3,11 +3,9 @@
|
||||
This module implements the validation stage of the topology pipeline.
|
||||
|
||||
The parser answers:
|
||||
|
||||
"Can this JSON be represented by the topology grammar?"
|
||||
|
||||
The validator answers:
|
||||
|
||||
"Does the parsed topology describe a coherent network?"
|
||||
|
||||
Validation is intentionally independent of Mininet. It does not create
|
||||
@@ -18,11 +16,11 @@ validation fails.
|
||||
Author: Christos Choutouridis <cchoutou@ece.auth.gr>
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass
|
||||
import ipaddress
|
||||
import math
|
||||
import re
|
||||
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
||||
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
||||
|
||||
from topology_parser import (
|
||||
AddressingMode,
|
||||
@@ -43,6 +41,8 @@ from topology_parser import (
|
||||
)
|
||||
|
||||
|
||||
# ========================== Validation constants ==========================
|
||||
|
||||
_RESERVED_INTERFACE_TAGS = {
|
||||
LinkInterfaceSelector.AUTO.value,
|
||||
ServiceInterfaceSelector.ALL.value,
|
||||
@@ -53,6 +53,8 @@ _MAC_ADDRESS_RE = re.compile(
|
||||
)
|
||||
|
||||
|
||||
# ====================== Diagnostics and lookup state ======================
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ValidationIssue:
|
||||
"""Describe one semantic topology validation failure.
|
||||
@@ -63,7 +65,7 @@ class ValidationIssue:
|
||||
"""
|
||||
|
||||
location: str
|
||||
message: str
|
||||
message: str
|
||||
|
||||
|
||||
class TopologyValidationError(ValueError):
|
||||
@@ -96,7 +98,7 @@ class TopologyValidationError(ValueError):
|
||||
count = len(self.issues)
|
||||
header = "%d topology validation error%s" % (
|
||||
count,
|
||||
"" if count == 1 else "s",
|
||||
"" if count == 1 else "s", # ;)
|
||||
)
|
||||
|
||||
lines = [header]
|
||||
@@ -110,26 +112,57 @@ class TopologyValidationError(ValueError):
|
||||
class _NodeIndex:
|
||||
"""Hold node lookup information prepared for validation.
|
||||
|
||||
_build_node_index() retains the first declaration for each nonempty name
|
||||
and records repeated names separately while reporting duplicate issues.
|
||||
Link and service checks consult duplicate_names before resolving a known
|
||||
node, avoiding secondary diagnostics based on an ambiguous declaration.
|
||||
|
||||
Args:
|
||||
nodes: Unique node declarations indexed by name.
|
||||
nodes: First declaration for each distinct nonempty node name.
|
||||
duplicate_names: Node names that were declared more than once.
|
||||
|
||||
Example:
|
||||
Suppose the source list contains three Node objects:
|
||||
topology.nodes = [first_h1, s1, second_h1]
|
||||
first_h1.name = "h1"
|
||||
s1.name = "s1"
|
||||
second_h1.name = "h1"
|
||||
|
||||
The resulting lookup has this shape:
|
||||
node_index = _NodeIndex(
|
||||
nodes={"h1": first_h1, "s1": s1},
|
||||
duplicate_names={"h1"},
|
||||
)
|
||||
|
||||
Dictionary keys are logical node names, and values reference the
|
||||
original Node objects, not copies or positions in the source list.
|
||||
node_index.nodes["s1"] therefore gives the s1 object directly.
|
||||
The duplicate set contains names only; it is empty for unique names.
|
||||
|
||||
Membership in nodes means a declaration exists, not that it is unique.
|
||||
Although nodes["h1"] retains first_h1, duplicate_names marks that
|
||||
reference as ambiguous so link and service checks skip resolving it.
|
||||
The original topology.nodes list still contains both h1 declarations.
|
||||
Empty names are reported as issues and excluded from both collections.
|
||||
"""
|
||||
|
||||
nodes: Dict[str, Node]
|
||||
nodes: Dict[str, Node]
|
||||
duplicate_names: Set[str]
|
||||
|
||||
|
||||
# =========================== Validation helpers ===========================
|
||||
|
||||
def _add_issue(
|
||||
issues: List[ValidationIssue],
|
||||
issues: List[ValidationIssue],
|
||||
location: str,
|
||||
message: str,
|
||||
message: str,
|
||||
) -> None:
|
||||
"""Append one validation issue.
|
||||
|
||||
Args:
|
||||
issues: Mutable list receiving validation failures.
|
||||
issues: Mutable list receiving validation failures.
|
||||
location: Logical location associated with the failure.
|
||||
message: Human-readable description of the failure.
|
||||
message: Human-readable description of the failure.
|
||||
"""
|
||||
|
||||
issues.append(ValidationIssue(location=location, message=message))
|
||||
@@ -143,13 +176,21 @@ def _build_node_index(
|
||||
|
||||
Args:
|
||||
topology: Parsed topology being validated.
|
||||
issues: Mutable list receiving validation failures.
|
||||
issues: Mutable list receiving validation failures.
|
||||
|
||||
Returns:
|
||||
Node lookup information for later validation passes.
|
||||
|
||||
Notes:
|
||||
The returned _NodeIndex holds name-to-object lookup and ambiguity
|
||||
information; validation issues are appended to the supplied list.
|
||||
first_index is a temporary name-to-source-position map used to point
|
||||
duplicate diagnostics back to the first declaration.
|
||||
It is not part of the returned index, whose shape is illustrated in
|
||||
the _NodeIndex class docstring.
|
||||
"""
|
||||
|
||||
nodes: Dict[str, Node] = {}
|
||||
nodes: Dict[str, Node] = {}
|
||||
first_index: Dict[str, int] = {}
|
||||
duplicate_names: Set[str] = set()
|
||||
|
||||
@@ -179,17 +220,21 @@ def _build_node_index(
|
||||
)
|
||||
|
||||
|
||||
|
||||
# ============================ Validation core =============================
|
||||
|
||||
|
||||
def _validate_node_settings(
|
||||
node: Node,
|
||||
node: Node,
|
||||
location: str,
|
||||
issues: List[ValidationIssue],
|
||||
issues: List[ValidationIssue],
|
||||
) -> None:
|
||||
"""Validate that a node carries settings matching its semantic type.
|
||||
|
||||
Args:
|
||||
node: Node whose settings object is checked.
|
||||
location: Logical location of the node.
|
||||
issues: Mutable list receiving validation failures.
|
||||
node: Node whose settings object is checked.
|
||||
location: Logical location of the node (where node data came from).
|
||||
issues: Mutable list receiving validation failures.
|
||||
"""
|
||||
|
||||
expected_type = {
|
||||
@@ -206,20 +251,46 @@ def _validate_node_settings(
|
||||
)
|
||||
|
||||
|
||||
def _parse_ipv4_interface(
|
||||
value: str,
|
||||
def _validate_ipv4_interface(
|
||||
value: str,
|
||||
location: str,
|
||||
issues: List[ValidationIssue],
|
||||
issues: List[ValidationIssue],
|
||||
) -> Optional[ipaddress.IPv4Interface]:
|
||||
"""Parse and validate one IPv4 CIDR interface address.
|
||||
"""Validate one IPv4 CIDR interface address.
|
||||
|
||||
Require an explicit prefix and convert the string to an IPv4Interface for
|
||||
subsequent subnet checks.
|
||||
Invalid syntax or an unsupported address family records a validation issue
|
||||
at the supplied location instead of raising a parsing exception.
|
||||
|
||||
Args:
|
||||
value: Address string expected to contain an IPv4 address and prefix.
|
||||
location: Logical location associated with the address.
|
||||
issues: Mutable list receiving validation failures.
|
||||
value: Address string expected to contain an IPv4 address and prefix.
|
||||
location: Logical location associated with the address(where address string--value-- came from).
|
||||
issues: Mutable list receiving validation failures.
|
||||
|
||||
Returns:
|
||||
Parsed IPv4Interface on success, otherwise None.
|
||||
IPv4Interface on success, otherwise None after recording an issue.
|
||||
|
||||
Example:
|
||||
Input:
|
||||
value = "192.168.1.10/24"
|
||||
location = "nodes[0].interfaces.lan.address"
|
||||
issues = []
|
||||
|
||||
Output (result names the return value):
|
||||
result = ipaddress.IPv4Interface("192.168.1.10/24")
|
||||
issues = []
|
||||
|
||||
With value = "192.168.1.10" and a fresh empty issues list:
|
||||
result = None
|
||||
issues = [
|
||||
ValidationIssue(
|
||||
location="nodes[0].interfaces.lan.address",
|
||||
message="static address must use CIDR notation",
|
||||
),
|
||||
]
|
||||
|
||||
Failure appends to the supplied list rather than raising an exception.
|
||||
"""
|
||||
|
||||
if "/" not in value:
|
||||
@@ -243,20 +314,47 @@ def _parse_ipv4_interface(
|
||||
return parsed
|
||||
|
||||
|
||||
def _parse_ipv4_address(
|
||||
value: str,
|
||||
def _validate_ipv4_address(
|
||||
value: str,
|
||||
location: str,
|
||||
issues: List[ValidationIssue],
|
||||
issues: List[ValidationIssue],
|
||||
) -> Optional[ipaddress.IPv4Address]:
|
||||
"""Parse and validate one IPv4 address without a prefix.
|
||||
"""Validate one IPv4 address without a prefix.
|
||||
|
||||
Convert the string to an IPv4Address for subsequent semantic checks.
|
||||
Invalid syntax or an unsupported address family records a validation issue
|
||||
at the supplied location instead of raising a parsing exception.
|
||||
|
||||
Args:
|
||||
value: Address string expected to contain one IPv4 address.
|
||||
location: Logical location associated with the address.
|
||||
issues: Mutable list receiving validation failures.
|
||||
value: Address string expected to contain one IPv4 address.
|
||||
location: Logical location associated with the address (where value string came from).
|
||||
issues: Mutable list receiving validation failures.
|
||||
|
||||
Returns:
|
||||
Parsed IPv4Address on success, otherwise None.
|
||||
IPv4Address on success, otherwise None after recording an issue.
|
||||
|
||||
Example:
|
||||
Input:
|
||||
value = "192.168.1.1"
|
||||
location = "nodes[0].interfaces.lan.gateway"
|
||||
issues = []
|
||||
|
||||
Output (result names the return value):
|
||||
result = ipaddress.IPv4Address("192.168.1.1")
|
||||
issues = []
|
||||
|
||||
With value = "::1" and a fresh empty issues list:
|
||||
result = None
|
||||
issues = [
|
||||
ValidationIssue(
|
||||
location="nodes[0].interfaces.lan.gateway",
|
||||
message="only IPv4 addresses are supported",
|
||||
),
|
||||
]
|
||||
|
||||
A prefixed value such as "192.168.1.1/24" also returns None and
|
||||
appends an invalid-IPv4-address issue, since this helper expects
|
||||
an address without a prefix.
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -273,18 +371,18 @@ def _parse_ipv4_address(
|
||||
|
||||
|
||||
def _validate_interface(
|
||||
node: Node,
|
||||
node: Node,
|
||||
interface: Interface,
|
||||
location: str,
|
||||
issues: List[ValidationIssue],
|
||||
location: str,
|
||||
issues: List[ValidationIssue],
|
||||
) -> None:
|
||||
"""Validate one declared logical interface.
|
||||
|
||||
Args:
|
||||
node: Node that owns the interface.
|
||||
node: Node that owns the interface.
|
||||
interface: Interface being validated.
|
||||
location: Logical location of the interface.
|
||||
issues: Mutable list receiving validation failures.
|
||||
location: Logical location of the interface (where the interface data came from).
|
||||
issues: Mutable list receiving validation failures.
|
||||
"""
|
||||
|
||||
if not interface.name:
|
||||
@@ -314,16 +412,18 @@ def _validate_interface(
|
||||
"static addressing requires an address",
|
||||
)
|
||||
else:
|
||||
parsed_address = _parse_ipv4_interface(
|
||||
parsed_address = _validate_ipv4_interface(
|
||||
interface.address,
|
||||
location + ".address",
|
||||
issues,
|
||||
)
|
||||
|
||||
# For subnets larger than two(2) addresses, reject assigning the network or broadcast
|
||||
# address to an interface.
|
||||
if parsed_address is not None:
|
||||
network = parsed_address.network
|
||||
if (
|
||||
network.num_addresses > 2
|
||||
network.num_addresses > 2 # /30 and up
|
||||
and parsed_address.ip in {network.network_address, network.broadcast_address}
|
||||
):
|
||||
_add_issue(
|
||||
@@ -333,7 +433,7 @@ def _validate_interface(
|
||||
)
|
||||
|
||||
if interface.gateway is not None:
|
||||
parsed_gateway = _parse_ipv4_address(
|
||||
parsed_gateway = _validate_ipv4_address(
|
||||
interface.gateway,
|
||||
location + ".gateway",
|
||||
issues,
|
||||
@@ -353,6 +453,7 @@ def _validate_interface(
|
||||
"gateway must not be the interface's own address",
|
||||
)
|
||||
elif (
|
||||
# Same here, /30 and up
|
||||
parsed_address.network.num_addresses > 2
|
||||
and parsed_gateway
|
||||
in {
|
||||
@@ -398,7 +499,7 @@ def _validate_interface(
|
||||
|
||||
def _validate_nodes(
|
||||
topology: Topology,
|
||||
issues: List[ValidationIssue],
|
||||
issues: List[ValidationIssue],
|
||||
) -> _NodeIndex:
|
||||
"""Validate node declarations and their local interface configuration.
|
||||
|
||||
@@ -413,6 +514,7 @@ def _validate_nodes(
|
||||
node_index = _build_node_index(topology, issues)
|
||||
|
||||
for node_number, node in enumerate(topology.nodes):
|
||||
# keep node's location for error reporting
|
||||
node_location = "nodes[%d]" % node_number
|
||||
|
||||
_validate_node_settings(node, node_location, issues)
|
||||
@@ -423,12 +525,14 @@ def _validate_nodes(
|
||||
gateway_locations: List[str] = []
|
||||
|
||||
for interface_name, interface in node.interfaces.items():
|
||||
# keep interface's location for error reporting
|
||||
interface_location = "%s.interfaces.%s" % (
|
||||
node_location,
|
||||
interface_name,
|
||||
)
|
||||
|
||||
if interface.name != interface_name:
|
||||
# yes I have name inside the interface
|
||||
_add_issue(
|
||||
issues,
|
||||
interface_location,
|
||||
@@ -437,10 +541,7 @@ def _validate_nodes(
|
||||
)
|
||||
|
||||
_validate_interface(
|
||||
node,
|
||||
interface,
|
||||
interface_location,
|
||||
issues,
|
||||
node, interface, interface_location, issues
|
||||
)
|
||||
|
||||
if interface.gateway is not None:
|
||||
@@ -457,24 +558,26 @@ def _validate_nodes(
|
||||
return node_index
|
||||
|
||||
|
||||
# ============================ Link validation ============================
|
||||
|
||||
def _validate_link_settings(
|
||||
settings: LinkSettings,
|
||||
location: str,
|
||||
issues: List[ValidationIssue],
|
||||
issues: List[ValidationIssue],
|
||||
) -> None:
|
||||
"""Validate optional link provisioning settings.
|
||||
|
||||
Args:
|
||||
settings: Link settings being validated.
|
||||
location: Logical location of the settings object.
|
||||
issues: Mutable list receiving validation failures.
|
||||
location: Logical location of the settings object (where the settings data came from).
|
||||
issues: Mutable list receiving validation failures.
|
||||
"""
|
||||
|
||||
numeric_settings = {
|
||||
"bandwidth_mbps": settings.bandwidth_mbps,
|
||||
"delay_ms": settings.delay_ms,
|
||||
"loss_percent": settings.loss_percent,
|
||||
"jitter_ms": settings.jitter_ms,
|
||||
"delay_ms": settings.delay_ms,
|
||||
"loss_percent": settings.loss_percent,
|
||||
"jitter_ms": settings.jitter_ms,
|
||||
}
|
||||
|
||||
for name, value in numeric_settings.items():
|
||||
@@ -525,7 +628,7 @@ def _validate_link_names(
|
||||
|
||||
Args:
|
||||
topology: Parsed topology being validated.
|
||||
issues: Mutable list receiving validation failures.
|
||||
issues: Mutable list receiving validation failures.
|
||||
"""
|
||||
|
||||
first_index: Dict[str, int] = {}
|
||||
@@ -559,10 +662,28 @@ def _validate_links(
|
||||
) -> None:
|
||||
"""Validate links, endpoint references, and interface allocation capacity.
|
||||
|
||||
First collect valid explicit interface usage and AUTO requests while
|
||||
checking endpoint references and rejecting explicit reuse.
|
||||
Then compare each fixed pool's AUTO demand with its remaining capacity
|
||||
after all explicit reservations are known.
|
||||
This checks allocation feasibility without selecting concrete interfaces;
|
||||
dynamic pools need no capacity check.
|
||||
|
||||
Args:
|
||||
topology: Parsed topology being validated.
|
||||
topology: Parsed topology being validated.
|
||||
node_index: Node lookup information prepared by node validation.
|
||||
issues: Mutable list receiving validation failures.
|
||||
issues: Mutable list receiving validation failures.
|
||||
|
||||
Example:
|
||||
For otherwise valid links involving a fixed pool on an example switch s1:
|
||||
Declared interfaces: p1, p2
|
||||
Explicit reservation: p2
|
||||
AUTO requests: 2
|
||||
Remaining capacity: 1
|
||||
Result: None; one capacity issue appended
|
||||
|
||||
With only one AUTO request, no capacity issue is appended.
|
||||
Neither case consumes the pool or assigns an interface to AUTO.
|
||||
"""
|
||||
|
||||
_validate_link_names(topology, issues)
|
||||
@@ -578,6 +699,7 @@ def _validate_links(
|
||||
)
|
||||
|
||||
for endpoint_index, endpoint in enumerate(link.endpoints):
|
||||
# keep endpoint's location for error reporting
|
||||
endpoint_location = "links[%d].endpoints[%d]" % (
|
||||
link_index,
|
||||
endpoint_index,
|
||||
@@ -651,9 +773,11 @@ def _validate_links(
|
||||
|
||||
used_explicit[key] = endpoint_location
|
||||
|
||||
# Count AUTO capacity only after collecting every explicit reservation.
|
||||
for node_name, request_locations in auto_requests.items():
|
||||
node = node_index.nodes[node_name]
|
||||
|
||||
# Dynamic check
|
||||
if node.interfaces is None:
|
||||
continue
|
||||
|
||||
@@ -670,39 +794,58 @@ def _validate_links(
|
||||
issues,
|
||||
"links",
|
||||
"node %r has %d automatic interface request%s but only %d "
|
||||
"unused declared interface%s remain%s"
|
||||
% (
|
||||
"unused declared interface%s remain%s" % (
|
||||
node_name,
|
||||
len(request_locations),
|
||||
"" if len(request_locations) == 1 else "s",
|
||||
free_count,
|
||||
"" if free_count == 1 else "s",
|
||||
"; %d request%s cannot be satisfied"
|
||||
% (
|
||||
"; %d request%s cannot be satisfied" % (
|
||||
shortage,
|
||||
"" if shortage == 1 else "s",
|
||||
),
|
||||
),
|
||||
# Ok, laugh as you want, but I like my plurals!
|
||||
)
|
||||
|
||||
|
||||
# =========================== Service validation ===========================
|
||||
|
||||
def _resolve_service_interface(
|
||||
service: Service,
|
||||
service: Service,
|
||||
service_location: str,
|
||||
node: Node,
|
||||
issues: List[ValidationIssue],
|
||||
node: Node,
|
||||
issues: List[ValidationIssue],
|
||||
) -> Optional[Interface]:
|
||||
"""Resolve a service binding to one declared interface when possible.
|
||||
|
||||
Explicit tags select a declared model directly; AUTO requires exactly one
|
||||
declared interface with static addressing.
|
||||
Service AUTO never allocates an interface or consumes a link's free pool.
|
||||
ALL is left unresolved for the service-specific validator to diagnose.
|
||||
|
||||
Args:
|
||||
service: Service whose interface binding is resolved.
|
||||
service_location: Logical location of the service.
|
||||
node: Node on which the service runs.
|
||||
issues: Mutable list receiving validation failures.
|
||||
service: Service whose interface binding is resolved.
|
||||
service_location: Logical location of the service (whre service data came from).
|
||||
node: Node on which the service runs.
|
||||
issues: Mutable list receiving validation failures.
|
||||
|
||||
Returns:
|
||||
Resolved Interface when the binding identifies exactly one valid
|
||||
interface, otherwise None.
|
||||
|
||||
Example:
|
||||
With service.interface = ServiceInterfaceSelector.AUTO and issues = []:
|
||||
Declared static ifaces: lan
|
||||
Result: node.interfaces["lan"]
|
||||
Issues: unchanged
|
||||
|
||||
With two declared static interfaces, lan and wan:
|
||||
Result: None
|
||||
Issues: one ambiguity issue appended at the service interface
|
||||
|
||||
These scenarios describe selection only; address validity and link
|
||||
attachment are checked elsewhere in the pipeline.
|
||||
"""
|
||||
|
||||
selector = service.interface
|
||||
@@ -772,18 +915,34 @@ def _resolve_service_interface(
|
||||
def _validate_dhcp_server(
|
||||
service: Service,
|
||||
service_location: str,
|
||||
node: Node,
|
||||
interface: Optional[Interface],
|
||||
issues: List[ValidationIssue],
|
||||
) -> None:
|
||||
"""Validate DHCP-server-specific semantics.
|
||||
|
||||
Check address syntax and range ordering independently of the binding, so
|
||||
those diagnostics remain available even when interface resolution fails.
|
||||
With a usable static binding, check subnet membership and reserved subnet
|
||||
addresses, then exclude the server and advertised gateway from the pool.
|
||||
Skip checks whose required address or binding could not be parsed.
|
||||
|
||||
Args:
|
||||
service: DHCP server service being validated.
|
||||
service_location: Logical location of the service.
|
||||
node: Node on which the service runs.
|
||||
interface: Resolved service interface, if available.
|
||||
issues: Mutable list receiving validation failures.
|
||||
service: DHCP server service being validated.
|
||||
service_location: Logical location of the service (where service data came from).
|
||||
interface: Resolved service interface, if available.
|
||||
issues: Mutable list receiving validation failures.
|
||||
|
||||
Example:
|
||||
For a valid static binding and otherwise valid service settings:
|
||||
Server interface: 192.168.1.1/24
|
||||
Advertised gateway: omitted
|
||||
Pool: 192.168.1.100 through 192.168.1.200
|
||||
Result: None; no issues appended
|
||||
|
||||
Changing the pool to 192.168.1.1 through 192.168.1.200 still keeps
|
||||
it inside the subnet, but includes the server's own address.
|
||||
The result remains None, with one server-address exclusion issue
|
||||
appended to the supplied issues list.
|
||||
"""
|
||||
|
||||
if service.interface is ServiceInterfaceSelector.ALL:
|
||||
@@ -801,12 +960,12 @@ def _validate_dhcp_server(
|
||||
)
|
||||
return
|
||||
|
||||
start = _parse_ipv4_address(
|
||||
start = _validate_ipv4_address(
|
||||
service.settings.address_range.start,
|
||||
service_location + ".settings.range.start",
|
||||
issues,
|
||||
)
|
||||
end = _parse_ipv4_address(
|
||||
end = _validate_ipv4_address(
|
||||
service.settings.address_range.end,
|
||||
service_location + ".settings.range.end",
|
||||
issues,
|
||||
@@ -821,7 +980,7 @@ def _validate_dhcp_server(
|
||||
|
||||
gateway: Optional[ipaddress.IPv4Address] = None
|
||||
if service.settings.gateway is not None:
|
||||
gateway = _parse_ipv4_address(
|
||||
gateway = _validate_ipv4_address(
|
||||
service.settings.gateway,
|
||||
service_location + ".settings.gateway",
|
||||
issues,
|
||||
@@ -842,7 +1001,7 @@ def _validate_dhcp_server(
|
||||
# The interface validator reports this as well.
|
||||
return
|
||||
|
||||
parsed_interface = _parse_ipv4_interface(
|
||||
parsed_interface = _validate_ipv4_interface(
|
||||
interface.address,
|
||||
service_location + ".interface",
|
||||
issues,
|
||||
@@ -962,18 +1121,45 @@ def _validate_services(
|
||||
_validate_dhcp_server(
|
||||
service,
|
||||
location,
|
||||
node,
|
||||
interface,
|
||||
issues,
|
||||
)
|
||||
|
||||
|
||||
# ============================ Public interface ============================
|
||||
|
||||
def validate(topology: Topology) -> None:
|
||||
"""Validate the semantic consistency of a parsed topology.
|
||||
|
||||
Args:
|
||||
topology: Parsed topology produced by topology_parser.parse_topology().
|
||||
|
||||
Returns:
|
||||
None when no semantic issues are found.
|
||||
|
||||
Example:
|
||||
Input constructed through the public parser:
|
||||
topology = parse_topology({
|
||||
"nodes": [{"name": "h1", "type": "host"}],
|
||||
})
|
||||
|
||||
Output:
|
||||
result = validate(topology)
|
||||
result is None
|
||||
|
||||
Invalid input:
|
||||
topology = parse_topology({
|
||||
"nodes": [
|
||||
{"name": "h1", "type": "host"},
|
||||
{"name": "h1", "type": "host"},
|
||||
],
|
||||
})
|
||||
|
||||
validate(topology) raises TopologyValidationError instead of returning.
|
||||
The exception's issues tuple contains one ValidationIssue with
|
||||
location="nodes[1].name" and a duplicate-node-name message.
|
||||
When multiple issues are discovered, they share one exception.
|
||||
|
||||
Raises:
|
||||
TopologyValidationError: If one or more semantic consistency errors are
|
||||
discovered.
|
||||
@@ -994,6 +1180,8 @@ def validate(topology: Topology) -> None:
|
||||
raise TopologyValidationError(issues)
|
||||
|
||||
|
||||
# =============================== Self-test ===============================
|
||||
|
||||
def _self_test() -> None:
|
||||
"""Run smoke tests against the public validate() interface."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user