Compare commits
12
Commits
eb5dc0afea
...
v1.0rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29ae341cdc | ||
|
|
839dc03f84 | ||
|
|
ad8414367d | ||
|
|
e1e164bd65 | ||
|
|
0e43b943c4 | ||
|
|
46ce29ade7 | ||
|
|
f9721224c9 | ||
|
|
ee4b144929 | ||
|
|
89c151c720 | ||
|
|
92a322eedb | ||
|
|
46f814effc | ||
|
|
7b6acb84e4 |
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "report/AUThReport"]
|
||||||
|
path = report/AUThReport
|
||||||
|
url = ssh://git@git.hoo2.net:222/hoo2/AUThReport.git
|
||||||
@@ -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,476 @@
|
|||||||
|
# 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`.
|
||||||
|
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,91 @@
|
|||||||
|
10:58:29.655251 12:07:f8:bf:6e:ff > ff:ff:ff:ff:ff:ff, ethertype IPv4 (0x0800), length 342: (tos 0x10, ttl 128, id 0, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
0.0.0.0.68 > 255.255.255.255.67: [udp sum ok] BOOTP/DHCP, Request from 12:07:f8:bf:6e:ff, length 300, xid 0xecb5a2e, Flags [none] (0x0000)
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: Discover
|
||||||
|
Hostname Option 12, length 10: "mininet-vm"
|
||||||
|
Parameter-Request Option 55, length 13:
|
||||||
|
Subnet-Mask, BR, Time-Zone, Default-Gateway
|
||||||
|
Domain-Name, Domain-Name-Server, Option 119, Hostname
|
||||||
|
Netbios-Name-Server, Netbios-Scope, MTU, Classless-Static-Route
|
||||||
|
NTP
|
||||||
|
10:58:29.655633 82:3b:16:e9:bb:17 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.131 tell 192.168.1.1, length 28
|
||||||
|
10:58:30.662723 82:3b:16:e9:bb:17 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.131 tell 192.168.1.1, length 28
|
||||||
|
10:58:31.687064 82:3b:16:e9:bb:17 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.131 tell 192.168.1.1, length 28
|
||||||
|
10:58:32.214530 12:07:f8:bf:6e:ff > ff:ff:ff:ff:ff:ff, ethertype IPv4 (0x0800), length 342: (tos 0x10, ttl 128, id 0, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
0.0.0.0.68 > 255.255.255.255.67: [udp sum ok] BOOTP/DHCP, Request from 12:07:f8:bf:6e:ff, length 300, xid 0xecb5a2e, secs 3, Flags [none] (0x0000)
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: Discover
|
||||||
|
Hostname Option 12, length 10: "mininet-vm"
|
||||||
|
Parameter-Request Option 55, length 13:
|
||||||
|
Subnet-Mask, BR, Time-Zone, Default-Gateway
|
||||||
|
Domain-Name, Domain-Name-Server, Option 119, Hostname
|
||||||
|
Netbios-Name-Server, Netbios-Scope, MTU, Classless-Static-Route
|
||||||
|
NTP
|
||||||
|
10:58:32.670822 82:3b:16:e9:bb:17 > 12:07:f8:bf:6e:ff, ethertype IPv4 (0x0800), length 62: (tos 0x0, ttl 64, id 28856, offset 0, flags [DF], proto ICMP (1), length 48)
|
||||||
|
192.168.1.1 > 192.168.1.131: ICMP echo request, id 21591, seq 0, length 28
|
||||||
|
10:58:32.670897 82:3b:16:e9:bb:17 > 12:07:f8:bf:6e:ff, ethertype IPv4 (0x0800), length 342: (tos 0xc0, ttl 64, id 52736, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
192.168.1.1.67 > 192.168.1.131.68: [udp sum ok] BOOTP/DHCP, Reply, length 300, xid 0xecb5a2e, Flags [none] (0x0000)
|
||||||
|
Your-IP 192.168.1.131
|
||||||
|
Server-IP 192.168.1.1
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: Offer
|
||||||
|
Server-ID Option 54, length 4: 192.168.1.1
|
||||||
|
Lease-Time Option 51, length 4: 43200
|
||||||
|
RN Option 58, length 4: 21600
|
||||||
|
RB Option 59, length 4: 37800
|
||||||
|
Subnet-Mask Option 1, length 4: 255.255.255.0
|
||||||
|
BR Option 28, length 4: 192.168.1.255
|
||||||
|
Default-Gateway Option 3, length 4: 192.168.1.1
|
||||||
|
10:58:32.671193 12:07:f8:bf:6e:ff > ff:ff:ff:ff:ff:ff, ethertype IPv4 (0x0800), length 342: (tos 0x10, ttl 128, id 0, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
0.0.0.0.68 > 255.255.255.255.67: [udp sum ok] BOOTP/DHCP, Request from 12:07:f8:bf:6e:ff, length 300, xid 0xecb5a2e, secs 3, Flags [none] (0x0000)
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: Request
|
||||||
|
Server-ID Option 54, length 4: 192.168.1.1
|
||||||
|
Requested-IP Option 50, length 4: 192.168.1.131
|
||||||
|
Hostname Option 12, length 10: "mininet-vm"
|
||||||
|
Parameter-Request Option 55, length 13:
|
||||||
|
Subnet-Mask, BR, Time-Zone, Default-Gateway
|
||||||
|
Domain-Name, Domain-Name-Server, Option 119, Hostname
|
||||||
|
Netbios-Name-Server, Netbios-Scope, MTU, Classless-Static-Route
|
||||||
|
NTP
|
||||||
|
10:58:32.673025 82:3b:16:e9:bb:17 > 12:07:f8:bf:6e:ff, ethertype IPv4 (0x0800), length 342: (tos 0xc0, ttl 64, id 52738, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
192.168.1.1.67 > 192.168.1.131.68: [bad udp cksum 0x851a -> 0x5896!] BOOTP/DHCP, Reply, length 300, xid 0xecb5a2e, secs 3, Flags [none] (0x0000)
|
||||||
|
Your-IP 192.168.1.131
|
||||||
|
Server-IP 192.168.1.1
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: ACK
|
||||||
|
Server-ID Option 54, length 4: 192.168.1.1
|
||||||
|
Lease-Time Option 51, length 4: 43200
|
||||||
|
RN Option 58, length 4: 21600
|
||||||
|
RB Option 59, length 4: 37800
|
||||||
|
Subnet-Mask Option 1, length 4: 255.255.255.0
|
||||||
|
BR Option 28, length 4: 192.168.1.255
|
||||||
|
Hostname Option 12, length 10: "mininet-vm"
|
||||||
|
Default-Gateway Option 3, length 4: 192.168.1.1
|
||||||
|
10:58:32.678132 82:3b:16:e9:bb:17 > 12:07:f8:bf:6e:ff, ethertype IPv4 (0x0800), length 342: (tos 0xc0, ttl 64, id 52737, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
192.168.1.1.67 > 192.168.1.131.68: [udp sum ok] BOOTP/DHCP, Reply, length 300, xid 0xecb5a2e, secs 3, Flags [none] (0x0000)
|
||||||
|
Your-IP 192.168.1.131
|
||||||
|
Server-IP 192.168.1.1
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: Offer
|
||||||
|
Server-ID Option 54, length 4: 192.168.1.1
|
||||||
|
Lease-Time Option 51, length 4: 43200
|
||||||
|
RN Option 58, length 4: 21600
|
||||||
|
RB Option 59, length 4: 37800
|
||||||
|
Subnet-Mask Option 1, length 4: 255.255.255.0
|
||||||
|
BR Option 28, length 4: 192.168.1.255
|
||||||
|
Default-Gateway Option 3, length 4: 192.168.1.1
|
||||||
|
10:58:37.831406 82:3b:16:e9:bb:17 > 12:07:f8:bf:6e:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.131 tell 192.168.1.1, length 28
|
||||||
|
10:58:37.831440 12:07:f8:bf:6e:ff > 82:3b:16:e9:bb:17, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Reply 192.168.1.131 is-at 12:07:f8:bf:6e:ff, length 28
|
||||||
Binary file not shown.
@@ -0,0 +1,109 @@
|
|||||||
|
mininet@mininet-vm:~/Networks-II/exercise$ sudo python3 ../source/build_network.py topology.json
|
||||||
|
Generated: topology.generated.py
|
||||||
|
mininet> h1 rm -f /var/lib/dhcp/dhclient.leases
|
||||||
|
mininet> h1 rm -f /tmp/dhcp.pcap
|
||||||
|
mininet> h1 tcpdump -i h1-eth0 -n -w /tmp/dhcp.pcap &
|
||||||
|
mininet> h1 ps aux | grep tcpdump
|
||||||
|
tcpdump: listening on h1-eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
|
||||||
|
tcpdump 4196 0.0 0.1 10904 5920 pts/2 S+ 10:57 0:00 tcpdump -i h1-eth0 -n -w /tmp/dhcp.pcap
|
||||||
|
root 4199 0.0 0.0 9368 2548 pts/2 S+ 10:58 0:00 grep tcpdump
|
||||||
|
mininet> h1 dhclient h1-eth0
|
||||||
|
mininet> h1 pkill -INT tcpdump
|
||||||
|
12 packets captured
|
||||||
|
12 packets received by filter
|
||||||
|
0 packets dropped by kernel
|
||||||
|
mininet> h1 tcpdump -n -e -vv -r /tmp/dhcp.pcap
|
||||||
|
reading from file /tmp/dhcp.pcap, link-type EN10MB (Ethernet)
|
||||||
|
10:58:29.655251 12:07:f8:bf:6e:ff > ff:ff:ff:ff:ff:ff, ethertype IPv4 (0x0800), length 342: (tos 0x10, ttl 128, id 0, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
0.0.0.0.68 > 255.255.255.255.67: [udp sum ok] BOOTP/DHCP, Request from 12:07:f8:bf:6e:ff, length 300, xid 0xecb5a2e, Flags [none] (0x0000)
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: Discover
|
||||||
|
Hostname Option 12, length 10: "mininet-vm"
|
||||||
|
Parameter-Request Option 55, length 13:
|
||||||
|
Subnet-Mask, BR, Time-Zone, Default-Gateway
|
||||||
|
Domain-Name, Domain-Name-Server, Option 119, Hostname
|
||||||
|
Netbios-Name-Server, Netbios-Scope, MTU, Classless-Static-Route
|
||||||
|
NTP
|
||||||
|
10:58:29.655633 82:3b:16:e9:bb:17 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.131 tell 192.168.1.1, length 28
|
||||||
|
10:58:30.662723 82:3b:16:e9:bb:17 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.131 tell 192.168.1.1, length 28
|
||||||
|
10:58:31.687064 82:3b:16:e9:bb:17 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.131 tell 192.168.1.1, length 28
|
||||||
|
10:58:32.214530 12:07:f8:bf:6e:ff > ff:ff:ff:ff:ff:ff, ethertype IPv4 (0x0800), length 342: (tos 0x10, ttl 128, id 0, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
0.0.0.0.68 > 255.255.255.255.67: [udp sum ok] BOOTP/DHCP, Request from 12:07:f8:bf:6e:ff, length 300, xid 0xecb5a2e, secs 3, Flags [none] (0x0000)
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: Discover
|
||||||
|
Hostname Option 12, length 10: "mininet-vm"
|
||||||
|
Parameter-Request Option 55, length 13:
|
||||||
|
Subnet-Mask, BR, Time-Zone, Default-Gateway
|
||||||
|
Domain-Name, Domain-Name-Server, Option 119, Hostname
|
||||||
|
Netbios-Name-Server, Netbios-Scope, MTU, Classless-Static-Route
|
||||||
|
NTP
|
||||||
|
10:58:32.670822 82:3b:16:e9:bb:17 > 12:07:f8:bf:6e:ff, ethertype IPv4 (0x0800), length 62: (tos 0x0, ttl 64, id 28856, offset 0, flags [DF], proto ICMP (1), length 48)
|
||||||
|
192.168.1.1 > 192.168.1.131: ICMP echo request, id 21591, seq 0, length 28
|
||||||
|
10:58:32.670897 82:3b:16:e9:bb:17 > 12:07:f8:bf:6e:ff, ethertype IPv4 (0x0800), length 342: (tos 0xc0, ttl 64, id 52736, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
192.168.1.1.67 > 192.168.1.131.68: [udp sum ok] BOOTP/DHCP, Reply, length 300, xid 0xecb5a2e, Flags [none] (0x0000)
|
||||||
|
Your-IP 192.168.1.131
|
||||||
|
Server-IP 192.168.1.1
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: Offer
|
||||||
|
Server-ID Option 54, length 4: 192.168.1.1
|
||||||
|
Lease-Time Option 51, length 4: 43200
|
||||||
|
RN Option 58, length 4: 21600
|
||||||
|
RB Option 59, length 4: 37800
|
||||||
|
Subnet-Mask Option 1, length 4: 255.255.255.0
|
||||||
|
BR Option 28, length 4: 192.168.1.255
|
||||||
|
Default-Gateway Option 3, length 4: 192.168.1.1
|
||||||
|
10:58:32.671193 12:07:f8:bf:6e:ff > ff:ff:ff:ff:ff:ff, ethertype IPv4 (0x0800), length 342: (tos 0x10, ttl 128, id 0, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
0.0.0.0.68 > 255.255.255.255.67: [udp sum ok] BOOTP/DHCP, Request from 12:07:f8:bf:6e:ff, length 300, xid 0xecb5a2e, secs 3, Flags [none] (0x0000)
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: Request
|
||||||
|
Server-ID Option 54, length 4: 192.168.1.1
|
||||||
|
Requested-IP Option 50, length 4: 192.168.1.131
|
||||||
|
Hostname Option 12, length 10: "mininet-vm"
|
||||||
|
Parameter-Request Option 55, length 13:
|
||||||
|
Subnet-Mask, BR, Time-Zone, Default-Gateway
|
||||||
|
Domain-Name, Domain-Name-Server, Option 119, Hostname
|
||||||
|
Netbios-Name-Server, Netbios-Scope, MTU, Classless-Static-Route
|
||||||
|
NTP
|
||||||
|
10:58:32.673025 82:3b:16:e9:bb:17 > 12:07:f8:bf:6e:ff, ethertype IPv4 (0x0800), length 342: (tos 0xc0, ttl 64, id 52738, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
192.168.1.1.67 > 192.168.1.131.68: [bad udp cksum 0x851a -> 0x5896!] BOOTP/DHCP, Reply, length 300, xid 0xecb5a2e, secs 3, Flags [none] (0x0000)
|
||||||
|
Your-IP 192.168.1.131
|
||||||
|
Server-IP 192.168.1.1
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: ACK
|
||||||
|
Server-ID Option 54, length 4: 192.168.1.1
|
||||||
|
Lease-Time Option 51, length 4: 43200
|
||||||
|
RN Option 58, length 4: 21600
|
||||||
|
RB Option 59, length 4: 37800
|
||||||
|
Subnet-Mask Option 1, length 4: 255.255.255.0
|
||||||
|
BR Option 28, length 4: 192.168.1.255
|
||||||
|
Hostname Option 12, length 10: "mininet-vm"
|
||||||
|
Default-Gateway Option 3, length 4: 192.168.1.1
|
||||||
|
10:58:32.678132 82:3b:16:e9:bb:17 > 12:07:f8:bf:6e:ff, ethertype IPv4 (0x0800), length 342: (tos 0xc0, ttl 64, id 52737, offset 0, flags [none], proto UDP (17), length 328)
|
||||||
|
192.168.1.1.67 > 192.168.1.131.68: [udp sum ok] BOOTP/DHCP, Reply, length 300, xid 0xecb5a2e, secs 3, Flags [none] (0x0000)
|
||||||
|
Your-IP 192.168.1.131
|
||||||
|
Server-IP 192.168.1.1
|
||||||
|
Client-Ethernet-Address 12:07:f8:bf:6e:ff
|
||||||
|
Vendor-rfc1048 Extensions
|
||||||
|
Magic Cookie 0x63825363
|
||||||
|
DHCP-Message Option 53, length 1: Offer
|
||||||
|
Server-ID Option 54, length 4: 192.168.1.1
|
||||||
|
Lease-Time Option 51, length 4: 43200
|
||||||
|
RN Option 58, length 4: 21600
|
||||||
|
RB Option 59, length 4: 37800
|
||||||
|
Subnet-Mask Option 1, length 4: 255.255.255.0
|
||||||
|
BR Option 28, length 4: 192.168.1.255
|
||||||
|
Default-Gateway Option 3, length 4: 192.168.1.1
|
||||||
|
10:58:37.831406 82:3b:16:e9:bb:17 > 12:07:f8:bf:6e:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.131 tell 192.168.1.1, length 28
|
||||||
|
10:58:37.831440 12:07:f8:bf:6e:ff > 82:3b:16:e9:bb:17, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Reply 192.168.1.131 is-at 12:07:f8:bf:6e:ff, length 28
|
||||||
|
mininet> exit
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
11:24:23.554475 4a:6f:2a:18:f4:80 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.164 tell 192.168.1.131, length 28
|
||||||
|
11:24:23.554730 82:c0:6d:ae:3f:56 > 4a:6f:2a:18:f4:80, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Reply 192.168.1.164 is-at 82:c0:6d:ae:3f:56, length 28
|
||||||
|
11:24:28.679054 82:c0:6d:ae:3f:56 > 4a:6f:2a:18:f4:80, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.131 tell 192.168.1.164, length 28
|
||||||
|
11:24:28.679071 4a:6f:2a:18:f4:80 > 82:c0:6d:ae:3f:56, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Reply 192.168.1.131 is-at 4a:6f:2a:18:f4:80, length 28
|
||||||
Binary file not shown.
@@ -0,0 +1,40 @@
|
|||||||
|
mininet@mininet-vm:~/Networks-II/exercise$ sudo python3 ../source/build_network.py topology.json
|
||||||
|
Generated: topology.generated.py
|
||||||
|
mininet> h1 ip -4 -br addr
|
||||||
|
lo UNKNOWN 127.0.0.1/8
|
||||||
|
mininet> h2 ip -4 -br addr
|
||||||
|
lo UNKNOWN 127.0.0.1/8
|
||||||
|
mininet> h1 dhclient h1-eth0
|
||||||
|
mininet> h2 dhclient h2-eth0
|
||||||
|
mininet> h1 ip -4 -br addr
|
||||||
|
lo UNKNOWN 127.0.0.1/8
|
||||||
|
h1-eth0@if56 UP 192.168.1.131/24
|
||||||
|
mininet> h2 ip -4 -br addr
|
||||||
|
lo UNKNOWN 127.0.0.1/8
|
||||||
|
h2-eth0@if57 UP 192.168.1.164/24
|
||||||
|
mininet> h1 ip neigh flush all
|
||||||
|
mininet> h1 ip neigh show
|
||||||
|
mininet> h1 rm -f /tmp/arp.pcap
|
||||||
|
mininet> h1 tcpdump -i h1-eth0 -n arp -w /tmp/arp.pcap &
|
||||||
|
mininet> h1 ping -c 1 192.168.1.164
|
||||||
|
tcpdump: listening on h1-eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
|
||||||
|
PING 192.168.1.164 (192.168.1.164) 56(84) bytes of data.
|
||||||
|
64 bytes from 192.168.1.164: icmp_seq=1 ttl=64 time=0.425 ms
|
||||||
|
|
||||||
|
--- 192.168.1.164 ping statistics ---
|
||||||
|
1 packets transmitted, 1 received, 0% packet loss, time 0ms
|
||||||
|
rtt min/avg/max/mdev = 0.425/0.425/0.425/0.000 ms
|
||||||
|
mininet> h1 ip neigh show
|
||||||
|
192.168.1.164 dev h1-eth0 lladdr 82:c0:6d:ae:3f:56 REACHABLE
|
||||||
|
mininet> h1 pkill -INT tcpdump
|
||||||
|
4 packets captured
|
||||||
|
4 packets received by filter
|
||||||
|
0 packets dropped by kernel
|
||||||
|
mininet> h1 tcpdump -n -e -vv -r /tmp/arp.pcap
|
||||||
|
reading from file /tmp/arp.pcap, link-type EN10MB (Ethernet)
|
||||||
|
11:24:23.554475 4a:6f:2a:18:f4:80 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.164 tell 192.168.1.131, length 28
|
||||||
|
11:24:23.554730 82:c0:6d:ae:3f:56 > 4a:6f:2a:18:f4:80, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Reply 192.168.1.164 is-at 82:c0:6d:ae:3f:56, length 28
|
||||||
|
11:24:28.679054 82:c0:6d:ae:3f:56 > 4a:6f:2a:18:f4:80, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.131 tell 192.168.1.164, length 28
|
||||||
|
11:24:28.679071 4a:6f:2a:18:f4:80 > 82:c0:6d:ae:3f:56, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Reply 192.168.1.131 is-at 4a:6f:2a:18:f4:80, length 28
|
||||||
|
mininet>
|
||||||
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
mininet>
|
||||||
|
mininet> h1 ip neigh flush all
|
||||||
|
mininet> h3 ip neigh flush all
|
||||||
|
mininet> r0 ip neigh flush all
|
||||||
|
mininet> h1 ip -4 -br addr
|
||||||
|
lo UNKNOWN 127.0.0.1/8
|
||||||
|
h1-eth0@if56 UP 192.168.1.131/24
|
||||||
|
mininet> h1 ip route
|
||||||
|
default via 192.168.1.1 dev h1-eth0
|
||||||
|
192.168.1.0/24 dev h1-eth0 proto kernel scope link src 192.168.1.131
|
||||||
|
mininet> r0 ip -4 -br addr
|
||||||
|
lo UNKNOWN 127.0.0.1/8
|
||||||
|
r0-eth0@if59 UP 192.168.1.1/24
|
||||||
|
r0-eth1@if60 UP 192.168.2.1/24
|
||||||
|
mininet> r0 ip route
|
||||||
|
192.168.1.0/24 dev r0-eth0 proto kernel scope link src 192.168.1.1
|
||||||
|
192.168.2.0/24 dev r0-eth1 proto kernel scope link src 192.168.2.1
|
||||||
|
mininet> h3 ip -4 -br addr
|
||||||
|
lo UNKNOWN 127.0.0.1/8
|
||||||
|
h3-eth0@if58 UP 192.168.2.10/24
|
||||||
|
mininet> h3 ip route
|
||||||
|
default via 192.168.2.1 dev h3-eth0
|
||||||
|
192.168.2.0/24 dev h3-eth0 proto kernel scope link src 192.168.2.10
|
||||||
|
mininet> h1 rm -f /tmp/icmp-A.pcap
|
||||||
|
mininet> h3 rm -f /tmp/icmp-B.pcap
|
||||||
|
mininet> h1 tcpdump -i h1-eth0 -n icmp -w /tmp/icmp-A.pcap &
|
||||||
|
mininet> h3 tcpdump -i h3-eth0 -n icmp -w /tmp/icmp-B.pcap &
|
||||||
|
mininet> h1 ps aux | grep tcpdump
|
||||||
|
tcpdump: listening on h1-eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
|
||||||
|
tcpdump 4640 0.0 0.1 10904 5984 pts/2 S+ 11:41 0:00 tcpdump -i h1-eth0 -n icmp -w /tmp/icmp-A.pcap
|
||||||
|
tcpdump 4642 0.0 0.1 10904 6044 pts/4 S+ 11:41 0:00 tcpdump -i h3-eth0 -n icmp -w /tmp/icmp-B.pcap
|
||||||
|
root 4645 0.0 0.0 9368 2272 pts/2 S+ 11:41 0:00 grep tcpdump
|
||||||
|
mininet> h3 ps aux | grep tcpdump
|
||||||
|
tcpdump: listening on h3-eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
|
||||||
|
tcpdump 4640 0.0 0.1 10904 5984 pts/2 S+ 11:41 0:00 tcpdump -i h1-eth0 -n icmp -w /tmp/icmp-A.pcap
|
||||||
|
tcpdump 4642 0.0 0.1 10904 6044 pts/4 S+ 11:41 0:00 tcpdump -i h3-eth0 -n icmp -w /tmp/icmp-B.pcap
|
||||||
|
root 4648 0.0 0.0 9368 2304 pts/4 S+ 11:41 0:00 grep tcpdump
|
||||||
|
mininet>
|
||||||
|
mininet> h1 ping -c 1 192.168.2.10
|
||||||
|
PING 192.168.2.10 (192.168.2.10) 56(84) bytes of data.
|
||||||
|
64 bytes from 192.168.2.10: icmp_seq=1 ttl=63 time=1.66 ms
|
||||||
|
|
||||||
|
--- 192.168.2.10 ping statistics ---
|
||||||
|
1 packets transmitted, 1 received, 0% packet loss, time 0ms
|
||||||
|
rtt min/avg/max/mdev = 1.659/1.659/1.659/0.000 ms
|
||||||
|
mininet> h1 pkill -INT tcpdump
|
||||||
|
2 packets captured
|
||||||
|
2 packets received by filter
|
||||||
|
0 packets dropped by kernel
|
||||||
|
mininet> h3 pkill -INT tcpdump
|
||||||
|
2 packets captured
|
||||||
|
2 packets received by filter
|
||||||
|
0 packets dropped by kernel
|
||||||
|
mininet> h1 ip link show h1-eth0
|
||||||
|
2: h1-eth0@if56: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
|
||||||
|
link/ether 4a:6f:2a:18:f4:80 brd ff:ff:ff:ff:ff:ff link-netnsid 0
|
||||||
|
mininet> h3 ip link show h3-eth0
|
||||||
|
2: h3-eth0@if58: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
|
||||||
|
link/ether 2e:ef:81:65:2c:c9 brd ff:ff:ff:ff:ff:ff link-netnsid 0
|
||||||
|
mininet> r0 ip link show r0-eth0
|
||||||
|
2: r0-eth0@if59: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
|
||||||
|
link/ether ea:2e:7b:6d:78:82 brd ff:ff:ff:ff:ff:ff link-netnsid 0
|
||||||
|
mininet> r0 ip link show r0-eth1
|
||||||
|
3: r0-eth1@if60: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
|
||||||
|
link/ether 7e:41:8e:cb:65:05 brd ff:ff:ff:ff:ff:ff link-netnsid 0
|
||||||
|
mininet> h1 tcpdump -n -e -vv -r /tmp/icmp-A.pcap
|
||||||
|
reading from file /tmp/icmp-A.pcap, link-type EN10MB (Ethernet)
|
||||||
|
11:42:14.509645 4a:6f:2a:18:f4:80 > ea:2e:7b:6d:78:82, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 33575, offset 0, flags [DF], proto ICMP (1), length 84)
|
||||||
|
192.168.1.131 > 192.168.2.10: ICMP echo request, id 4650, seq 1, length 64
|
||||||
|
11:42:14.510633 ea:2e:7b:6d:78:82 > 4a:6f:2a:18:f4:80, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 63, id 65287, offset 0, flags [none], proto ICMP (1), length 84)
|
||||||
|
192.168.2.10 > 192.168.1.131: ICMP echo reply, id 4650, seq 1, length 64
|
||||||
|
mininet> h3 tcpdump -n -e -vv -r /tmp/icmp-B.pcap
|
||||||
|
reading from file /tmp/icmp-B.pcap, link-type EN10MB (Ethernet)
|
||||||
|
11:42:14.510255 7e:41:8e:cb:65:05 > 2e:ef:81:65:2c:c9, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 63, id 33575, offset 0, flags [DF], proto ICMP (1), length 84)
|
||||||
|
192.168.1.131 > 192.168.2.10: ICMP echo request, id 4650, seq 1, length 64
|
||||||
|
11:42:14.510299 2e:ef:81:65:2c:c9 > 7e:41:8e:cb:65:05, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 65287, offset 0, flags [none], proto ICMP (1), length 84)
|
||||||
|
192.168.2.10 > 192.168.1.131: ICMP echo reply, id 4650, seq 1, length 64
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
11:42:14.509645 4a:6f:2a:18:f4:80 > ea:2e:7b:6d:78:82, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 33575, offset 0, flags [DF], proto ICMP (1), length 84)
|
||||||
|
192.168.1.131 > 192.168.2.10: ICMP echo request, id 4650, seq 1, length 64
|
||||||
|
11:42:14.510633 ea:2e:7b:6d:78:82 > 4a:6f:2a:18:f4:80, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 63, id 65287, offset 0, flags [none], proto ICMP (1), length 84)
|
||||||
|
192.168.2.10 > 192.168.1.131: ICMP echo reply, id 4650, seq 1, length 64
|
||||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
|||||||
|
11:42:14.510255 7e:41:8e:cb:65:05 > 2e:ef:81:65:2c:c9, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 63, id 33575, offset 0, flags [DF], proto ICMP (1), length 84)
|
||||||
|
192.168.1.131 > 192.168.2.10: ICMP echo request, id 4650, seq 1, length 64
|
||||||
|
11:42:14.510299 2e:ef:81:65:2c:c9 > 7e:41:8e:cb:65:05, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 65287, offset 0, flags [none], proto ICMP (1), length 84)
|
||||||
|
192.168.2.10 > 192.168.1.131: ICMP echo reply, id 4650, seq 1, length 64
|
||||||
Binary file not shown.
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generated Mininet topology. Do not edit by hand."""
|
||||||
|
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from mininet.cli import CLI
|
||||||
|
from mininet.link import TCLink
|
||||||
|
from mininet.net import Mininet
|
||||||
|
|
||||||
|
|
||||||
|
def _shell_join(args):
|
||||||
|
"""Quote command arguments for execution inside a Mininet node."""
|
||||||
|
|
||||||
|
return ' '.join(shlex.quote(str(arg)) for arg in args)
|
||||||
|
|
||||||
|
|
||||||
|
def _node_cmd(node, *args):
|
||||||
|
"""Execute one safely quoted command inside a Mininet node."""
|
||||||
|
|
||||||
|
return node.cmd(_shell_join(args))
|
||||||
|
|
||||||
|
|
||||||
|
def _start_background(node, args, log_path):
|
||||||
|
"""Start one background process and return its shell PID."""
|
||||||
|
|
||||||
|
command = _shell_join(args)
|
||||||
|
shell_command = (
|
||||||
|
command
|
||||||
|
+ ' >'
|
||||||
|
+ shlex.quote(log_path)
|
||||||
|
+ ' 2>&1 & echo $!'
|
||||||
|
)
|
||||||
|
return node.cmd('sh -c ' + shlex.quote(shell_command)).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Build, configure, expose, and clean up the planned network."""
|
||||||
|
|
||||||
|
net = Mininet(controller=None, build=False)
|
||||||
|
nodes = {}
|
||||||
|
service_processes = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create nodes.
|
||||||
|
nodes['h1'] = net.addHost('h1', ip=None)
|
||||||
|
nodes['h2'] = net.addHost('h2', ip=None)
|
||||||
|
nodes['h3'] = net.addHost('h3', ip=None)
|
||||||
|
nodes['s1'] = net.addSwitch('s1', failMode='standalone')
|
||||||
|
nodes['s2'] = net.addSwitch('s2', failMode='standalone')
|
||||||
|
nodes['r0'] = net.addHost('r0', ip=None)
|
||||||
|
|
||||||
|
# Create links.
|
||||||
|
net.addLink(
|
||||||
|
nodes['h1'],
|
||||||
|
nodes['s1'],
|
||||||
|
intfName1='h1-eth0',
|
||||||
|
intfName2='s1-eth0',
|
||||||
|
)
|
||||||
|
net.addLink(
|
||||||
|
nodes['h2'],
|
||||||
|
nodes['s1'],
|
||||||
|
intfName1='h2-eth0',
|
||||||
|
intfName2='s1-eth1',
|
||||||
|
)
|
||||||
|
net.addLink(
|
||||||
|
nodes['h3'],
|
||||||
|
nodes['s2'],
|
||||||
|
intfName1='h3-eth0',
|
||||||
|
intfName2='s2-eth0',
|
||||||
|
)
|
||||||
|
net.addLink(
|
||||||
|
nodes['r0'],
|
||||||
|
nodes['s1'],
|
||||||
|
intfName1='r0-eth0',
|
||||||
|
intfName2='s1-eth2',
|
||||||
|
)
|
||||||
|
net.addLink(
|
||||||
|
nodes['r0'],
|
||||||
|
nodes['s2'],
|
||||||
|
intfName1='r0-eth1',
|
||||||
|
intfName2='s2-eth1',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Materialize the Mininet topology.
|
||||||
|
net.build()
|
||||||
|
net.start()
|
||||||
|
|
||||||
|
# Configure concrete interfaces.
|
||||||
|
_node_cmd(nodes['h1'], 'ip', '-4', 'addr', 'flush', 'dev', 'h1-eth0')
|
||||||
|
_node_cmd(nodes['h1'], 'ip', 'link', 'set', 'dev', 'h1-eth0', 'up')
|
||||||
|
_node_cmd(nodes['h2'], 'ip', '-4', 'addr', 'flush', 'dev', 'h2-eth0')
|
||||||
|
_node_cmd(nodes['h2'], 'ip', 'link', 'set', 'dev', 'h2-eth0', 'up')
|
||||||
|
_node_cmd(nodes['h3'], 'ip', '-4', 'addr', 'flush', 'dev', 'h3-eth0')
|
||||||
|
_node_cmd(nodes['h3'], 'ip', 'addr', 'add', '192.168.2.10/24', 'dev', 'h3-eth0')
|
||||||
|
_node_cmd(nodes['h3'], 'ip', 'link', 'set', 'dev', 'h3-eth0', 'up')
|
||||||
|
_node_cmd(nodes['r0'], 'ip', '-4', 'addr', 'flush', 'dev', 'r0-eth0')
|
||||||
|
_node_cmd(nodes['r0'], 'ip', 'addr', 'add', '192.168.1.1/24', 'dev', 'r0-eth0')
|
||||||
|
_node_cmd(nodes['r0'], 'ip', 'link', 'set', 'dev', 'r0-eth0', 'up')
|
||||||
|
_node_cmd(nodes['r0'], 'ip', '-4', 'addr', 'flush', 'dev', 'r0-eth1')
|
||||||
|
_node_cmd(nodes['r0'], 'ip', 'addr', 'add', '192.168.2.1/24', 'dev', 'r0-eth1')
|
||||||
|
_node_cmd(nodes['r0'], 'ip', 'link', 'set', 'dev', 'r0-eth1', 'up')
|
||||||
|
|
||||||
|
# Configure static default routes.
|
||||||
|
_node_cmd(nodes['h3'], 'ip', 'route', 'replace', 'default', 'via', '192.168.2.1', 'dev', 'h3-eth0')
|
||||||
|
|
||||||
|
# Apply node-level networking settings.
|
||||||
|
_node_cmd(nodes['r0'], 'sysctl', '-w', 'net.ipv4.ip_forward=1')
|
||||||
|
_node_cmd(nodes['r0'], 'sysctl', '-w', 'net.ipv4.conf.all.rp_filter=0')
|
||||||
|
_node_cmd(nodes['r0'], 'sysctl', '-w', 'net.ipv4.conf.default.rp_filter=0')
|
||||||
|
_node_cmd(nodes['r0'], 'sysctl', '-w', 'net.ipv4.conf.r0-eth0.rp_filter=0')
|
||||||
|
_node_cmd(nodes['r0'], 'sysctl', '-w', 'net.ipv4.conf.r0-eth1.rp_filter=0')
|
||||||
|
|
||||||
|
# Start planned services.
|
||||||
|
_node_cmd(nodes['r0'], 'rm', '-f', '/tmp/mininet-dnsmasq-0.leases', '/tmp/mininet-dnsmasq-0.pid')
|
||||||
|
pid = _start_background(
|
||||||
|
nodes['r0'],
|
||||||
|
['dnsmasq', '--no-daemon', '--conf-file=', '--port=0', '--bind-interfaces', '--dhcp-authoritative', '--interface=r0-eth0', '--dhcp-range=192.168.1.100,192.168.1.200,12h', '--dhcp-leasefile=/tmp/mininet-dnsmasq-0.leases', '--pid-file=/tmp/mininet-dnsmasq-0.pid', '--dhcp-option=3,192.168.1.1'],
|
||||||
|
'/tmp/mininet-dnsmasq-0.log',
|
||||||
|
)
|
||||||
|
service_processes.append((nodes['r0'], pid))
|
||||||
|
|
||||||
|
# DHCP clients are intentionally not started here.
|
||||||
|
# The assignment captures DHCP before dhclient is run manually.
|
||||||
|
CLI(net)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Stop only the service processes started by this script.
|
||||||
|
for node, pid in reversed(service_processes):
|
||||||
|
if pid:
|
||||||
|
_node_cmd(node, 'kill', pid)
|
||||||
|
|
||||||
|
net.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -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,8 @@
|
|||||||
|
# LaTeX auxiliary files
|
||||||
|
*.aux
|
||||||
|
*.log
|
||||||
|
*.out
|
||||||
|
*.synctex.gz
|
||||||
|
_minted*/*
|
||||||
|
|
||||||
|
|
||||||
Submodule
+1
Submodule report/AUThReport added at 74ec4b5f6c
Binary file not shown.
+1505
File diff suppressed because it is too large
Load Diff
Executable
+425
@@ -0,0 +1,425 @@
|
|||||||
|
#!/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_topology
|
||||||
|
from topology_parser import TopologyParseError, parse_topology
|
||||||
|
from topology_plan import ExecutionPlan, TopologyPlanError, plan
|
||||||
|
from topology_renderer import render_python
|
||||||
|
from topology_validator import TopologyValidationError, validate
|
||||||
|
|
||||||
|
|
||||||
|
# ============================ Types and errors ============================
|
||||||
|
|
||||||
|
PathLike = Union[str, Path]
|
||||||
|
|
||||||
|
|
||||||
|
class BuildNetworkError(RuntimeError):
|
||||||
|
"""Raised for coordinator-level generation or execution failures."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EnvironmentRequirement:
|
||||||
|
"""Describe one external runtime requirement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Human-readable requirement name.
|
||||||
|
available: Whether the requirement is available in the current
|
||||||
|
environment.
|
||||||
|
detail: Resolved path, explanatory status, or installation hint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
available: bool
|
||||||
|
detail: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EnvironmentReport:
|
||||||
|
"""Contain the result of checking external project dependencies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
requirements: Individual dependency checks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
requirements: Tuple[EnvironmentRequirement, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ok(self) -> bool:
|
||||||
|
"""Return whether every required dependency is available."""
|
||||||
|
|
||||||
|
return all(item.available for item in self.requirements)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================== Environment checks ===========================
|
||||||
|
|
||||||
|
def _check_python_module(name: str, install_hint: str) -> EnvironmentRequirement:
|
||||||
|
"""Check whether one Python module is importable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Importable Python module name.
|
||||||
|
install_hint: Human-readable installation hint used when missing.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
EnvironmentRequirement describing the result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
available = importlib.util.find_spec(name) is not None
|
||||||
|
detail = "Python module available" if available else install_hint
|
||||||
|
|
||||||
|
return EnvironmentRequirement(
|
||||||
|
name="python module %s" % name,
|
||||||
|
available=available,
|
||||||
|
detail=detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_command(
|
||||||
|
command: str,
|
||||||
|
purpose: str,
|
||||||
|
install_hint: str,
|
||||||
|
) -> EnvironmentRequirement:
|
||||||
|
"""Check whether one executable is available through PATH.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Executable name to locate.
|
||||||
|
purpose: Short description of why the executable is required.
|
||||||
|
install_hint: Human-readable installation hint used when missing.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
EnvironmentRequirement describing the result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
path = shutil.which(command)
|
||||||
|
|
||||||
|
if path is not None:
|
||||||
|
detail = "%s (%s)" % (path, purpose)
|
||||||
|
else:
|
||||||
|
detail = "%s; %s" % (purpose, install_hint)
|
||||||
|
|
||||||
|
return EnvironmentRequirement(
|
||||||
|
name=command,
|
||||||
|
available=path is not None,
|
||||||
|
detail=detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_environment() -> EnvironmentReport:
|
||||||
|
"""Check dependencies required by the builder and assignment experiments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
EnvironmentReport containing all required dependency checks.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This function performs read-only availability checks.
|
||||||
|
It does not install packages or modify the system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
requirements = [
|
||||||
|
_check_python_module(
|
||||||
|
"mininet",
|
||||||
|
"install Mininet, for example: sudo apt install mininet",
|
||||||
|
),
|
||||||
|
_check_command(
|
||||||
|
"mn",
|
||||||
|
"Mininet command-line utilities",
|
||||||
|
"install Mininet, for example: sudo apt install mininet",
|
||||||
|
),
|
||||||
|
_check_command(
|
||||||
|
"ovs-vsctl",
|
||||||
|
"Open vSwitch configuration used by Mininet switches",
|
||||||
|
"install Open vSwitch, for example: sudo apt install openvswitch-switch",
|
||||||
|
),
|
||||||
|
_check_command(
|
||||||
|
"ip",
|
||||||
|
"interface and route configuration",
|
||||||
|
"install iproute2: sudo apt install iproute2",
|
||||||
|
),
|
||||||
|
_check_command(
|
||||||
|
"tc",
|
||||||
|
"traffic-control settings for TCLink",
|
||||||
|
"install iproute2: sudo apt install iproute2",
|
||||||
|
),
|
||||||
|
_check_command(
|
||||||
|
"sysctl",
|
||||||
|
"router kernel settings",
|
||||||
|
"install procps: sudo apt install procps",
|
||||||
|
),
|
||||||
|
_check_command(
|
||||||
|
"dnsmasq",
|
||||||
|
"DHCP server service",
|
||||||
|
"install dnsmasq-base: sudo apt install dnsmasq-base",
|
||||||
|
),
|
||||||
|
_check_command(
|
||||||
|
"dhclient",
|
||||||
|
"manual DHCP client experiment",
|
||||||
|
"install isc-dhcp-client: sudo apt install isc-dhcp-client",
|
||||||
|
),
|
||||||
|
_check_command(
|
||||||
|
"tcpdump",
|
||||||
|
"packet capture for DHCP, ARP, and routing experiments",
|
||||||
|
"install tcpdump: sudo apt install tcpdump",
|
||||||
|
),
|
||||||
|
_check_command(
|
||||||
|
"ping",
|
||||||
|
"ICMP connectivity experiments",
|
||||||
|
"install iputils-ping: sudo apt install iputils-ping",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
return EnvironmentReport(requirements=tuple(requirements))
|
||||||
|
|
||||||
|
|
||||||
|
# =================== Compilation and artifact execution ===================
|
||||||
|
|
||||||
|
def compile_topology(path: PathLike) -> Tuple[ExecutionPlan, str]:
|
||||||
|
"""Compile one topology file into a plan and executable Python source.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to the declarative topology JSON file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pair containing the fully resolved ExecutionPlan and rendered Python
|
||||||
|
source.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TopologyLoadError: If the JSON source cannot be loaded.
|
||||||
|
TopologyParseError: If the JSON violates the topology grammar.
|
||||||
|
TopologyValidationError: If the parsed topology is semantically
|
||||||
|
inconsistent.
|
||||||
|
TopologyPlanError: If the validated topology cannot be lowered into a
|
||||||
|
complete execution plan.
|
||||||
|
"""
|
||||||
|
|
||||||
|
topology = parse_topology(load_topology(path))
|
||||||
|
validate(topology)
|
||||||
|
execution_plan = plan(topology)
|
||||||
|
source = render_python(execution_plan)
|
||||||
|
|
||||||
|
return execution_plan, source
|
||||||
|
|
||||||
|
|
||||||
|
def default_generated_path(topology_path: PathLike) -> Path:
|
||||||
|
"""Return the default intermediate Python path for a topology source.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topology_path: Source topology path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path alongside the topology using the `.generated.py` suffix.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return Path(topology_path).with_suffix(".generated.py")
|
||||||
|
|
||||||
|
|
||||||
|
def write_generated_python(source: str, path: PathLike) -> Path:
|
||||||
|
"""Write generated executable Python source to disk.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: Rendered Python program.
|
||||||
|
path: Destination path for the generated artifact.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Destination Path, remaining relative if the supplied path is relative.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BuildNetworkError: If the artifact cannot be written.
|
||||||
|
"""
|
||||||
|
|
||||||
|
destination = Path(path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
destination.write_text(source, encoding="utf-8")
|
||||||
|
except OSError as exc:
|
||||||
|
raise BuildNetworkError(
|
||||||
|
"failed to write generated Python %s: %s" % (destination, exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
return destination
|
||||||
|
|
||||||
|
|
||||||
|
def execute_generated_python(path: PathLike) -> int:
|
||||||
|
"""Execute one generated Mininet Python artifact interactively.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Generated Python file to execute.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Exit status returned by the generated process.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BuildNetworkError: If execution is attempted without root privileges
|
||||||
|
or the generated process cannot be started.
|
||||||
|
"""
|
||||||
|
|
||||||
|
source = Path(path)
|
||||||
|
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
raise BuildNetworkError(
|
||||||
|
"network execution requires root privileges; rerun with sudo or "
|
||||||
|
"use --emit to generate the intermediate Python without executing it"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
completed = subprocess.run(
|
||||||
|
[sys.executable, str(source)],
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except OSError as exc:
|
||||||
|
raise BuildNetworkError(
|
||||||
|
"failed to execute generated Python %s: %s" % (source, exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
return completed.returncode
|
||||||
|
|
||||||
|
|
||||||
|
# ================== Command-line reporting and arguments ==================
|
||||||
|
|
||||||
|
def _print_environment_report(report: EnvironmentReport) -> None:
|
||||||
|
"""Print an environment report from the command-line layer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
report: Environment report to present to the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("Environment check:")
|
||||||
|
for requirement in report.requirements:
|
||||||
|
marker = "OK" if requirement.available else "MISSING"
|
||||||
|
print("[%s] %-16s %s" % (marker, requirement.name, requirement.detail))
|
||||||
|
|
||||||
|
if report.ok:
|
||||||
|
print("Environment OK.")
|
||||||
|
else:
|
||||||
|
print("Environment check failed: one or more required dependencies are missing.")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_argument_parser() -> argparse.ArgumentParser:
|
||||||
|
"""Create the command-line argument parser.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured ArgumentParser instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=(
|
||||||
|
"Load, validate, plan, render, and execute a declarative Mininet "
|
||||||
|
"topology."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"topology",
|
||||||
|
nargs="?",
|
||||||
|
help="path to topology.json; omitted only with --env-check",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--env-check",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"check Mininet, Open vSwitch, dnsmasq, and experiment tools, then exit"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--emit",
|
||||||
|
nargs="?",
|
||||||
|
const="",
|
||||||
|
metavar="PATH",
|
||||||
|
help=(
|
||||||
|
"generate the intermediate Python and stop; optionally write it to PATH"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
# ======================== Command-line entry point ========================
|
||||||
|
|
||||||
|
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||||
|
"""Run the coordinator command-line interface.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
argv: Optional argument sequence excluding the executable name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Process exit status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parser = _build_argument_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.env_check:
|
||||||
|
report = check_environment()
|
||||||
|
_print_environment_report(report)
|
||||||
|
return 0 if report.ok else 1
|
||||||
|
|
||||||
|
if args.topology is None:
|
||||||
|
parser.error("topology is required unless --env-check is used")
|
||||||
|
|
||||||
|
topology_path = Path(args.topology)
|
||||||
|
|
||||||
|
if args.emit is None:
|
||||||
|
generated_path = default_generated_path(topology_path)
|
||||||
|
execute = True
|
||||||
|
else:
|
||||||
|
generated_path = (
|
||||||
|
default_generated_path(topology_path)
|
||||||
|
if args.emit == ""
|
||||||
|
else Path(args.emit)
|
||||||
|
)
|
||||||
|
execute = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
_, source = compile_topology(topology_path)
|
||||||
|
generated_path = write_generated_python(source, generated_path)
|
||||||
|
|
||||||
|
print("Generated: %s" % generated_path)
|
||||||
|
|
||||||
|
if not execute:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
report = check_environment()
|
||||||
|
if not report.ok:
|
||||||
|
_print_environment_report(report)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return execute_generated_python(generated_path)
|
||||||
|
|
||||||
|
except (
|
||||||
|
TopologyLoadError,
|
||||||
|
TopologyParseError,
|
||||||
|
TopologyValidationError,
|
||||||
|
TopologyPlanError,
|
||||||
|
BuildNetworkError,
|
||||||
|
) as exc:
|
||||||
|
print("error: %s" % exc, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -10,9 +10,12 @@ The loading stage has two public entry points:
|
|||||||
model by calling topology_parser.parse_topology().
|
model by calling topology_parser.parse_topology().
|
||||||
|
|
||||||
No Mininet code is imported or executed in this module.
|
No Mininet code is imported or executed in this module.
|
||||||
|
|
||||||
|
Author: Christos Choutouridis <cchoutou@ece.auth.gr>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from typing import Any, Dict, Union
|
from typing import Any, Dict, Union
|
||||||
@@ -20,6 +23,8 @@ from typing import Any, Dict, Union
|
|||||||
from topology_parser import Topology, parse_topology
|
from topology_parser import Topology, parse_topology
|
||||||
|
|
||||||
|
|
||||||
|
# ============================ Types and errors ============================
|
||||||
|
|
||||||
PathLike = Union[str, Path]
|
PathLike = Union[str, Path]
|
||||||
|
|
||||||
|
|
||||||
@@ -27,6 +32,8 @@ class TopologyLoadError(ValueError):
|
|||||||
"""Raised when a topology file cannot be loaded as a JSON object."""
|
"""Raised when a topology file cannot be loaded as a JSON object."""
|
||||||
|
|
||||||
|
|
||||||
|
# ============================ Public interface ============================
|
||||||
|
|
||||||
def load_topology(path: PathLike) -> Dict[str, Any]:
|
def load_topology(path: PathLike) -> Dict[str, Any]:
|
||||||
"""Load a topology JSON file into a raw dictionary.
|
"""Load a topology JSON file into a raw dictionary.
|
||||||
|
|
||||||
@@ -64,23 +71,8 @@ def load_topology(path: PathLike) -> Dict[str, Any]:
|
|||||||
return raw
|
return raw
|
||||||
|
|
||||||
|
|
||||||
def load(path: PathLike) -> Topology:
|
|
||||||
"""Load and parse a topology file in one convenience call.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: Path to the topology JSON file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Parsed Topology model.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TopologyLoadError: If the JSON file cannot be loaded.
|
|
||||||
topology_parser.TopologyParseError: If the loaded JSON cannot be
|
|
||||||
represented by the topology grammar.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return parse_topology(load_topology(path))
|
|
||||||
|
|
||||||
|
# =============================== Self-test ===============================
|
||||||
|
|
||||||
def _self_test() -> None:
|
def _self_test() -> None:
|
||||||
"""Run a small smoke test against load_topology() and load()."""
|
"""Run a small smoke test against load_topology() and load()."""
|
||||||
@@ -106,7 +98,7 @@ def _self_test() -> None:
|
|||||||
handle.flush()
|
handle.flush()
|
||||||
|
|
||||||
raw = load_topology(handle.name)
|
raw = load_topology(handle.name)
|
||||||
topology = load(handle.name)
|
topology = parse_topology(load_topology(handle.name))
|
||||||
|
|
||||||
assert raw["nodes"][0]["name"] == "s1"
|
assert raw["nodes"][0]["name"] == "s1"
|
||||||
assert topology.nodes[0].name == "s1"
|
assert topology.nodes[0].name == "s1"
|
||||||
|
|||||||
+200
-34
@@ -15,6 +15,8 @@ from enum import Enum
|
|||||||
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union
|
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
|
|
||||||
|
# ======================= Semantic model and errors =======================
|
||||||
|
|
||||||
JSONMapping = Mapping[str, Any]
|
JSONMapping = Mapping[str, Any]
|
||||||
|
|
||||||
|
|
||||||
@@ -63,12 +65,16 @@ class Interface:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: Logical interface tag used by the topology description.
|
name: Logical interface tag used by the topology description.
|
||||||
addressing: Layer-3 addressing strategy. Omitting addressing in JSON
|
addressing: Layer-3 addressing strategy.
|
||||||
is represented as AddressingMode.NONE.
|
Omitting addressing in JSON is represented as AddressingMode.NONE.
|
||||||
address: Static CIDR address, valid when addressing is STATIC.
|
address: Static CIDR address, valid when addressing is STATIC.
|
||||||
gateway: Optional default gateway, valid when addressing is STATIC.
|
gateway: [Optional] default gateway, valid when addressing is STATIC.
|
||||||
mac: Optional explicit MAC address. Reserved for current/future use.
|
mac: [Optional] explicit MAC address applied to the concrete interface.
|
||||||
mtu: Optional MTU value. Reserved for current/future use.
|
mtu: [Optional] MTU in bytes applied to the concrete interface.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
MAC and MTU are independent of the addressing mode.
|
||||||
|
The generated program applies them using Linux ip link commands.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
@@ -83,8 +89,8 @@ class Interface:
|
|||||||
class HostSettings:
|
class HostSettings:
|
||||||
"""Contain host-specific settings.
|
"""Contain host-specific settings.
|
||||||
|
|
||||||
The current grammar does not require host-specific settings yet. Keeping
|
The current grammar does not require host-specific settings yet.
|
||||||
a dedicated type gives the model a stable extension point.
|
Keeping a dedicated type gives the model a stable extension point.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -92,8 +98,8 @@ class HostSettings:
|
|||||||
class SwitchSettings:
|
class SwitchSettings:
|
||||||
"""Contain switch-specific settings.
|
"""Contain switch-specific settings.
|
||||||
|
|
||||||
The current grammar does not require switch-specific settings yet. Keeping
|
The current grammar does not require switch-specific settings yet.
|
||||||
a dedicated type gives the model a stable extension point.
|
Keping a dedicated type gives the model a stable extension point.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -109,7 +115,7 @@ class RouterSettings:
|
|||||||
ip_forward: Optional[bool] = None
|
ip_forward: Optional[bool] = None
|
||||||
rp_filter: Optional[bool] = None
|
rp_filter: Optional[bool] = None
|
||||||
|
|
||||||
|
# Mutable global
|
||||||
NodeSettings = Union[HostSettings, SwitchSettings, RouterSettings]
|
NodeSettings = Union[HostSettings, SwitchSettings, RouterSettings]
|
||||||
|
|
||||||
|
|
||||||
@@ -120,10 +126,12 @@ class Node:
|
|||||||
Args:
|
Args:
|
||||||
name: Unique logical node name.
|
name: Unique logical node name.
|
||||||
type: Semantic node type.
|
type: Semantic node type.
|
||||||
interfaces: Declared logical interfaces. None means that the node did
|
interfaces:
|
||||||
not declare an interface set and therefore allows dynamic interface
|
Declared logical interfaces.
|
||||||
allocation. An empty dictionary means that an explicit, fixed
|
None means that the node did not declare an interface set and therefore
|
||||||
interface set was declared and contains zero interfaces.
|
allows dynamic interface allocation.
|
||||||
|
An empty dictionary means that an explicit, fixed interface set was declared
|
||||||
|
and contains zero interfaces.
|
||||||
settings: Type-specific node settings.
|
settings: Type-specific node settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -151,16 +159,17 @@ class LinkSettings:
|
|||||||
"""Contain optional link-specific provisioning settings.
|
"""Contain optional link-specific provisioning settings.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
bandwidth_mbps: Optional link bandwidth limit in megabits per second.
|
bandwidth_mbps: [Optional] link bandwidth limit in megabits per second.
|
||||||
delay_ms: Optional one-way propagation delay in milliseconds.
|
delay_ms: [Optional] one-way propagation delay in milliseconds.
|
||||||
loss_percent: Optional packet-loss percentage.
|
loss_percent: [Optional] packet-loss percentage.
|
||||||
jitter_ms: Optional delay variation in milliseconds.
|
jitter_ms: [Optional] delay variation in milliseconds.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
These fields are reserved as extension points for later Mininet link
|
All four settings are implemented through Mininet TCLink parameters
|
||||||
provisioning, for example through traffic-controlled link classes.
|
in the generated program.
|
||||||
They are parsed now so the semantic model already has a stable place
|
Bandwidth maps to bw, loss to loss, and delay and jitter to millisecond strings.
|
||||||
for link-level configuration.
|
Additional provisioning settings require corresponding parser,
|
||||||
|
validator, planner, and renderer support before they can be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
bandwidth_mbps: Optional[float] = None
|
bandwidth_mbps: Optional[float] = None
|
||||||
@@ -175,7 +184,7 @@ class Link:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
endpoints: Ordered pair of link endpoints.
|
endpoints: Ordered pair of link endpoints.
|
||||||
name: Optional logical link name used for diagnostics or extensions.
|
name: [ Optional] logical link name used for diagnostics or extensions.
|
||||||
settings: Link-specific provisioning settings.
|
settings: Link-specific provisioning settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -203,13 +212,14 @@ class DHCPServerSettings:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
address_range: Address range offered to DHCP clients.
|
address_range: Address range offered to DHCP clients.
|
||||||
gateway: Optional default gateway advertised to DHCP clients.
|
gateway: [Optional] default gateway advertised to DHCP clients.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
address_range: DHCPRange
|
address_range: DHCPRange
|
||||||
gateway: Optional[str] = None
|
gateway: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# Mutable global
|
||||||
ServiceSettings = DHCPServerSettings
|
ServiceSettings = DHCPServerSettings
|
||||||
|
|
||||||
|
|
||||||
@@ -235,14 +245,16 @@ class Service:
|
|||||||
class Topology:
|
class Topology:
|
||||||
"""Contain the complete parsed topology model.
|
"""Contain the complete parsed topology model.
|
||||||
|
|
||||||
Args:
|
nodes:
|
||||||
nodes: Nodes in declaration order. Keeping nodes as a list preserves
|
Nodes in declaration order. Keeping nodes as a list preserves
|
||||||
the parsed source faithfully, including duplicate names, so that
|
the parsed source faithfully, including duplicate names, so that
|
||||||
the separate validation stage can diagnose them instead of losing
|
the separate validation stage can diagnose them instead of losing
|
||||||
information during parsing.
|
information during parsing.
|
||||||
links: Links in declaration order. Order is preserved because it can
|
links:
|
||||||
|
Links in declaration order. Order is preserved because it can
|
||||||
make automatic interface allocation deterministic.
|
make automatic interface allocation deterministic.
|
||||||
services: Services in declaration order. Order may later be useful for
|
services:
|
||||||
|
Services in declaration order. Order may later be useful for
|
||||||
deterministic startup and shutdown.
|
deterministic startup and shutdown.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -251,6 +263,8 @@ class Topology:
|
|||||||
services: List[Service] = field(default_factory=list)
|
services: List[Service] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================ Parsing filters ============================
|
||||||
|
|
||||||
def _mapping(value: Any, context: str) -> JSONMapping:
|
def _mapping(value: Any, context: str) -> JSONMapping:
|
||||||
"""Return a value as a mapping or raise a parse error.
|
"""Return a value as a mapping or raise a parse error.
|
||||||
|
|
||||||
@@ -292,7 +306,7 @@ def _reject_unknown_fields(
|
|||||||
"%s contains unknown field%s: %s"
|
"%s contains unknown field%s: %s"
|
||||||
% (
|
% (
|
||||||
context,
|
context,
|
||||||
"s" if len(unknown) != 1 else "",
|
"s" if len(unknown) != 1 else "", # don't forget your English grammar ;)
|
||||||
", ".join(repr(name) for name in unknown),
|
", ".join(repr(name) for name in unknown),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -392,6 +406,7 @@ def _optional_int(value: Any, context: str) -> Optional[int]:
|
|||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
|
# bool is a subclass of int, but JSON booleans are not numeric settings.
|
||||||
if isinstance(value, bool) or not isinstance(value, int):
|
if isinstance(value, bool) or not isinstance(value, int):
|
||||||
raise TopologyParseError("%s must be an integer" % context)
|
raise TopologyParseError("%s must be an integer" % context)
|
||||||
return value
|
return value
|
||||||
@@ -401,8 +416,7 @@ def _optional_float(value: Any, context: str) -> Optional[float]:
|
|||||||
"""Return an optional numeric value as float or raise a parse error.
|
"""Return an optional numeric value as float or raise a parse error.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value: None or a value expected to be an integer or floating-point
|
value: None or a value expected to be an integer or floating-point number.
|
||||||
number.
|
|
||||||
context: Human-readable location used in the error message.
|
context: Human-readable location used in the error message.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -414,10 +428,12 @@ def _optional_float(value: Any, context: str) -> Optional[float]:
|
|||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
|
# Reject booleans before accepting Python's integer and float types.
|
||||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||||
raise TopologyParseError("%s must be a number" % context)
|
raise TopologyParseError("%s must be a number" % context)
|
||||||
return float(value)
|
return float(value)
|
||||||
|
|
||||||
|
# ============================= Entity parsing =============================
|
||||||
|
|
||||||
def _parse_interface(name: str, raw: Any, context: str) -> Interface:
|
def _parse_interface(name: str, raw: Any, context: str) -> Interface:
|
||||||
"""Parse one interface definition.
|
"""Parse one interface definition.
|
||||||
@@ -430,6 +446,24 @@ def _parse_interface(name: str, raw: Any, context: str) -> Interface:
|
|||||||
Returns:
|
Returns:
|
||||||
Parsed Interface object.
|
Parsed Interface object.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Input (diagnostic context omitted):
|
||||||
|
name = "lan"
|
||||||
|
raw = {
|
||||||
|
"addressing": "static",
|
||||||
|
"address": "192.168.1.1/24",
|
||||||
|
}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
Interface(
|
||||||
|
name="lan",
|
||||||
|
addressing=AddressingMode.STATIC,
|
||||||
|
address="192.168.1.1/24",
|
||||||
|
gateway=None,
|
||||||
|
mac=None,
|
||||||
|
mtu=None,
|
||||||
|
)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TopologyParseError: If the interface cannot be represented.
|
TopologyParseError: If the interface cannot be represented.
|
||||||
"""
|
"""
|
||||||
@@ -467,13 +501,31 @@ def _parse_interfaces(raw_node: JSONMapping, context: str) -> Optional[Dict[str,
|
|||||||
context: Human-readable node location used in error messages.
|
context: Human-readable node location used in error messages.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None when the interfaces field is omitted, otherwise a dictionary of
|
- None when the interfaces field is omitted (dynamic), otherwise
|
||||||
logical interface tags to Interface objects.
|
- a dictionary of logical interface tags to Interface objects.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Each case shows the value passed as raw_node and the returned value,
|
||||||
|
the diagnostic context argument is omitted.
|
||||||
|
raw_node is the node object, not just its interfaces member.
|
||||||
|
Unrelated node fields are omitted from these examples.
|
||||||
|
|
||||||
|
Interface set raw_node["interfaces"] return
|
||||||
|
--------------------------------------------------------
|
||||||
|
Omitted: {} None
|
||||||
|
Explicitly empty: {"interfaces": {}} {}
|
||||||
|
One declared: {"interfaces": {"lan": {}}} {"lan": Interface(name="lan")}
|
||||||
|
|
||||||
|
Omission preserves a dynamic interface set. An empty object preserves
|
||||||
|
a fixed pool containing zero interfaces.
|
||||||
|
Interface(name="lan") uses the model defaults, including NONE
|
||||||
|
addressing and no address, gateway, MAC, or MTU.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TopologyParseError: If the interfaces field has an invalid shape.
|
TopologyParseError: If the interfaces field has an invalid shape.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Preserve the distinction between dynamic allocation and a fixed empty pool.
|
||||||
if "interfaces" not in raw_node:
|
if "interfaces" not in raw_node:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -502,6 +554,17 @@ def _parse_node_settings(node_type: NodeType, raw: Any, context: str) -> NodeSet
|
|||||||
Returns:
|
Returns:
|
||||||
Type-specific node settings object.
|
Type-specific node settings object.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Input (Python values after JSON loading, diagnostic context omitted):
|
||||||
|
node_type = NodeType.ROUTER
|
||||||
|
raw = {"ip_forward": True, "rp_filter": False}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
RouterSettings(ip_forward=True, rp_filter=False)
|
||||||
|
|
||||||
|
For node_type = NodeType.HOST and raw = {}, the output is
|
||||||
|
HostSettings().
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TopologyParseError: If the settings object has an invalid shape or
|
TopologyParseError: If the settings object has an invalid shape or
|
||||||
contains values that cannot be represented by the selected type.
|
contains values that cannot be represented by the selected type.
|
||||||
@@ -544,6 +607,28 @@ def _parse_node(raw: Any, index: int) -> Node:
|
|||||||
Returns:
|
Returns:
|
||||||
Parsed Node object.
|
Parsed Node object.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Input raw (diagnostic index omitted):
|
||||||
|
{
|
||||||
|
"name": "h1",
|
||||||
|
"type": "host",
|
||||||
|
"interfaces": {"net0": {"addressing": "dhcp"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
Node(
|
||||||
|
name="h1",
|
||||||
|
type=NodeType.HOST,
|
||||||
|
interfaces={
|
||||||
|
"net0": Interface(
|
||||||
|
name="net0", addressing=AddressingMode.DHCP,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
settings=HostSettings(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Unspecified Interface fields use their model defaults.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TopologyParseError: If the node cannot be represented.
|
TopologyParseError: If the node cannot be represented.
|
||||||
"""
|
"""
|
||||||
@@ -671,6 +756,29 @@ def _parse_link(raw: Any, index: int) -> Link:
|
|||||||
Returns:
|
Returns:
|
||||||
Parsed Link object.
|
Parsed Link object.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Input raw (diagnostic index omitted):
|
||||||
|
{
|
||||||
|
"endpoints": [
|
||||||
|
{"node": "h1", "interface": "net0"},
|
||||||
|
{"node": "s1", "interface": "auto"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
Link(
|
||||||
|
endpoints=(
|
||||||
|
LinkEndpoint(node="h1", interface="net0"),
|
||||||
|
LinkEndpoint(
|
||||||
|
node="s1", interface=LinkInterfaceSelector.AUTO,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Note:
|
||||||
|
- Omitted name and settings use the Link defaults.
|
||||||
|
- Parsing converts the endpoint list to a tuple and recognizes AUTO.
|
||||||
|
- Validation checks whether the referenced nodes and interfaces exist.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TopologyParseError: If the link cannot be represented.
|
TopologyParseError: If the link cannot be represented.
|
||||||
"""
|
"""
|
||||||
@@ -741,6 +849,26 @@ def _parse_dhcp_server_settings(raw: Any, context: str) -> DHCPServerSettings:
|
|||||||
Returns:
|
Returns:
|
||||||
Parsed DHCPServerSettings object.
|
Parsed DHCPServerSettings object.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Input raw (diagnostic context omitted):
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": "192.168.1.100",
|
||||||
|
"end": "192.168.1.200",
|
||||||
|
},
|
||||||
|
"gateway": "192.168.1.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
DHCPServerSettings(
|
||||||
|
address_range=DHCPRange(
|
||||||
|
start="192.168.1.100", end="192.168.1.200",
|
||||||
|
),
|
||||||
|
gateway="192.168.1.1",
|
||||||
|
)
|
||||||
|
|
||||||
|
The JSON range object becomes the typed address_range field.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TopologyParseError: If required structural fields are missing or have
|
TopologyParseError: If required structural fields are missing or have
|
||||||
incompatible JSON types.
|
incompatible JSON types.
|
||||||
@@ -773,6 +901,37 @@ def _parse_service(raw: Any, index: int) -> Service:
|
|||||||
Returns:
|
Returns:
|
||||||
Parsed Service object.
|
Parsed Service object.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Input raw (diagnostic index omitted):
|
||||||
|
{
|
||||||
|
"type": "dhcp-server",
|
||||||
|
"node": "r0",
|
||||||
|
"interface": "lan",
|
||||||
|
"settings": {
|
||||||
|
"range": {
|
||||||
|
"start": "192.168.1.100",
|
||||||
|
"end": "192.168.1.200",
|
||||||
|
},
|
||||||
|
"gateway": "192.168.1.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
Service(
|
||||||
|
type=ServiceType.DHCP_SERVER,
|
||||||
|
node="r0",
|
||||||
|
interface="lan",
|
||||||
|
settings=DHCPServerSettings(
|
||||||
|
address_range=DHCPRange(
|
||||||
|
start="192.168.1.100", end="192.168.1.200",
|
||||||
|
),
|
||||||
|
gateway="192.168.1.1",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Binding fields remain separate from service settings.
|
||||||
|
Parsing preserves the references. Validation checks their validity.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TopologyParseError: If the service cannot be represented.
|
TopologyParseError: If the service cannot be represented.
|
||||||
"""
|
"""
|
||||||
@@ -814,6 +973,10 @@ def _parse_service(raw: Any, index: int) -> Service:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ============================ Public interface ============================
|
||||||
|
|
||||||
def parse_topology(raw: Mapping[str, Any]) -> Topology:
|
def parse_topology(raw: Mapping[str, Any]) -> Topology:
|
||||||
"""Parse a raw JSON dictionary into the typed topology model.
|
"""Parse a raw JSON dictionary into the typed topology model.
|
||||||
|
|
||||||
@@ -828,7 +991,7 @@ def parse_topology(raw: Mapping[str, Any]) -> Topology:
|
|||||||
topology grammar.
|
topology grammar.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
This function intentionally does not validate cross references such as
|
This function intentionally does not validate cross-references such as
|
||||||
whether a link references an existing node. Those checks belong to
|
whether a link references an existing node. Those checks belong to
|
||||||
the separate validation stage.
|
the separate validation stage.
|
||||||
"""
|
"""
|
||||||
@@ -858,6 +1021,9 @@ def parse_topology(raw: Mapping[str, Any]) -> Topology:
|
|||||||
return Topology(nodes=nodes, links=links, services=services)
|
return Topology(nodes=nodes, links=links, services=services)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# =============================== Self-test ===============================
|
||||||
|
|
||||||
def _self_test() -> None:
|
def _self_test() -> None:
|
||||||
"""Run a small smoke test against the public parse_topology() interface."""
|
"""Run a small smoke test against the public parse_topology() interface."""
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,889 @@
|
|||||||
|
"""Lower a validated topology into a fully resolved execution plan.
|
||||||
|
|
||||||
|
This module is the planning stage of the topology pipeline.
|
||||||
|
|
||||||
|
The parser produces a typed description of the user's intent and the validator
|
||||||
|
proves that the description is semantically coherent. The planner then makes
|
||||||
|
all implementation choices that must be resolved before executable Mininet
|
||||||
|
Python can be generated.
|
||||||
|
|
||||||
|
In particular, the planner resolves logical and automatic interface references
|
||||||
|
into concrete Linux/Mininet interface names. It does not import Mininet,
|
||||||
|
execute commands, modify the network, or render Python source.
|
||||||
|
|
||||||
|
Author: Christos Choutouridis <cchoutou@ece.auth.gr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, List, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
|
from topology_parser import (
|
||||||
|
AddressingMode,
|
||||||
|
DHCPServerSettings,
|
||||||
|
Interface,
|
||||||
|
LinkInterfaceSelector,
|
||||||
|
Node,
|
||||||
|
NodeType,
|
||||||
|
RouterSettings,
|
||||||
|
Service,
|
||||||
|
ServiceInterfaceSelector,
|
||||||
|
ServiceType,
|
||||||
|
Topology,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ========================== Execution plan model ==========================
|
||||||
|
|
||||||
|
class TopologyPlanError(ValueError):
|
||||||
|
"""Raised when a validated topology cannot be lowered into a plan."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlannedNode:
|
||||||
|
"""Describe one concrete node creation operation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Logical node name used by Mininet.
|
||||||
|
type: Concrete semantic node type to create.
|
||||||
|
switch_fail_mode: Optional Open vSwitch fail mode selected by the
|
||||||
|
planner for switch nodes. Non-switch nodes leave this unset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
type: NodeType
|
||||||
|
switch_fail_mode: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlannedLinkEndpoint:
|
||||||
|
"""Describe one fully resolved endpoint of a planned link.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node: Logical node name that owns the endpoint.
|
||||||
|
logical_interface: Resolved logical interface identifier. For a node
|
||||||
|
with a dynamic interface set, the planner creates a deterministic
|
||||||
|
synthetic identifier for an automatically allocated interface.
|
||||||
|
interface_name: Concrete Linux/Mininet interface name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
node: str
|
||||||
|
logical_interface: str
|
||||||
|
interface_name: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlannedLinkSettings:
|
||||||
|
"""Contain resolved provisioning settings for one link.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bandwidth_mbps: Optional bandwidth limit in megabits per second.
|
||||||
|
delay_ms: Optional one-way delay in milliseconds.
|
||||||
|
loss_percent: Optional packet-loss percentage.
|
||||||
|
jitter_ms: Optional delay variation in milliseconds.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bandwidth_mbps: Optional[float] = None
|
||||||
|
delay_ms: Optional[float] = None
|
||||||
|
loss_percent: Optional[float] = None
|
||||||
|
jitter_ms: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlannedLink:
|
||||||
|
"""Describe one fully resolved Mininet link creation operation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
endpoints: Ordered pair of fully resolved link endpoints.
|
||||||
|
name: Optional logical link name preserved for diagnostics.
|
||||||
|
settings: Concrete link provisioning settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
endpoints: Tuple[PlannedLinkEndpoint, PlannedLinkEndpoint]
|
||||||
|
name: Optional[str]
|
||||||
|
settings: PlannedLinkSettings
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlannedInterface:
|
||||||
|
"""Describe post-link configuration for one concrete interface.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node: Logical node name that owns the interface.
|
||||||
|
logical_interface: Resolved logical interface identifier.
|
||||||
|
interface_name: Concrete Linux/Mininet interface name.
|
||||||
|
addressing: Layer-3 addressing strategy.
|
||||||
|
address: Optional static CIDR address.
|
||||||
|
gateway: Optional static default gateway.
|
||||||
|
mac: Optional explicit MAC address.
|
||||||
|
mtu: Optional MTU value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
node: str
|
||||||
|
logical_interface: str
|
||||||
|
interface_name: str
|
||||||
|
addressing: AddressingMode
|
||||||
|
address: Optional[str]
|
||||||
|
gateway: Optional[str]
|
||||||
|
mac: Optional[str]
|
||||||
|
mtu: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlannedSysctl:
|
||||||
|
"""Describe one namespace-scoped sysctl operation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node: Logical node on which the sysctl is applied.
|
||||||
|
key: Fully resolved sysctl key.
|
||||||
|
value: Integer value written to the key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
node: str
|
||||||
|
key: str
|
||||||
|
value: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlannedDHCPServerSettings:
|
||||||
|
"""Contain fully resolved DHCP server settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
range_start: First address offered to DHCP clients.
|
||||||
|
range_end: Last address offered to DHCP clients.
|
||||||
|
gateway: Optional default gateway advertised to DHCP clients.
|
||||||
|
"""
|
||||||
|
|
||||||
|
range_start: str
|
||||||
|
range_end: str
|
||||||
|
gateway: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
PlannedServiceSettings = PlannedDHCPServerSettings
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlannedService:
|
||||||
|
"""Describe one fully resolved service startup operation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type: Service implementation type.
|
||||||
|
node: Logical node on which the service runs.
|
||||||
|
logical_interface: Resolved logical interface identifier.
|
||||||
|
interface_name: Concrete Linux/Mininet interface name to bind.
|
||||||
|
settings: Service-type-specific resolved settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type: ServiceType
|
||||||
|
node: str
|
||||||
|
logical_interface: str
|
||||||
|
interface_name: str
|
||||||
|
settings: PlannedServiceSettings
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ExecutionPlan:
|
||||||
|
"""Contain the complete, fully resolved execution plan.
|
||||||
|
|
||||||
|
Ordered tuples of frozen operation objects capture the result of planning.
|
||||||
|
They carry concrete names and configuration into the renderer without
|
||||||
|
exposing the mutable allocation context or the source model objects.
|
||||||
|
The renderer emits the operations in lifecycle order: construction,
|
||||||
|
configuration, and service startup.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodes: Node creation operations in declaration order.
|
||||||
|
links: Link creation operations in declaration order.
|
||||||
|
interfaces: Concrete interface configuration operations.
|
||||||
|
node_settings: Concrete namespace-level configuration operations.
|
||||||
|
services: Service startup operations in declaration order.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The plan contains no AUTO or ALL selectors. All such decisions are
|
||||||
|
resolved before an ExecutionPlan is returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
nodes: Tuple[PlannedNode, ...]
|
||||||
|
links: Tuple[PlannedLink, ...]
|
||||||
|
interfaces: Tuple[PlannedInterface, ...]
|
||||||
|
node_settings: Tuple[PlannedSysctl, ...]
|
||||||
|
services: Tuple[PlannedService, ...]
|
||||||
|
|
||||||
|
|
||||||
|
# ======================== Mutable planning context ========================
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _PlanningContext:
|
||||||
|
"""Hold mutable state used while lowering a topology.
|
||||||
|
|
||||||
|
_build_context() initializes node lookup, reservations, free pools, and
|
||||||
|
counters from the validated topology.
|
||||||
|
During _plan_links(), allocation helpers consume pool entries, advance
|
||||||
|
counters, and record interface mappings.
|
||||||
|
Interface, node-setting, and service planning then read those mappings.
|
||||||
|
|
||||||
|
A logical interface is identified by (node_name, logical_name), since tags
|
||||||
|
need only be unique within their owning node.
|
||||||
|
interface_names and interface_models acquire the same keys on allocation:
|
||||||
|
one supplies the backend name, and the other supplies its configuration.
|
||||||
|
A reservation alone does not create an entry in either mapping.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodes: Source nodes indexed by name, safe after uniqueness validation.
|
||||||
|
reserved_interfaces: All explicit link references per node, used to
|
||||||
|
construct free pools and retained unchanged during allocation.
|
||||||
|
free_interfaces: Ordered lists of unreserved declared tags for fixed
|
||||||
|
pools; AUTO removes the first entry.
|
||||||
|
An empty list means exhaustion; dynamic nodes have no entry.
|
||||||
|
dynamic_interface_counters: Next synthetic logical-tag indices, such
|
||||||
|
as auto0; advanced only for nodes with omitted interface sets.
|
||||||
|
physical_interface_counters: Next concrete eth-index per node;
|
||||||
|
advanced for every allocation, including explicit selections.
|
||||||
|
interface_names: Allocated (node, logical tag) pairs mapped to concrete
|
||||||
|
Linux/Mininet names, in interface-configuration order.
|
||||||
|
interface_models: The same allocated keys mapped to source Interface
|
||||||
|
objects or default models created for dynamic interfaces.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
A fixed pool declared as p1, p2, uplink with an explicit uplink
|
||||||
|
reference starts with free_interfaces containing [p1, p2].
|
||||||
|
If the first endpoint for that node uses AUTO, it consumes p1 and
|
||||||
|
receives <node>-eth0, recording both mappings for (node, p1).
|
||||||
|
"""
|
||||||
|
|
||||||
|
nodes: Dict[str, Node]
|
||||||
|
reserved_interfaces: Dict[str, Set[str]]
|
||||||
|
free_interfaces: Dict[str, List[str]]
|
||||||
|
dynamic_interface_counters: Dict[str, int]
|
||||||
|
physical_interface_counters: Dict[str, int]
|
||||||
|
interface_names: Dict[Tuple[str, str], str]
|
||||||
|
interface_models: Dict[Tuple[str, str], Interface]
|
||||||
|
|
||||||
|
|
||||||
|
# ========================= Context initialization =========================
|
||||||
|
|
||||||
|
def _build_context(topology: Topology) -> _PlanningContext:
|
||||||
|
"""Create planning state and reserve all explicit link interfaces.
|
||||||
|
|
||||||
|
Scan every link before constructing the free-interface pools so an AUTO
|
||||||
|
endpoint cannot consume an interface explicitly referenced by a later link.
|
||||||
|
Preserve declaration order in each pool for deterministic allocation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topology: Validated topology to lower.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Initialized mutable planning context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
nodes = {node.name: node for node in topology.nodes}
|
||||||
|
reserved: Dict[str, Set[str]] = {node.name: set() for node in topology.nodes}
|
||||||
|
|
||||||
|
for link in topology.links:
|
||||||
|
for endpoint in link.endpoints:
|
||||||
|
if isinstance(endpoint.interface, str):
|
||||||
|
reserved[endpoint.node].add(endpoint.interface)
|
||||||
|
|
||||||
|
free_interfaces: Dict[str, List[str]] = {}
|
||||||
|
for node in topology.nodes:
|
||||||
|
if node.interfaces is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
free_interfaces[node.name] = [
|
||||||
|
name
|
||||||
|
for name in node.interfaces.keys()
|
||||||
|
if name not in reserved[node.name]
|
||||||
|
]
|
||||||
|
|
||||||
|
return _PlanningContext(
|
||||||
|
nodes=nodes,
|
||||||
|
reserved_interfaces=reserved,
|
||||||
|
free_interfaces=free_interfaces,
|
||||||
|
dynamic_interface_counters={node.name: 0 for node in topology.nodes},
|
||||||
|
physical_interface_counters={node.name: 0 for node in topology.nodes},
|
||||||
|
interface_names={},
|
||||||
|
interface_models={},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ========================= Node and link planning =========================
|
||||||
|
|
||||||
|
def _plan_nodes(topology: Topology) -> Tuple[PlannedNode, ...]:
|
||||||
|
"""Lower topology nodes into concrete node creation operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topology: Validated topology to lower.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Planned nodes in source declaration order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
PlannedNode(
|
||||||
|
name=node.name,
|
||||||
|
type=node.type,
|
||||||
|
switch_fail_mode=(
|
||||||
|
"standalone" if node.type is NodeType.SWITCH else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for node in topology.nodes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _allocate_logical_interface(
|
||||||
|
node: Node,
|
||||||
|
selector: Union[str, LinkInterfaceSelector],
|
||||||
|
context: _PlanningContext,
|
||||||
|
) -> Tuple[str, Interface]:
|
||||||
|
"""Resolve one link endpoint to a concrete logical interface.
|
||||||
|
|
||||||
|
Explicit tags look up their declared models without consuming a free pool.
|
||||||
|
AUTO consumes the first available tag for a fixed pool, or creates a
|
||||||
|
synthetic tag and an unconfigured model for a dynamic pool.
|
||||||
|
Concrete backend names are assigned separately by physical allocation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node: Node that owns the endpoint.
|
||||||
|
selector: Explicit interface tag or AUTO selector.
|
||||||
|
context: Mutable planning context.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pair containing the resolved logical interface name and its interface
|
||||||
|
configuration model.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TopologyPlanError: If the topology violates a planner precondition.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(selector, str):
|
||||||
|
if node.interfaces is None or selector not in node.interfaces:
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"explicit interface %r cannot be resolved on node %r"
|
||||||
|
% (selector, node.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return selector, node.interfaces[selector]
|
||||||
|
|
||||||
|
if selector is not LinkInterfaceSelector.AUTO:
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"unsupported link interface selector %r" % selector
|
||||||
|
)
|
||||||
|
|
||||||
|
if node.interfaces is not None:
|
||||||
|
free = context.free_interfaces[node.name]
|
||||||
|
if not free:
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"node %r has no declared interface available for AUTO"
|
||||||
|
% node.name
|
||||||
|
)
|
||||||
|
|
||||||
|
logical_name = free.pop(0)
|
||||||
|
return logical_name, node.interfaces[logical_name]
|
||||||
|
|
||||||
|
counter = context.dynamic_interface_counters[node.name]
|
||||||
|
context.dynamic_interface_counters[node.name] = counter + 1
|
||||||
|
|
||||||
|
logical_name = "auto%d" % counter
|
||||||
|
return logical_name, Interface(name=logical_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _allocate_physical_interface(
|
||||||
|
node_name: str,
|
||||||
|
logical_name: str,
|
||||||
|
interface: Interface,
|
||||||
|
context: _PlanningContext,
|
||||||
|
) -> str:
|
||||||
|
"""Assign one deterministic concrete interface name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node_name: Logical node that owns the interface.
|
||||||
|
logical_name: Resolved logical interface identifier.
|
||||||
|
interface: Interface configuration model.
|
||||||
|
context: Mutable planning context.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Concrete Linux/Mininet interface name.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TopologyPlanError: If the same logical interface is allocated twice.
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = (node_name, logical_name)
|
||||||
|
if key in context.interface_names:
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"interface %s:%s was allocated more than once"
|
||||||
|
% (node_name, logical_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
index = context.physical_interface_counters[node_name]
|
||||||
|
context.physical_interface_counters[node_name] = index + 1
|
||||||
|
|
||||||
|
interface_name = "%s-eth%d" % (node_name, index)
|
||||||
|
# These mappings let later stages recover both the name and configuration.
|
||||||
|
context.interface_names[key] = interface_name
|
||||||
|
context.interface_models[key] = interface
|
||||||
|
|
||||||
|
return interface_name
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_links(
|
||||||
|
topology: Topology,
|
||||||
|
context: _PlanningContext,
|
||||||
|
) -> Tuple[PlannedLink, ...]:
|
||||||
|
"""Resolve all link endpoints and concrete interface names.
|
||||||
|
|
||||||
|
Traverse links and their endpoints in source order, resolving each logical
|
||||||
|
selection before assigning its backend name.
|
||||||
|
Allocation records mappings in the shared context for subsequent interface,
|
||||||
|
node-setting, and service planning, so traversal order determines naming.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topology: Validated topology to lower.
|
||||||
|
context: Mutable planning context.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Planned links in source declaration order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
planned_links: List[PlannedLink] = []
|
||||||
|
|
||||||
|
for link in topology.links:
|
||||||
|
planned_endpoints: List[PlannedLinkEndpoint] = []
|
||||||
|
|
||||||
|
for endpoint in link.endpoints:
|
||||||
|
node = context.nodes[endpoint.node]
|
||||||
|
logical_name, interface = _allocate_logical_interface(
|
||||||
|
node,
|
||||||
|
endpoint.interface,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
interface_name = _allocate_physical_interface(
|
||||||
|
node.name,
|
||||||
|
logical_name,
|
||||||
|
interface,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
|
||||||
|
planned_endpoints.append(
|
||||||
|
PlannedLinkEndpoint(
|
||||||
|
node=node.name,
|
||||||
|
logical_interface=logical_name,
|
||||||
|
interface_name=interface_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
planned_links.append(
|
||||||
|
PlannedLink(
|
||||||
|
endpoints=(planned_endpoints[0], planned_endpoints[1]),
|
||||||
|
name=link.name,
|
||||||
|
settings=PlannedLinkSettings(
|
||||||
|
bandwidth_mbps=link.settings.bandwidth_mbps,
|
||||||
|
delay_ms=link.settings.delay_ms,
|
||||||
|
loss_percent=link.settings.loss_percent,
|
||||||
|
jitter_ms=link.settings.jitter_ms,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return tuple(planned_links)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Interface and node configuration ====================
|
||||||
|
|
||||||
|
def _interface_requires_materialization(interface: Interface) -> bool:
|
||||||
|
"""Return whether an unused declared interface carries configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interface: Declared interface to inspect.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True when silently leaving the interface unmaterialized would discard
|
||||||
|
requested configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
interface.addressing is not AddressingMode.NONE
|
||||||
|
or interface.address is not None
|
||||||
|
or interface.gateway is not None
|
||||||
|
or interface.mac is not None
|
||||||
|
or interface.mtu is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_interfaces(
|
||||||
|
topology: Topology,
|
||||||
|
context: _PlanningContext,
|
||||||
|
) -> Tuple[PlannedInterface, ...]:
|
||||||
|
"""Create interface-configuration operations for materialized interfaces.
|
||||||
|
|
||||||
|
First reject configured declarations absent from the allocation mappings:
|
||||||
|
Mininet creates these interfaces through links, so there would be no
|
||||||
|
concrete interface on which to apply their configuration.
|
||||||
|
Empty unused declarations need no operation and may remain unallocated.
|
||||||
|
Then pair each allocated name with its model in allocation order.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topology: Validated topology to lower.
|
||||||
|
context: Planning context containing resolved interface mappings.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Planned interface configuration operations.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TopologyPlanError: If a configured declared interface is not attached to
|
||||||
|
any link and therefore has no concrete Mininet interface to
|
||||||
|
configure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for node in topology.nodes:
|
||||||
|
if node.interfaces is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for logical_name, interface in node.interfaces.items():
|
||||||
|
key = (node.name, logical_name)
|
||||||
|
if (
|
||||||
|
key not in context.interface_names
|
||||||
|
and _interface_requires_materialization(interface)
|
||||||
|
):
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"configured interface %s:%s is not attached to any link"
|
||||||
|
% (node.name, logical_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
planned: List[PlannedInterface] = []
|
||||||
|
|
||||||
|
# The mapping insertion order is the exact allocation order from
|
||||||
|
# _plan_links(), which is deterministic on supported Python versions.
|
||||||
|
for key, interface_name in context.interface_names.items():
|
||||||
|
node_name, logical_name = key
|
||||||
|
interface = context.interface_models[key]
|
||||||
|
|
||||||
|
planned.append(
|
||||||
|
PlannedInterface(
|
||||||
|
node=node_name,
|
||||||
|
logical_interface=logical_name,
|
||||||
|
interface_name=interface_name,
|
||||||
|
addressing=interface.addressing,
|
||||||
|
address=interface.address,
|
||||||
|
gateway=interface.gateway,
|
||||||
|
mac=interface.mac,
|
||||||
|
mtu=interface.mtu,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return tuple(planned)
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_node_settings(
|
||||||
|
topology: Topology,
|
||||||
|
context: _PlanningContext,
|
||||||
|
) -> Tuple[PlannedSysctl, ...]:
|
||||||
|
"""Lower node settings into concrete namespace sysctl operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topology: Validated topology to lower.
|
||||||
|
context: Planning context containing concrete interface names.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Planned sysctl operations in deterministic order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
planned: List[PlannedSysctl] = []
|
||||||
|
|
||||||
|
for node in topology.nodes:
|
||||||
|
if node.type is not NodeType.ROUTER:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not isinstance(node.settings, RouterSettings):
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"router %r does not carry RouterSettings" % node.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if node.settings.ip_forward is not None:
|
||||||
|
planned.append(
|
||||||
|
PlannedSysctl(
|
||||||
|
node=node.name,
|
||||||
|
key="net.ipv4.ip_forward",
|
||||||
|
value=1 if node.settings.ip_forward else 0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if node.settings.rp_filter is not None:
|
||||||
|
value = 1 if node.settings.rp_filter else 0
|
||||||
|
|
||||||
|
planned.append(
|
||||||
|
PlannedSysctl(
|
||||||
|
node=node.name,
|
||||||
|
key="net.ipv4.conf.all.rp_filter",
|
||||||
|
value=value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
planned.append(
|
||||||
|
PlannedSysctl(
|
||||||
|
node=node.name,
|
||||||
|
key="net.ipv4.conf.default.rp_filter",
|
||||||
|
value=value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (owner, _), interface_name in context.interface_names.items():
|
||||||
|
if owner != node.name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
planned.append(
|
||||||
|
PlannedSysctl(
|
||||||
|
node=node.name,
|
||||||
|
key="net.ipv4.conf.%s.rp_filter" % interface_name,
|
||||||
|
value=value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return tuple(planned)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================ Service planning ============================
|
||||||
|
|
||||||
|
def _resolve_service_logical_interface(
|
||||||
|
service: Service,
|
||||||
|
node: Node,
|
||||||
|
) -> str:
|
||||||
|
"""Resolve one validated service interface selector.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
service: Service whose interface binding is resolved.
|
||||||
|
node: Node on which the service runs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Concrete logical interface tag.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TopologyPlanError: If the selector cannot be lowered uniquely.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(service.interface, str):
|
||||||
|
return service.interface
|
||||||
|
|
||||||
|
if service.interface is ServiceInterfaceSelector.AUTO:
|
||||||
|
if node.interfaces is None:
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"automatic service binding on node %r requires declared "
|
||||||
|
"interfaces" % node.name
|
||||||
|
)
|
||||||
|
|
||||||
|
candidates = [
|
||||||
|
name
|
||||||
|
for name, interface in node.interfaces.items()
|
||||||
|
if interface.addressing is AddressingMode.STATIC
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(candidates) != 1:
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"automatic service binding on node %r is not uniquely "
|
||||||
|
"resolvable" % node.name
|
||||||
|
)
|
||||||
|
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
if service.interface is ServiceInterfaceSelector.ALL:
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"service type %r cannot be lowered from the ALL selector"
|
||||||
|
% service.type.value
|
||||||
|
)
|
||||||
|
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"unsupported service interface selector %r" % service.interface
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_services(
|
||||||
|
topology: Topology,
|
||||||
|
context: _PlanningContext,
|
||||||
|
) -> Tuple[PlannedService, ...]:
|
||||||
|
"""Resolve service bindings and lower service-specific settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topology: Validated topology to lower.
|
||||||
|
context: Planning context containing concrete interface names.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Planned service startup operations in declaration order.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TopologyPlanError: If a service cannot be mapped to a materialized
|
||||||
|
interface or has unsupported settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
planned: List[PlannedService] = []
|
||||||
|
|
||||||
|
for service in topology.services:
|
||||||
|
node = context.nodes[service.node]
|
||||||
|
logical_name = _resolve_service_logical_interface(service, node)
|
||||||
|
key = (node.name, logical_name)
|
||||||
|
|
||||||
|
if key not in context.interface_names:
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"service %r binds to interface %s:%s, but that interface is "
|
||||||
|
"not attached to any link"
|
||||||
|
% (service.type.value, node.name, logical_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if service.type is ServiceType.DHCP_SERVER:
|
||||||
|
if not isinstance(service.settings, DHCPServerSettings):
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"DHCP service on %r has incompatible settings" % node.name
|
||||||
|
)
|
||||||
|
|
||||||
|
settings = PlannedDHCPServerSettings(
|
||||||
|
range_start=service.settings.address_range.start,
|
||||||
|
range_end=service.settings.address_range.end,
|
||||||
|
gateway=service.settings.gateway,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise TopologyPlanError(
|
||||||
|
"unsupported service type %r" % service.type.value
|
||||||
|
)
|
||||||
|
|
||||||
|
planned.append(
|
||||||
|
PlannedService(
|
||||||
|
type=service.type,
|
||||||
|
node=node.name,
|
||||||
|
logical_interface=logical_name,
|
||||||
|
interface_name=context.interface_names[key],
|
||||||
|
settings=settings,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return tuple(planned)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================ Public interface ============================
|
||||||
|
|
||||||
|
def plan(topology: Topology) -> ExecutionPlan:
|
||||||
|
"""Lower a validated topology into a fully resolved execution plan.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topology: Topology that has already passed semantic validation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Fully resolved execution plan suitable for source-code rendering.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TopologyPlanError: If a planner precondition is violated or if a
|
||||||
|
validated construct cannot be lowered by the current backend.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This function deliberately does not call validate(). Validation and
|
||||||
|
planning remain separate pipeline stages, and callers are expected to
|
||||||
|
validate the topology before invoking the planner.
|
||||||
|
"""
|
||||||
|
|
||||||
|
context = _build_context(topology)
|
||||||
|
|
||||||
|
nodes = _plan_nodes(topology)
|
||||||
|
links = _plan_links(topology, context)
|
||||||
|
interfaces = _plan_interfaces(topology, context)
|
||||||
|
node_settings = _plan_node_settings(topology, context)
|
||||||
|
services = _plan_services(topology, context)
|
||||||
|
|
||||||
|
return ExecutionPlan(
|
||||||
|
nodes=nodes,
|
||||||
|
links=links,
|
||||||
|
interfaces=interfaces,
|
||||||
|
node_settings=node_settings,
|
||||||
|
services=services,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================== Self-test ===============================
|
||||||
|
|
||||||
|
def _self_test() -> None:
|
||||||
|
"""Run a smoke test against the public plan() interface."""
|
||||||
|
|
||||||
|
from topology_parser import parse_topology
|
||||||
|
from topology_validator import validate
|
||||||
|
|
||||||
|
raw = {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "h1",
|
||||||
|
"type": "host",
|
||||||
|
"interfaces": {
|
||||||
|
"net0": {
|
||||||
|
"addressing": "dhcp",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "s1",
|
||||||
|
"type": "switch",
|
||||||
|
"interfaces": {
|
||||||
|
"client": {},
|
||||||
|
"uplink": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "r0",
|
||||||
|
"type": "router",
|
||||||
|
"interfaces": {
|
||||||
|
"lan": {
|
||||||
|
"addressing": "static",
|
||||||
|
"address": "192.168.1.1/24",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"ip_forward": True,
|
||||||
|
"rp_filter": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"endpoints": [
|
||||||
|
{"node": "h1", "interface": "net0"},
|
||||||
|
{"node": "s1", "interface": "auto"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"endpoints": [
|
||||||
|
{"node": "r0", "interface": "lan"},
|
||||||
|
{"node": "s1", "interface": "uplink"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"type": "dhcp-server",
|
||||||
|
"node": "r0",
|
||||||
|
"interface": "lan",
|
||||||
|
"settings": {
|
||||||
|
"range": {
|
||||||
|
"start": "192.168.1.100",
|
||||||
|
"end": "192.168.1.200",
|
||||||
|
},
|
||||||
|
"gateway": "192.168.1.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
topology = parse_topology(raw)
|
||||||
|
validate(topology)
|
||||||
|
execution_plan = plan(topology)
|
||||||
|
|
||||||
|
assert len(execution_plan.nodes) == 3
|
||||||
|
assert len(execution_plan.links) == 2
|
||||||
|
assert next(
|
||||||
|
node for node in execution_plan.nodes if node.name == "s1"
|
||||||
|
).switch_fail_mode == "standalone"
|
||||||
|
assert execution_plan.links[0].endpoints[1].logical_interface == "client"
|
||||||
|
assert execution_plan.links[0].endpoints[0].interface_name == "h1-eth0"
|
||||||
|
assert execution_plan.links[1].endpoints[0].interface_name == "r0-eth0"
|
||||||
|
assert execution_plan.services[0].interface_name == "r0-eth0"
|
||||||
|
assert any(
|
||||||
|
setting.key == "net.ipv4.ip_forward" and setting.value == 1
|
||||||
|
for setting in execution_plan.node_settings
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_self_test()
|
||||||
|
print("topology_plan: self-test passed")
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
"""Render a resolved execution plan as executable Mininet Python source.
|
||||||
|
|
||||||
|
The renderer is intentionally a serialization stage. It does not perform
|
||||||
|
semantic validation, allocate AUTO interfaces, import Mininet itself, or make
|
||||||
|
networking decisions. All topology decisions must already be represented in
|
||||||
|
the ExecutionPlan produced by topology_plan.plan().
|
||||||
|
|
||||||
|
The returned Python source is both an inspectable intermediate artifact and the
|
||||||
|
canonical executable form of the planned topology.
|
||||||
|
|
||||||
|
Author: Christos Choutouridis <cchoutou@ece.auth.gr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from topology_parser import AddressingMode, NodeType, ServiceType
|
||||||
|
from topology_plan import (
|
||||||
|
ExecutionPlan,
|
||||||
|
PlannedDHCPServerSettings,
|
||||||
|
PlannedInterface,
|
||||||
|
PlannedLink,
|
||||||
|
PlannedService,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================== Rendering helpers ===========================
|
||||||
|
|
||||||
|
def _python_string(value: str) -> str:
|
||||||
|
"""Return a safe Python string literal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: String to serialize into generated Python source.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Python source literal representing the string exactly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return repr(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_link_call(link: PlannedLink) -> List[str]:
|
||||||
|
"""Render one Mininet addLink() call.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
link: Fully resolved link to render.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Source lines implementing the link creation operation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
first, second = link.endpoints
|
||||||
|
settings = link.settings
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
"nodes[%s]" % _python_string(first.node),
|
||||||
|
"nodes[%s]" % _python_string(second.node),
|
||||||
|
"intfName1=%s" % _python_string(first.interface_name),
|
||||||
|
"intfName2=%s" % _python_string(second.interface_name),
|
||||||
|
]
|
||||||
|
|
||||||
|
has_tc_settings = any(
|
||||||
|
value is not None
|
||||||
|
for value in (
|
||||||
|
settings.bandwidth_mbps,
|
||||||
|
settings.delay_ms,
|
||||||
|
settings.loss_percent,
|
||||||
|
settings.jitter_ms,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_tc_settings:
|
||||||
|
arguments.append("cls=TCLink")
|
||||||
|
|
||||||
|
if settings.bandwidth_mbps is not None:
|
||||||
|
arguments.append("bw=%r" % settings.bandwidth_mbps)
|
||||||
|
|
||||||
|
if settings.delay_ms is not None:
|
||||||
|
arguments.append("delay=%s" % _python_string("%gms" % settings.delay_ms))
|
||||||
|
elif settings.jitter_ms is not None:
|
||||||
|
# Linux netem expresses jitter as variation around a base delay. A
|
||||||
|
# zero base delay preserves the declarative meaning when only jitter
|
||||||
|
# was provided in the topology.
|
||||||
|
arguments.append("delay=%s" % _python_string("0ms"))
|
||||||
|
|
||||||
|
if settings.loss_percent is not None:
|
||||||
|
arguments.append("loss=%r" % settings.loss_percent)
|
||||||
|
|
||||||
|
if settings.jitter_ms is not None:
|
||||||
|
arguments.append("jitter=%s" % _python_string("%gms" % settings.jitter_ms))
|
||||||
|
|
||||||
|
lines = [" net.addLink("]
|
||||||
|
for argument in arguments:
|
||||||
|
lines.append(" %s," % argument)
|
||||||
|
lines.append(" )")
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _render_interface_configuration(interface: PlannedInterface) -> List[str]:
|
||||||
|
"""Render configuration commands for one concrete interface.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interface: Fully resolved interface configuration operation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated Python source lines for the interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
node = "nodes[%s]" % _python_string(interface.node)
|
||||||
|
name = _python_string(interface.interface_name)
|
||||||
|
lines: List[str] = []
|
||||||
|
|
||||||
|
if interface.mac is not None:
|
||||||
|
lines.append(
|
||||||
|
" _node_cmd(%s, 'ip', 'link', 'set', 'dev', %s, "
|
||||||
|
"'address', %s)"
|
||||||
|
% (node, name, _python_string(interface.mac))
|
||||||
|
)
|
||||||
|
|
||||||
|
if interface.mtu is not None:
|
||||||
|
lines.append(
|
||||||
|
" _node_cmd(%s, 'ip', 'link', 'set', 'dev', %s, "
|
||||||
|
"'mtu', %s)"
|
||||||
|
% (node, name, _python_string(str(interface.mtu)))
|
||||||
|
)
|
||||||
|
|
||||||
|
if interface.addressing in {
|
||||||
|
AddressingMode.STATIC,
|
||||||
|
AddressingMode.DHCP,
|
||||||
|
}:
|
||||||
|
# addHost(ip=None) prevents Mininet from assigning its default address,
|
||||||
|
# and this flush makes the generated artifact explicit about the
|
||||||
|
# desired IPv4 state before static or manual-DHCP configuration.
|
||||||
|
lines.append(
|
||||||
|
" _node_cmd(%s, 'ip', '-4', 'addr', 'flush', 'dev', %s)"
|
||||||
|
% (node, name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if interface.addressing is AddressingMode.STATIC:
|
||||||
|
lines.append(
|
||||||
|
" _node_cmd(%s, 'ip', 'addr', 'add', %s, 'dev', %s)"
|
||||||
|
% (
|
||||||
|
node,
|
||||||
|
repr(interface.address),
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
interface.addressing in {AddressingMode.STATIC, AddressingMode.DHCP}
|
||||||
|
or interface.mac is not None
|
||||||
|
or interface.mtu is not None
|
||||||
|
):
|
||||||
|
lines.append(
|
||||||
|
" _node_cmd(%s, 'ip', 'link', 'set', 'dev', %s, 'up')"
|
||||||
|
% (node, name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _render_gateway_configuration(interface: PlannedInterface) -> List[str]:
|
||||||
|
"""Render an optional static default route for one interface.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interface: Fully resolved interface configuration operation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated Python source lines, possibly empty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if interface.gateway is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
node = "nodes[%s]" % _python_string(interface.node)
|
||||||
|
|
||||||
|
return [
|
||||||
|
" _node_cmd(%s, 'ip', 'route', 'replace', 'default', 'via', "
|
||||||
|
"%s, 'dev', %s)"
|
||||||
|
% (
|
||||||
|
node,
|
||||||
|
_python_string(interface.gateway),
|
||||||
|
_python_string(interface.interface_name),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _render_service_start(service: PlannedService, index: int) -> List[str]:
|
||||||
|
"""Render startup code for one planned service.
|
||||||
|
|
||||||
|
Construct dnsmasq arguments from the resolved binding and DHCP settings,
|
||||||
|
using the service index for lease, PID, and log filenames.
|
||||||
|
Emit commands that remove previous lease and PID files, launch the daemon
|
||||||
|
in the background, and record its shell PID for later cleanup.
|
||||||
|
The emitted startup sequence does not check daemon readiness.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
service: Fully resolved service operation.
|
||||||
|
index: Stable service index used for temporary runtime files.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated Python source lines that start and track the service.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
node = "nodes[%s]" % _python_string(service.node)
|
||||||
|
lease_file = "/tmp/mininet-dnsmasq-%d.leases" % index
|
||||||
|
pid_file = "/tmp/mininet-dnsmasq-%d.pid" % index
|
||||||
|
log_file = "/tmp/mininet-dnsmasq-%d.log" % index
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"dnsmasq",
|
||||||
|
"--no-daemon",
|
||||||
|
"--conf-file=",
|
||||||
|
"--port=0",
|
||||||
|
"--bind-interfaces",
|
||||||
|
"--dhcp-authoritative",
|
||||||
|
"--interface=%s" % service.interface_name,
|
||||||
|
"--dhcp-range=%s,%s,12h"
|
||||||
|
% (service.settings.range_start, service.settings.range_end),
|
||||||
|
"--dhcp-leasefile=%s" % lease_file,
|
||||||
|
"--pid-file=%s" % pid_file,
|
||||||
|
]
|
||||||
|
|
||||||
|
if service.settings.gateway is not None:
|
||||||
|
args.append(
|
||||||
|
"--dhcp-option=3,%s" % service.settings.gateway
|
||||||
|
)
|
||||||
|
|
||||||
|
rendered_args = ", ".join(_python_string(arg) for arg in args)
|
||||||
|
|
||||||
|
return [
|
||||||
|
" _node_cmd(%s, 'rm', '-f', %s, %s)"
|
||||||
|
% (
|
||||||
|
node,
|
||||||
|
_python_string(lease_file),
|
||||||
|
_python_string(pid_file),
|
||||||
|
),
|
||||||
|
" pid = _start_background(",
|
||||||
|
" %s," % node,
|
||||||
|
" [%s]," % rendered_args,
|
||||||
|
" %s," % _python_string(log_file),
|
||||||
|
" )",
|
||||||
|
" service_processes.append((%s, pid))" % node,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================ Public interface ============================
|
||||||
|
|
||||||
|
def render_python(plan: ExecutionPlan) -> str:
|
||||||
|
"""Render an execution plan as a complete executable Python program.
|
||||||
|
|
||||||
|
Assemble source lines for runtime helpers and the network lifecycle.
|
||||||
|
Emit construction first so interfaces exist before configuration; emit
|
||||||
|
addresses before default routes and services that depend on them.
|
||||||
|
Finish with the interactive CLI and a finally block that attempts tracked
|
||||||
|
service cleanup followed by network shutdown.
|
||||||
|
Rendering itself only produces text and does not execute these operations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plan: Fully resolved execution plan returned by topology_plan.plan().
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Python source code that creates the network, applies configuration,
|
||||||
|
starts services, opens the Mininet CLI, and performs structured cleanup.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
lines: List[str] = [
|
||||||
|
"#!/usr/bin/env python3",
|
||||||
|
'"""Generated Mininet topology. Do not edit by hand."""',
|
||||||
|
"",
|
||||||
|
"import shlex",
|
||||||
|
"",
|
||||||
|
"from mininet.cli import CLI",
|
||||||
|
"from mininet.link import TCLink",
|
||||||
|
"from mininet.net import Mininet",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"def _shell_join(args):",
|
||||||
|
' """Quote command arguments for execution inside a Mininet node."""',
|
||||||
|
"",
|
||||||
|
" return ' '.join(shlex.quote(str(arg)) for arg in args)",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"def _node_cmd(node, *args):",
|
||||||
|
' """Execute one safely quoted command inside a Mininet node."""',
|
||||||
|
"",
|
||||||
|
" return node.cmd(_shell_join(args))",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"def _start_background(node, args, log_path):",
|
||||||
|
' """Start one background process and return its shell PID."""',
|
||||||
|
"",
|
||||||
|
" command = _shell_join(args)",
|
||||||
|
" shell_command = (",
|
||||||
|
" command",
|
||||||
|
" + ' >'",
|
||||||
|
" + shlex.quote(log_path)",
|
||||||
|
" + ' 2>&1 & echo $!'",
|
||||||
|
" )",
|
||||||
|
" return node.cmd('sh -c ' + shlex.quote(shell_command)).strip()",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"def main():",
|
||||||
|
' """Build, configure, expose, and clean up the planned network."""',
|
||||||
|
"",
|
||||||
|
" net = Mininet(controller=None, build=False)",
|
||||||
|
" nodes = {}",
|
||||||
|
" service_processes = []",
|
||||||
|
"",
|
||||||
|
" try:",
|
||||||
|
" # Create nodes.",
|
||||||
|
]
|
||||||
|
|
||||||
|
for node in plan.nodes:
|
||||||
|
if node.type in {NodeType.HOST, NodeType.ROUTER}:
|
||||||
|
lines.append(
|
||||||
|
" nodes[%s] = net.addHost(%s, ip=None)"
|
||||||
|
% (
|
||||||
|
_python_string(node.name),
|
||||||
|
_python_string(node.name),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
" nodes[%s] = net.addSwitch(%s, failMode=%r)"
|
||||||
|
% (
|
||||||
|
_python_string(node.name),
|
||||||
|
_python_string(node.name),
|
||||||
|
node.switch_fail_mode,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.extend(["", " # Create links."])
|
||||||
|
for link in plan.links:
|
||||||
|
lines.extend(_render_link_call(link))
|
||||||
|
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
" # Materialize the Mininet topology.",
|
||||||
|
" net.build()",
|
||||||
|
" net.start()",
|
||||||
|
"",
|
||||||
|
" # Configure concrete interfaces.",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for interface in plan.interfaces:
|
||||||
|
lines.extend(_render_interface_configuration(interface))
|
||||||
|
|
||||||
|
lines.extend(["", " # Configure static default routes."])
|
||||||
|
for interface in plan.interfaces:
|
||||||
|
lines.extend(_render_gateway_configuration(interface))
|
||||||
|
|
||||||
|
lines.extend(["", " # Apply node-level networking settings."])
|
||||||
|
for setting in plan.node_settings:
|
||||||
|
lines.append(
|
||||||
|
" _node_cmd(nodes[%s], 'sysctl', '-w', %s)"
|
||||||
|
% (
|
||||||
|
_python_string(setting.node),
|
||||||
|
_python_string("%s=%d" % (setting.key, setting.value)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.extend(["", " # Start planned services."])
|
||||||
|
for index, service in enumerate(plan.services):
|
||||||
|
lines.extend(_render_service_start(service, index))
|
||||||
|
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
" # DHCP clients are intentionally not started here.",
|
||||||
|
" # The assignment captures DHCP before dhclient is run manually.",
|
||||||
|
" CLI(net)",
|
||||||
|
"",
|
||||||
|
" finally:",
|
||||||
|
" # Stop only the service processes started by this script.",
|
||||||
|
" for node, pid in reversed(service_processes):",
|
||||||
|
" if pid:",
|
||||||
|
" _node_cmd(node, 'kill', pid)",
|
||||||
|
"",
|
||||||
|
" net.stop()",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"if __name__ == '__main__':",
|
||||||
|
" main()",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================== Self-test ===============================
|
||||||
|
|
||||||
|
def _self_test() -> None:
|
||||||
|
"""Run a smoke test against the public render_python() interface."""
|
||||||
|
|
||||||
|
from topology_parser import parse_topology
|
||||||
|
from topology_plan import plan
|
||||||
|
from topology_validator import validate
|
||||||
|
|
||||||
|
raw = {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "h1",
|
||||||
|
"type": "host",
|
||||||
|
"interfaces": {
|
||||||
|
"net0": {
|
||||||
|
"addressing": "dhcp",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "s1",
|
||||||
|
"type": "switch",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "r0",
|
||||||
|
"type": "router",
|
||||||
|
"interfaces": {
|
||||||
|
"lan": {
|
||||||
|
"addressing": "static",
|
||||||
|
"address": "192.168.1.1/24",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"ip_forward": True,
|
||||||
|
"rp_filter": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"endpoints": [
|
||||||
|
{"node": "h1", "interface": "net0"},
|
||||||
|
{"node": "s1", "interface": "auto"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"endpoints": [
|
||||||
|
{"node": "r0", "interface": "lan"},
|
||||||
|
{"node": "s1", "interface": "auto"},
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"delay_ms": 1.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"type": "dhcp-server",
|
||||||
|
"node": "r0",
|
||||||
|
"interface": "lan",
|
||||||
|
"settings": {
|
||||||
|
"range": {
|
||||||
|
"start": "192.168.1.100",
|
||||||
|
"end": "192.168.1.200",
|
||||||
|
},
|
||||||
|
"gateway": "192.168.1.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
topology = parse_topology(raw)
|
||||||
|
validate(topology)
|
||||||
|
source = render_python(plan(topology))
|
||||||
|
|
||||||
|
ast.parse(source)
|
||||||
|
|
||||||
|
assert "net.addHost('h1', ip=None)" in source
|
||||||
|
assert "net.addSwitch('s1', failMode='standalone')" in source
|
||||||
|
assert "net.addLink(" in source
|
||||||
|
assert "cls=TCLink" in source
|
||||||
|
assert "dnsmasq" in source
|
||||||
|
assert "rm', '-f', '/tmp/mininet-dnsmasq-0.leases'" in source
|
||||||
|
assert "CLI(net)" in source
|
||||||
|
assert "dhclient" in source
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_self_test()
|
||||||
|
print("topology_renderer: self-test passed")
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user