Prepare exersice experiments
This commit is contained in:
@@ -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
|
||||
<tag>
|
||||
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
|
||||
<tag>
|
||||
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).
|
||||
|
||||
@@ -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 `<node>-eth<N>`, 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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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