From ee4b144929d7574b430f61fc8405e57e855c1608 Mon Sep 17 00:00:00 2001 From: Christos Choutouridis Date: Sat, 26 Sep 2026 20:36:10 +0300 Subject: [PATCH] Prepare exersice experiments --- ... Computer_Networks_II-Programming_task.pdf | Bin Mininet_DSL_Design.md | 885 ++++++++++++++++++ README.md | 477 ++++++++++ exercise/topology.json | 95 ++ source/build_network.py | 415 ++++++++ 5 files changed, 1872 insertions(+) rename Computer Networks II - Programming task.pdf => Computer_Networks_II-Programming_task.pdf (100%) create mode 100644 Mininet_DSL_Design.md create mode 100644 README.md create mode 100644 exercise/topology.json create mode 100644 source/build_network.py diff --git a/Computer Networks II - Programming task.pdf b/Computer_Networks_II-Programming_task.pdf similarity index 100% rename from Computer Networks II - Programming task.pdf rename to Computer_Networks_II-Programming_task.pdf diff --git a/Mininet_DSL_Design.md b/Mininet_DSL_Design.md new file mode 100644 index 0000000..fd95c43 --- /dev/null +++ b/Mininet_DSL_Design.md @@ -0,0 +1,885 @@ +# Mininet Topology JSON Language: Rationale and Reference + +## 1. Purpose and design rationale + +This document explains why the topology language has its current shape, then +serves as a reference for its JSON fields, defaults, and semantic constraints. +The starting point is the assignment: describe hosts, switches, a router, two +subnets, and DHCP configuration in a form that can also express other valid +topologies. + +### 1.1 Describe network intent + +The JSON describes the desired network and its configuration. +A reader should be able to identify participants, connections, addresses, and +services without tracing a sequence of Python or shell commands. +This makes a topology easier to inspect and change independently of the code +that constructs it. + +The design combines top-down modeling with bottom-up implementability. +We start with the concepts needed to describe the network, while requiring each +concept to have a clear translation into Mininet and Linux networking +primitives. +Nodes, interfaces, links, network-stack settings, and processes provide that +translation without requiring a separate abstraction for every assignment step. + +This is why the language distinguishes a router from a host even though both +can use similar underlying construction mechanisms. +Their network roles differ, so the description expresses that distinction. +Conversely, DHCP is a service on a node rather than a new kind of network node. + +### 1.2 Give each property a clear owner + +The network structure leads to five core entities: + +| Entity | Question it answers | +| --- | --- | +| Node | What participates in the network, and what role does it have? | +| Interface | Where does a node attach, and how is that attachment configured? | +| Link | Which two interfaces are connected? | +| Node settings | How should the node's network stack behave? | +| Service | Which service runs on a node, and where is it bound? | + +An interface belongs to its node because addressing, MAC, and MTU describe that +node's attachment to the network. +A link references two interfaces and owns connection properties such as delay +or bandwidth. +Keeping endpoint configuration out of links gives each property one clear +place in the description. + +The same interface structure works for hosts, routers, and switches. +An interface is an attachment point even when it has no IP address, which makes +unaddressed switch ports natural to represent. +Addressing is therefore optional, while MAC and MTU remain independent fields. + +### 1.3 Separate behavior from services + +Forwarding and reverse-path filtering describe network-stack behavior and +belong in node settings. +A DHCP server is a running service with its own configuration, so it belongs in +`services`. +This separation lets a node host a service without changing its node type. + +A service's `node` and `interface` fields describe its placement. +Its `settings` describe its behavior, such as the DHCP address range and +advertised gateway. +Keeping placement outside service-specific settings preserves the same shape +for future service types. + +### 1.4 Make identity explicit where it matters + +Logical interface tags such as `lan` and `wan` express the meaning of a +connection without depending on generated operating-system interface names. +Explicit tags let a topology identify a particular attachment point. +The `auto` selector avoids unnecessary port naming when only connectivity +matters, as is often the case for a switch. + +An omitted interface set allows automatic allocation as needed. +An explicitly declared set defines the available ports, so automatic selection +must stay within that set. +This distinction preserves both convenience and the ability to express a fixed +interface limit; an explicit empty set consequently means zero available ports. + +### 1.5 Describe configuration without recording runtime state + +Static addressing declares a known address and, optionally, a default gateway. +DHCP addressing declares how configuration will be obtained; the acquired +address and lease are runtime results. +The language therefore does not require a DHCP client to contain an address +that is not yet known. + +The same boundary excludes learned ARP entries, packet counters, captures, and +experimental commands from the topology description. +Those observations change as the network runs, while the declared topology +continues to describe its configuration intent. + +### 1.6 Keep the language small and predictable + +The current assignment needs a static default gateway, rather than a general +routing-policy language. +It needs a DHCP service with an address range and an optional advertised +gateway, rather than arbitrary daemon command lines. +The chosen fields cover those requirements while leaving settings and services +as clear places for future extensions. + +Unknown fields are rejected so a misspelled setting cannot silently change the +meaning of a topology. +The reference below defines the supported vocabulary and the constraints that +make declarations consistent. +It proceeds from the entity model through nodes, interfaces, links, and +services, then collects the grammar and semantic rules before a complete +example. + +For setup and usage, see the [README](README.md). +Its [Architecture section](README.md#architecture) describes the Python +processing pipeline, generated code, and execution behavior. + +--- + +## 2. High-level entity model + +The current grammar is: + +```text +Network +├── Nodes +│ ├── name +│ ├── type +│ ├── interfaces +│ └── settings +│ +├── Links +│ ├── name? +│ ├── endpoints +│ │ ├── node +│ │ └── interface +│ └── settings +│ +└── Services + ├── type + ├── node + ├── interface + └── settings +``` + +The actual JSON keys are lowercase: `nodes`, `links`, and `services`. +Each top-level list may be omitted and defaults to an empty list. +Node `interfaces` and node/link `settings` are optional. +Service `settings` must contain the fields required by its type. + +The main entities intentionally have different responsibilities. + +```text +Node + identifies a network participant + +Link + connects two network attachment points + +Service + represents a process or daemon running on a node +``` + +Common entities used in these entities are: + +```text +Interface + identifies a network attachment point owned by a node + +Settings + configure the node/link/service or its networking stack +``` + +--- + +## 3. Nodes + +A node represents a logical network participant. + +The current structure is: + +```text +Node +├── name +├── type +├── interfaces +└── settings +``` + +### 3.1 Node name + +`name` is the logical identifier used by the rest of the topology. + +For example: + +```json +{ + "name": "r0", + "type": "router" +} +``` + +Node names are expected to be unique. + +Duplicate node names are invalid. + +### 3.2 Node type + +The currently supported node types are: + +```text +host +switch +router +``` + +A `host` is an end system. +A `switch` is an autonomous Layer-2 learning switch. +A `router` connects IP networks and supports forwarding and reverse-path-filter +settings. +Selecting `router` does not implicitly enable forwarding. + +### 3.3 Node settings + +`settings` contains configuration that changes the behavior of the node itself. + +For a router, the currently supported settings are: + +```json +"settings": { + "ip_forward": true, + "rp_filter": false +} +``` + +These values configure the networking behavior of the node namespace. +Omitted router settings leave the corresponding behavior unspecified. +Hosts and switches currently accept only empty settings objects. + +They are not modeled as services because they do not represent independent +user-space processes. + +The distinction is: + +```text +Node settings + -> properties of the node or its networking stack + +Services + -> processes or daemons running on the node +``` + +Node settings are type-specific. + +Unknown settings for the selected node type are invalid. + +--- + +## 4. Interfaces + +Interfaces belong to nodes. + +This is a deliberate modeling decision. + +```text +Node owns Interfaces +Link connects Interfaces +``` + +An interface is broader than an object that merely owns an IP address. + +It is a network attachment point with optional Layer-3 configuration and +optional interface-level properties. + +Conceptually: + +```text +Interface += +network attachment point ++ optional L3 configuration ++ optional interface properties +``` + +A host or router interface will commonly have Layer-3 addressing. + +A Layer-2 switch port normally will not. + +Using one interface abstraction for all node types keeps link endpoints +uniform. + +### 4.1 Logical interface tags + +Interfaces are declared as members of the `interfaces` object. + +The JSON key is the logical interface tag. + +For example: + +```json +"interfaces": { + "lan": { + "addressing": "static", + "address": "192.168.1.1/24" + }, + "wan": { + "addressing": "static", + "address": "192.168.2.1/24" + } +} +``` + +`lan` and `wan` are topology-level identifiers. + +They are not required to be the real Linux interface names. + +Logical tags identify interfaces within the JSON independently of generated +operating-system interface names. + +The same mechanism may be used for meaningful switch-port names. + +For example: + +```json +"interfaces": { + "client1": {}, + "client2": {}, + "server": {}, + "uplink": {} +} +``` + +### 4.2 Interface properties + +The current interface structure is flat. + +There is no nested addressing object. + +The currently modeled properties are: + +```text +addressing? static | dhcp | none +address? required with static addressing +gateway? optional with static addressing +mac? optional interface property +mtu? optional interface property +``` + +Possible future interface properties can be added at the same level. + +### 4.3 Addressing semantics + +The supported addressing values are: + +```text +static +dhcp +none +``` + +The `addressing` field is optional. + +Omitting it is semantically equivalent to: + +```json +"addressing": "none" +``` + +Therefore these two declarations mean the same thing: + +```json +"p1": {} +``` + +```json +"p1": { + "addressing": "none" +} +``` + +Both mean that no Layer-3 addressing configuration is requested. + +#### Static addressing + +A static interface is written as: + +```json +"net0": { + "addressing": "static", + "address": "192.168.2.10/24", + "gateway": "192.168.2.1" +} +``` + +`address` is required. + +`gateway` is optional. + +The gateway is kept with static interface configuration because this matches +the simple configuration model used by traditional Linux network +configuration. + +For the scope of this assignment, this is simpler than introducing a general +routing language. + +#### DHCP addressing + +A DHCP interface is written as: + +```json +"net0": { + "addressing": "dhcp" +} +``` + +A DHCP interface does not contain a predetermined address or gateway. + +Those values are runtime configuration learned from the DHCP server. + +The resulting distinction is: + +```text +static + address and optional gateway are configured locally + +dhcp + address and gateway are learned dynamically + +none / omitted + no L3 configuration is requested +``` + +### 4.4 Switch interfaces + +Switch ports use the same interface abstraction. + +For example, a switch with a fixed set of four logical ports may be written as: + +```json +{ + "name": "s1", + "type": "switch", + "interfaces": { + "p1": {}, + "p2": {}, + "p3": {}, + "uplink": {} + } +} +``` + +This is useful when link placement should refer to specific switch ports. + +If exact port identity does not matter, the `interfaces` field may be omitted +and links may use automatic interface allocation. + +### 4.5 Omitted interfaces versus an empty interface set + +The distinction between these two forms is intentional. + +```json +{ + "name": "s1", + "type": "switch" +} +``` + +means: + +```text +interface set omitted +-> dynamic interface set +``` + +while: + +```json +{ + "name": "s1", + "type": "switch", + "interfaces": {} +} +``` + +means: + +```text +interface set explicitly declared +-> fixed interface set containing zero interfaces +``` + +This distinction is important for the semantics of `auto`. + +### 4.6 Connected and unused interfaces + +An interface requesting addressing, an address, a gateway, a MAC address, or an +MTU must be attached to a link. +Unused empty interface declarations may remain in a fixed interface pool. +An interface used by a service must also be attached to a link. + +--- + +## 5. Links + +A link is a first-class entity connecting exactly two endpoints. + +The current structure is: + +```text +Link +├── name? +├── endpoints +│ ├── endpoint A +│ │ ├── node +│ │ └── interface +│ └── endpoint B +│ ├── node +│ └── interface +└── settings +``` + +Keeping the `endpoints` wrapper is intentional. + +A link may carry properties other than its endpoints, so flattening the link +into a raw pair would make future link-level configuration awkward. + +### 5.1 Explicit interface references + +An endpoint may reference an explicitly declared interface. + +For example: + +```json +{ + "node": "r0", + "interface": "lan" +} +``` + +The referenced interface tag must exist on that node. + +### 5.2 Automatic interface allocation + +An endpoint may instead use: + +```json +{ + "node": "s1", + "interface": "auto" +} +``` + +The meaning of `auto` depends on whether the node declared an interface set. + +If `interfaces` is omitted: + +```text +dynamic interface set +-> auto may allocate/create interfaces as needed +``` + +If `interfaces` is explicitly declared: + +```text +fixed interface set +-> auto selects an unused declared interface +-> auto must not silently create an additional interface +``` + +For example: + +```json +"interfaces": { + "p1": {}, + "p2": {}, + "p3": {}, + "uplink": {} +} +``` + +allows an explicit link to select: + +```json +"interface": "uplink" +``` + +or an automatic link to select one of the remaining unused ports. + +Explicit references reserve their interfaces before `auto` selections are made. +Each `auto` selection consumes the first remaining declared interface in +interface declaration order, following link and endpoint order. +If the fixed interface pool is exhausted, the topology is invalid. + +### 5.3 Why interfaces remain under nodes + +Interfaces remain under nodes because they may carry or receive: + +- addressing configuration, +- MAC configuration, +- MTU configuration, +- service bindings, +- future interface-specific properties. + +The selected relationship therefore remains: + +```text +Node owns Interface +Link references Interface +``` + +### 5.4 Link settings + +Links contain a `settings` object for provisioning properties. + +The supported provisioning fields are: + +```text +bandwidth_mbps? +delay_ms? +loss_percent? +jitter_ms? +``` + +Example: + +```json +{ + "endpoints": [ + { + "node": "h1", + "interface": "net0" + }, + { + "node": "s1", + "interface": "auto" + } + ], + "settings": { + "bandwidth_mbps": 100, + "delay_ms": 1.5, + "loss_percent": 0.1 + } +} +``` + +Bandwidth is expressed in megabits per second, delay and jitter in milliseconds, +and loss as a percentage. + +The allowed ranges are: + +```text +bandwidth_mbps > 0 +delay_ms >= 0 +jitter_ms >= 0 +0 <= loss_percent <= 100 +``` + +--- + +## 6. Services + +A service represents a process or daemon running on a topology node. + +The generic structure is: + +```text +Service +├── type +├── node +├── interface +└── settings +``` + +The currently supported service type is: + +```text +dhcp-server +``` + +Example: + +```json +{ + "type": "dhcp-server", + "node": "r0", + "interface": "lan", + "settings": { + "range": { + "start": "192.168.1.100", + "end": "192.168.1.200" + }, + "gateway": "192.168.1.1" + } +} +``` + +### 6.1 Service binding + +`node` and `interface` describe where the service runs and where it is bound. + +They therefore remain outside `settings`. + +The service interface grammar reserves: + +```text + +auto +all +``` + +The validity and meaning of special selectors are service-type-specific. + +A DHCP service accepts a specific declared interface tag. + +The service also supports `auto` only when exactly one suitable statically +addressed interface can be selected unambiguously from the declared set. + +The DHCP server does not support `all`. + +### 6.2 DHCP settings + +The current DHCP server settings are: + +```text +range.start +range.end +gateway? +``` + +The service `gateway` is the gateway advertised to DHCP clients. + +This is distinct from a static interface gateway. + +```text +static interface gateway + -> locally configured default gateway + +DHCP service gateway + -> gateway distributed to DHCP clients +``` + +--- + +## 7. Grammar summary + +### Node + +```text +name + required unique logical node identifier + +type + host | switch | router + +interfaces + optional map of logical interface tags to Interface objects + +settings + optional node-type-specific configuration +``` + +### Interface + +```text + + logical interface identifier + +addressing + optional: static | dhcp | none + omission is equivalent to none + +address + required when addressing = static + forbidden otherwise + +gateway + optional when addressing = static + forbidden otherwise + +mac + optional + +mtu + optional +``` + +### Link + +```text +name + optional logical link name + +endpoints + exactly two endpoints + +endpoint.node + node name + +endpoint.interface + declared interface tag | auto + +settings.bandwidth_mbps + optional + +settings.delay_ms + optional + +settings.loss_percent + optional + +settings.jitter_ms + optional +``` + +### Service + +```text +type + currently dhcp-server + +node + node on which the service runs + +interface + interface tag | auto | all + validity depends on service type + +settings + service-type-specific configuration +``` + +--- + +## 8. Strict grammar + +Unknown fields and unsupported tokens are invalid. +For example, `ip_foward` is not an alias for `ip_forward`, and `bananas` is not +an addressing mode. + +Field names, value types, and closed token sets define the JSON grammar. +Cross-references and network constraints define semantic validity. +A recognized `static` addressing mode without an address is therefore a +semantic error, as is a link referring to an unknown node. + +Only the fields described in this reference are currently supported. +Additional node types, services, or settings require a language extension. + +--- + +## 9. Semantic constraints + +A topology must satisfy the following constraints in addition to the JSON +grammar. +Invalid configurations include: + +- duplicate node names, +- duplicate explicit link names, +- empty logical names, +- reserved interface tags, +- static interfaces without an address, +- invalid IPv4/CIDR syntax, +- invalid gateway syntax, +- gateways outside the interface subnet, +- addresses or gateways used with DHCP or no addressing, +- invalid MAC syntax, +- invalid MTU values, +- explicit link references to unknown nodes, +- explicit link references to unknown interfaces, +- explicit interface reuse across links, +- fixed interface-pool exhaustion caused by `auto`, +- invalid link provisioning ranges, +- services referencing unknown nodes, +- services referencing unknown interfaces, +- DHCP services bound to interfaces without static addressing, +- invalid DHCP IPv4 ranges, +- reversed DHCP ranges, +- DHCP range values outside the bound interface subnet, +- DHCP network or broadcast addresses, +- DHCP pools that include the server address, +- DHCP pools that include the advertised gateway. + +Switch interfaces may not request Layer-3 addressing. +Only one static default gateway is supported per node. +Configured interfaces and service bindings must be connected as described in +[Connected and unused interfaces](#46-connected-and-unused-interfaces). + diff --git a/README.md b/README.md new file mode 100644 index 0000000..f93ead0 --- /dev/null +++ b/README.md @@ -0,0 +1,477 @@ +# Computer Networks II — Declarative Mininet Topology Builder + +This repository implements the programming assignment for Computer Networks II +using a small declarative JSON topology language and a staged Python build +pipeline. + +The JSON describes the desired network rather than the exact Mininet commands +used to create it. + +The implementation loads that description, parses it into typed Python objects, +validates its semantics, resolves all abstract interface selections into a +concrete execution plan, renders executable Mininet Python, and then runs that +generated artifact. + +Start with [Quick start](#quick-start) to generate and run a topology. +The [topology language overview](#topology-language) introduces the JSON format; +the [JSON language reference](Mininet_DSL_Design.md) +defines its fields, defaults, and semantic constraints. + +## Project goals + +The main goals of the implementation are: + +- describe Mininet topologies declaratively in JSON, +- keep topology intent separate from Mininet implementation details, +- use typed Python objects throughout the processing pipeline, +- detect grammar and semantic errors before changing the network, +- generate an inspectable intermediate Python artifact, +- execute exactly that generated artifact for normal runs, +- keep runtime experiments separate from topology construction, +- clean up services and Mininet state safely after the CLI exits. + +The topology language is intentionally small. + +Its core semantic entities are: + +```text +Node +Interface +Link +Node Settings +Service +``` + +## Repository layout + +```text +source/ + build_network.py CLI coordinator and environment checks + topology_loader.py JSON loading + topology_parser.py Strict parsing and typed semantic model + topology_validator.py Semantic and cross-reference checks + topology_plan.py Fully resolved execution plan + topology_renderer.py Executable Mininet Python generation +report/ Assignment report workspace +exercise/ Assignment execution files +Mininet_DSL_Design.md Mininet JSON domain specific language rules and semantics +README.md Repository's main documentation file +``` + +Save a topology source as `topology.json`. +Normal execution writes `topology.generated.py` beside that source file. + +## Requirements + +The implementation targets a Linux environment capable of running Mininet. + +The current runtime and experiment dependencies are: + +```text +Python Mininet module +mn +ovs-vsctl +ip +tc +sysctl +dnsmasq +dhclient +tcpdump +ping +``` + +On Ubuntu, the corresponding packages are typically provided by: + +```bash +sudo apt install \ + mininet \ + openvswitch-switch \ + iproute2 \ + procps \ + dnsmasq-base \ + isc-dhcp-client \ + tcpdump \ + iputils-ping +``` + +The exact environment can be checked without modifying the system: + +```bash +python3 source/build_network.py --env-check +``` + +The check reports each required module or executable as `OK` or `MISSING`. + +## Quick start + +Run these commands from the repository root. +Save the [complete assignment example](#complete-assignment-topology-example) +as `topology.json` first, or use your own topology file. + +### 1. Check the environment + +Run: + +```bash +python3 source/build_network.py --env-check +``` + +### 2. Generate the intermediate Python only + +Run: + +```bash +python3 source/build_network.py topology.json --emit +``` + +This writes: + +```text +topology.generated.py +``` + +and stops without creating a Mininet network. + +A custom output path can be supplied: + +```bash +python3 source/build_network.py topology.json --emit gen-test.py +``` + +Generation does not require root privileges. + +### 3. Generate and execute + +Run: + +```bash +sudo python3 source/build_network.py topology.json +``` + +The coordinator: + +```text +loads +parses +validates +plans +renders +writes topology.generated.py +executes topology.generated.py +``` + +Network execution requires root privileges because Mininet creates namespaces, +virtual interfaces, Open vSwitch state, and other kernel networking objects. + +### 4. Exit the Mininet CLI + +When finished, leave the CLI with: + +```text +mininet> exit +``` + +The generated script then stops the service processes it started and calls +`net.stop()`. + +## Topology language + +The JSON describes `nodes`, their `interfaces`, `links`, and `services`. +Nodes may be hosts, Layer-2 switches, or routers; interfaces use static, DHCP, +or no addressing. +Links connect interfaces, and services describe processes such as a DHCP server. + +Use the [JSON language reference](Mininet_DSL_Design.md) for field +definitions, defaults, interface selection rules, and semantic constraints. +The following example provides a complete starting point for the assignment. + +## Complete assignment topology example + +The example [here](exercise/topology.json) describes the required two-subnet topology. + +## Configuration versus runtime state + +The topology source describes configuration intent. + +It does not record all runtime state. + +Examples include: + +```text +JSON declaration Runtime result + +static address -----> configured IPv4 address +static gateway -----> configured default route +DHCP addressing -----> dynamically learned address +DHCP service settings -----> dynamically advertised options +links -----> Mininet/veth interfaces +services -----> running processes +``` + +DHCP leases, ARP entries, packet counters, and packet captures are runtime +state. + +They do not belong in the topology grammar. + +## Runtime experiments + +After the generated topology enters the Mininet CLI, the assignment experiments +can be performed with tools such as: + +```text +dhclient +tcpdump +ip neigh +ping +``` + +These commands operate on the running network and may create transient runtime +state. + +They do not modify the declarative topology source. + + +### DHCP capture preparation + +DHCP clients are intentionally not started by the generated network. + +The assignment requires packet capture to begin before the DHCP exchange is +triggered manually. + +A clean client-side DHCP experiment can use a temporary lease and PID file: + +```text +mininet> h1 rm -f /tmp/h1-dhclient.leases /tmp/h1-dhclient.pid +mininet> h1 dhclient -v \ + -lf /tmp/h1-dhclient.leases \ + -pf /tmp/h1-dhclient.pid \ + h1-eth0 +``` + +A successful exchange should show the normal DORA sequence: + +```text +DHCPDISCOVER +DHCPOFFER +DHCPREQUEST +DHCPACK +``` + +The DHCP server lease file generated by this project is reset before the +service starts, so stale server-side leases from earlier topology runs do not +affect a new experiment. + +## Architecture + +The implementation follows a compiler-like pipeline: + +```text +topology.json + → load → parse → validate → plan → render + → write generated Python → execute → Mininet CLI → cleanup +``` + +The language describes network concepts that map to Mininet and Linux +primitives: nodes, interfaces, links, network-stack settings, and services. +Each stage has a separate responsibility so it can be tested independently. +The generated Python is the only executable Mininet backend. + +### Loading and parsing + +`topology_loader.py` provides `load_topology(path)`, which reads a JSON file into +a raw dictionary. +`topology_parser.py` provides `parse_topology(raw)`, which converts that dictionary +into a typed `Topology`. +The loader's convenience API, `load(path)`, performs both operations. +Neither stage depends on Mininet. + +The semantic model uses dataclasses for `Topology`, `Node`, `Interface`, `Link`, +`LinkEndpoint`, `Service`, and their structured settings. +Enums represent node types, addressing modes, service types, and selectors. +Interfaces and settings are composed into their owning entities. + +Nodes, links, and services remain ordered lists during parsing. +Keeping nodes in a list preserves duplicate declarations for diagnostics; +converting them immediately to a name-indexed dictionary could hide duplicates. +`Node.interfaces` uses `Optional[Dict[str, Interface]]`: `None` preserves an +omitted interface set, while `{}` preserves an explicitly empty set. + +### Validation + +`topology_validator.py` exposes `validate(topology)`. +It checks semantic and cross-reference constraints after parsing has checked +field names, representation, and value types. +See the [language reference](Mininet_DSL_Design.md) for those rules. + +The validator collects `ValidationIssue(location, message)` objects and raises +one `TopologyValidationError` containing all issues discovered in the pass. +It does not print diagnostics or create Mininet objects. +Validation checks whether automatic interface allocation is possible; +planning performs the concrete allocation. + +### Planning + +`topology_plan.py` exposes `plan(topology)` and produces an immutable +`ExecutionPlan` from a validated topology. +It does not import Mininet, execute commands, or render Python. + +The plan contains ordered tuples of node, link, interface, sysctl, and service +operations. +Its types include `PlannedNode`, `PlannedLinkEndpoint`, `PlannedLink`, +`PlannedLinkSettings`, `PlannedInterface`, `PlannedSysctl`, `PlannedService`, and +`PlannedDHCPServerSettings`. +No unresolved `auto` or `all` selectors remain. + +The planner reserves all explicit link-interface references before allocating +`auto` selections from fixed pools in declaration order. +Dynamic pools receive synthetic logical identifiers `auto0`, `auto1`, and so on. +Concrete interface names follow `-eth`, starting at zero per node in +link and endpoint traversal order. +For example, `(r0, lan)` may resolve to `r0-eth0`. + +Mininet creates interfaces through links. +Planning therefore rejects configured interfaces or service bindings that have +no corresponding link, while allowing unused empty interface declarations. + +Switches are planned with Open vSwitch `failMode="standalone"` to operate as +Layer-2 learning switches without an OpenFlow controller. +Hosts and routers are created with `net.addHost(..., ip=None)`. +Router settings generate sysctls only when explicitly supplied. +`rp_filter` operations cover `all`, `default`, and each materialized router +interface inside the router namespace. + +### Rendering and runtime lifecycle + +`topology_renderer.py` exposes `render_python(plan)`. +It serializes the resolved plan without allocating interfaces or validating +language semantics. +The resulting standalone Python program is preserved for inspection. + +The generated program: + +1. Creates nodes and links, then builds and starts Mininet. +2. Configures interfaces, static default routes, and namespace-scoped sysctls. +3. Starts services and enters the Mininet CLI. +4. Terminates tracked service processes and calls `net.stop()` in a `finally` + block. + +When a link has traffic-control settings, the renderer emits `TCLink`. +Bandwidth maps to `bw`, loss to `loss`, and delay and jitter to millisecond +strings. +For example: + +```python +net.addLink( + nodes["r0"], + nodes["s1"], + intfName1="r0-eth0", + intfName2="s1-eth1", + cls=TCLink, + delay="1.5ms", +) +``` + +Interface configuration uses Linux commands such as `ip addr add`, +`ip route replace`, and `sysctl -w` inside the appropriate node namespace. + +DHCP services run through `dnsmasq`, with DNS disabled and a 12-hour lease time. +These are backend choices, not configurable JSON fields. +Each service uses indexed lease, PID, and log paths under `/tmp`. +The generated script removes its previous lease and PID files before startup +and tracks the background PID for cleanup. +DHCP clients are started manually after packet capture begins, as described in +[Runtime experiments](#runtime-experiments). + +### Coordination and error reporting + +`build_network.py` coordinates the stages through `compile_topology(path)`, +which returns the execution plan and rendered source. +The coordinator writes the generated artifact beside the input using the +`.generated.py` suffix unless `--emit PATH` selects a different destination. + +`--emit` stops after writing, without requiring Mininet or root privileges. +Normal execution writes the same artifact, checks dependencies, and runs it +with the coordinator's Python interpreter. +Network execution requires root privileges. +`--env-check` reports dependencies and exits without compiling a topology. +See [Quick start](#quick-start) for commands. + +Core stages raise typed exceptions; the command-line layer reports them. + +| Stage | Exception | +| --- | --- | +| Loading | `TopologyLoadError` | +| Parsing | `TopologyParseError` | +| Validation | `TopologyValidationError` | +| Planning | `TopologyPlanError` | +| Artifact writing or process launch | `BuildNetworkError` | + +The coordinator returns a failing status for reported errors and propagates the +exit status of the generated program. + +## Self-tests + +The five core pipeline modules contain lightweight `__main__` smoke tests. +The coordinator's `__main__` invokes its command-line interface instead. + +They exercise each module's public interface. + +They can be run directly: + +```bash +python3 source/topology_loader.py +python3 source/topology_parser.py +python3 source/topology_validator.py +python3 source/topology_plan.py +python3 source/topology_renderer.py +``` + +These smoke tests do not require Mininet, root privileges, or network changes. + +The environment can be checked separately with: + +```bash +python3 source/build_network.py --env-check +``` + +A generated topology can then be tested in Mininet with: + +```bash +sudo python3 source/build_network.py topology.json +``` + +## Current scope and extension points + +The current language supports: + +- hosts, +- Layer-2 learning switches, +- routers, +- static IPv4 addressing, +- DHCP client intent, +- one optional static default gateway per node, +- logical interface tags, +- automatic interface allocation, +- link bandwidth, delay, loss, and jitter, +- DHCP server services, +- router forwarding and reverse-path-filter settings. + +Extending the language requires corresponding parser, validator, planner, and +renderer support. +Potential extensions include: + +- additional node types, +- additional services, +- richer interface properties, +- richer link behavior, +- more general routing constructs. + +The project currently supports IPv4 network configuration for the assignment. + +## Further documentation + +- [JSON language reference](Mininet_DSL_Design.md): + complete field definitions, defaults, and semantic constraints. +- [Assignment brief](Computer_Networks_II-Programming_task.pdf): + required topology, experiments, and deliverables. + diff --git a/exercise/topology.json b/exercise/topology.json new file mode 100644 index 0000000..516d135 --- /dev/null +++ b/exercise/topology.json @@ -0,0 +1,95 @@ +{ + "nodes": [ { + "name": "h1", + "type": "host", + "interfaces": { + "net0": { + "addressing": "dhcp" + } + } + }, { + "name": "h2", + "type": "host", + "interfaces": { + "net0": { + "addressing": "dhcp" + } + } + }, { + "name": "h3", + "type": "host", + "interfaces": { + "net0": { + "addressing": "static", + "address": "192.168.2.10/24", + "gateway": "192.168.2.1" + } + } + }, { + "name": "s1", + "type": "switch" + }, { + "name": "s2", + "type": "switch" + }, { + "name": "r0", + "type": "router", + "interfaces": { + "netA": { + "addressing": "static", + "address": "192.168.1.1/24" + }, + "netB": { + "addressing": "static", + "address": "192.168.2.1/24" + } + }, + "settings": { + "ip_forward": true, + "rp_filter": false + } + } + ], + + "links": [ { + "endpoints": [ + { "node": "h1", "interface": "net0" }, + { "node": "s1", "interface": "auto" } + ] + }, { + "endpoints": [ + { "node": "h2", "interface": "net0" }, + { "node": "s1", "interface": "auto" } + ] + }, { + "endpoints": [ + { "node": "h3", "interface": "net0" }, + { "node": "s2", "interface": "auto" } + ] + }, { + "endpoints": [ + { "node": "r0", "interface": "netA" }, + { "node": "s1", "interface": "auto" } + ] + }, { + "endpoints": [ + { "node": "r0", "interface": "netB" }, + { "node": "s2", "interface": "auto" } + ] + } + ], + + "services": [ { + "type": "dhcp-server", + "node": "r0", + "interface": "netA", + "settings": { + "range": { + "start": "192.168.1.100", + "end": "192.168.1.200" + }, + "gateway": "192.168.1.1" + } + } + ] +} \ No newline at end of file diff --git a/source/build_network.py b/source/build_network.py new file mode 100644 index 0000000..34e105c --- /dev/null +++ b/source/build_network.py @@ -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 +""" + +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())