#!/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 """ 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_topology from topology_parser import TopologyParseError, parse_topology from topology_plan import ExecutionPlan, TopologyPlanError, plan from topology_renderer import render_python from topology_validator import TopologyValidationError, validate # ============================ Types and errors ============================ 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) # =========================== Environment checks =========================== 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)) # =================== Compilation and artifact execution =================== 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 = parse_topology(load_topology(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: Destination Path, remaining relative if the supplied path is relative. 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 # ================== Command-line reporting and arguments ================== 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 # ======================== Command-line entry point ======================== 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())