Prepare exersice experiments
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user