Files
Computer-Networks-II/source/build_network.py
T
2026-09-26 20:36:10 +03:00

416 lines
11 KiB
Python

#!/usr/bin/env python3
"""Coordinate topology loading, validation, planning, rendering, and execution.
The coordinator is the user-facing entry point of the project.
Normal execution follows this pipeline:
topology.json
-> load and parse
-> validate
-> plan
-> render executable Mininet Python
-> write the generated intermediate artifact
-> execute that generated artifact
The generated Python file is intentionally preserved so it can be inspected
independently of the declarative topology source.
Author: Christos Choutouridis <cchoutou@ece.auth.gr>
"""
import argparse
from dataclasses import dataclass
import importlib.util
import os
from pathlib import Path
import shutil
import subprocess
import sys
from typing import Optional, Sequence, Tuple, Union
from topology_loader import TopologyLoadError, load
from topology_parser import TopologyParseError
from topology_plan import ExecutionPlan, TopologyPlanError, plan
from topology_renderer import render_python
from topology_validator import TopologyValidationError, validate
PathLike = Union[str, Path]
class BuildNetworkError(RuntimeError):
"""Raised for coordinator-level generation or execution failures."""
@dataclass(frozen=True)
class EnvironmentRequirement:
"""Describe one external runtime requirement.
Args:
name: Human-readable requirement name.
available: Whether the requirement is available in the current
environment.
detail: Resolved path, explanatory status, or installation hint.
"""
name: str
available: bool
detail: str
@dataclass(frozen=True)
class EnvironmentReport:
"""Contain the result of checking external project dependencies.
Args:
requirements: Individual dependency checks.
"""
requirements: Tuple[EnvironmentRequirement, ...]
@property
def ok(self) -> bool:
"""Return whether every required dependency is available."""
return all(item.available for item in self.requirements)
def _check_python_module(name: str, install_hint: str) -> EnvironmentRequirement:
"""Check whether one Python module is importable.
Args:
name: Importable Python module name.
install_hint: Human-readable installation hint used when missing.
Returns:
EnvironmentRequirement describing the result.
"""
available = importlib.util.find_spec(name) is not None
detail = "Python module available" if available else install_hint
return EnvironmentRequirement(
name="python module %s" % name,
available=available,
detail=detail,
)
def _check_command(
command: str,
purpose: str,
install_hint: str,
) -> EnvironmentRequirement:
"""Check whether one executable is available through PATH.
Args:
command: Executable name to locate.
purpose: Short description of why the executable is required.
install_hint: Human-readable installation hint used when missing.
Returns:
EnvironmentRequirement describing the result.
"""
path = shutil.which(command)
if path is not None:
detail = "%s (%s)" % (path, purpose)
else:
detail = "%s; %s" % (purpose, install_hint)
return EnvironmentRequirement(
name=command,
available=path is not None,
detail=detail,
)
def check_environment() -> EnvironmentReport:
"""Check dependencies required by the builder and assignment experiments.
Returns:
EnvironmentReport containing all required dependency checks.
Notes:
This function performs read-only availability checks.
It does not install packages or modify the system.
"""
requirements = [
_check_python_module(
"mininet",
"install Mininet, for example: sudo apt install mininet",
),
_check_command(
"mn",
"Mininet command-line utilities",
"install Mininet, for example: sudo apt install mininet",
),
_check_command(
"ovs-vsctl",
"Open vSwitch configuration used by Mininet switches",
"install Open vSwitch, for example: sudo apt install openvswitch-switch",
),
_check_command(
"ip",
"interface and route configuration",
"install iproute2: sudo apt install iproute2",
),
_check_command(
"tc",
"traffic-control settings for TCLink",
"install iproute2: sudo apt install iproute2",
),
_check_command(
"sysctl",
"router kernel settings",
"install procps: sudo apt install procps",
),
_check_command(
"dnsmasq",
"DHCP server service",
"install dnsmasq-base: sudo apt install dnsmasq-base",
),
_check_command(
"dhclient",
"manual DHCP client experiment",
"install isc-dhcp-client: sudo apt install isc-dhcp-client",
),
_check_command(
"tcpdump",
"packet capture for DHCP, ARP, and routing experiments",
"install tcpdump: sudo apt install tcpdump",
),
_check_command(
"ping",
"ICMP connectivity experiments",
"install iputils-ping: sudo apt install iputils-ping",
),
]
return EnvironmentReport(requirements=tuple(requirements))
def compile_topology(path: PathLike) -> Tuple[ExecutionPlan, str]:
"""Compile one topology file into a plan and executable Python source.
Args:
path: Path to the declarative topology JSON file.
Returns:
Pair containing the fully resolved ExecutionPlan and rendered Python
source.
Raises:
TopologyLoadError: If the JSON source cannot be loaded.
TopologyParseError: If the JSON violates the topology grammar.
TopologyValidationError: If the parsed topology is semantically
inconsistent.
TopologyPlanError: If the validated topology cannot be lowered into a
complete execution plan.
"""
topology = load(path)
validate(topology)
execution_plan = plan(topology)
source = render_python(execution_plan)
return execution_plan, source
def default_generated_path(topology_path: PathLike) -> Path:
"""Return the default intermediate Python path for a topology source.
Args:
topology_path: Source topology path.
Returns:
Path alongside the topology using the `.generated.py` suffix.
"""
return Path(topology_path).with_suffix(".generated.py")
def write_generated_python(source: str, path: PathLike) -> Path:
"""Write generated executable Python source to disk.
Args:
source: Rendered Python program.
path: Destination path for the generated artifact.
Returns:
Resolved destination Path.
Raises:
BuildNetworkError: If the artifact cannot be written.
"""
destination = Path(path)
try:
destination.write_text(source, encoding="utf-8")
except OSError as exc:
raise BuildNetworkError(
"failed to write generated Python %s: %s" % (destination, exc)
) from exc
return destination
def execute_generated_python(path: PathLike) -> int:
"""Execute one generated Mininet Python artifact interactively.
Args:
path: Generated Python file to execute.
Returns:
Exit status returned by the generated process.
Raises:
BuildNetworkError: If execution is attempted without root privileges
or the generated process cannot be started.
"""
source = Path(path)
if os.geteuid() != 0:
raise BuildNetworkError(
"network execution requires root privileges; rerun with sudo or "
"use --emit to generate the intermediate Python without executing it"
)
try:
completed = subprocess.run(
[sys.executable, str(source)],
check=False,
)
except OSError as exc:
raise BuildNetworkError(
"failed to execute generated Python %s: %s" % (source, exc)
) from exc
return completed.returncode
def _print_environment_report(report: EnvironmentReport) -> None:
"""Print an environment report from the command-line layer.
Args:
report: Environment report to present to the user.
"""
print("Environment check:")
for requirement in report.requirements:
marker = "OK" if requirement.available else "MISSING"
print("[%s] %-16s %s" % (marker, requirement.name, requirement.detail))
if report.ok:
print("Environment OK.")
else:
print("Environment check failed: one or more required dependencies are missing.")
def _build_argument_parser() -> argparse.ArgumentParser:
"""Create the command-line argument parser.
Returns:
Configured ArgumentParser instance.
"""
parser = argparse.ArgumentParser(
description=(
"Load, validate, plan, render, and execute a declarative Mininet "
"topology."
)
)
parser.add_argument(
"topology",
nargs="?",
help="path to topology.json; omitted only with --env-check",
)
parser.add_argument(
"--env-check",
action="store_true",
help=(
"check Mininet, Open vSwitch, dnsmasq, and experiment tools, then exit"
),
)
parser.add_argument(
"--emit",
nargs="?",
const="",
metavar="PATH",
help=(
"generate the intermediate Python and stop; optionally write it to PATH"
),
)
return parser
def main(argv: Optional[Sequence[str]] = None) -> int:
"""Run the coordinator command-line interface.
Args:
argv: Optional argument sequence excluding the executable name.
Returns:
Process exit status.
"""
parser = _build_argument_parser()
args = parser.parse_args(argv)
if args.env_check:
report = check_environment()
_print_environment_report(report)
return 0 if report.ok else 1
if args.topology is None:
parser.error("topology is required unless --env-check is used")
topology_path = Path(args.topology)
if args.emit is None:
generated_path = default_generated_path(topology_path)
execute = True
else:
generated_path = (
default_generated_path(topology_path)
if args.emit == ""
else Path(args.emit)
)
execute = False
try:
_, source = compile_topology(topology_path)
generated_path = write_generated_python(source, generated_path)
print("Generated: %s" % generated_path)
if not execute:
return 0
report = check_environment()
if not report.ok:
_print_environment_report(report)
return 1
return execute_generated_python(generated_path)
except (
TopologyLoadError,
TopologyParseError,
TopologyValidationError,
TopologyPlanError,
BuildNetworkError,
) as exc:
print("error: %s" % exc, file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())