-
Notifications
You must be signed in to change notification settings - Fork 1
Command Reference
This page provides a complete reference for all dot2net commands and their options.
dot2net [COMMAND] [OPTIONS] <topology.dot>Arguments:
-
<topology.dot>- The DOT file describing the network (required)
Common Options:
-
-c, --config <file>- Configuration YAML file (default:input.yaml) -
-v, --verbose- Enable verbose output
Important: Always run dot2net from the directory containing your configuration files, as the tool references files relative to the current working directory.
Generates configuration files based on the input topology and configuration templates.
dot2net build [OPTIONS] <topology.dot>Examples:
# Basic usage (uses input.yaml by default)
dot2net build input.dot
# Specify custom config file
dot2net build -c custom.yaml input.dot
# With verbose output
dot2net build -v input.dot
# With CPU profiling
dot2net build -p profile.out input.dotExample with ospf_simple topology:
cd topologies/ospf_simple/
dot2net build -c input.yaml input.dotThis generates:
-
Node directories:
r1/,r2/,r3/containing device-specific configuration files -
TiNET deployment:
spec.yamlfor TiNET network emulation -
Containerlab deployment:
topo.yamlfor Containerlab container-based labs
Generated files for each router include:
r1/etc/frr/frr.conf # FRR routing daemon configuration
r1/etc/frr/daemons # FRR daemon startup configuration
r1/etc/frr/vtysh.conf # FRR CLI configuration
Sample r1/etc/frr/frr.conf content:
ip forwarding
!
router ospf
ospf router-id 10.0.255.1
network 10.0.0.0/24 area 0
!
interface eth0
ip address 10.0.0.1/24
!
| Flag | Alias | Default | Description |
|---|---|---|---|
--config |
-c |
input.yaml |
Specify the configuration YAML file |
--name |
(the topology's own) | Name this lab, in place of the topology's name:. Generate one topology under two names to deploy it twice at once — see Running one topology twice. Every command that reads a topology takes it, so files and clean name the same files build wrote |
|
--profile |
-p |
(empty) | Profile CPU performance and output to specified file |
--verbose |
-v |
false |
Enable verbose output |
A lab's containers are named after the lab, so two labs from one topology
collide unless they are told apart. --name is how: it replaces the topology's
own name: for that build, and every platform's file takes the name up in the
way that platform namespaces its containers.
mkdir lab0 lab1
cd lab0 && dot2net build -c ../input.yaml --name lab0 ../input.dot
cd ../lab1 && dot2net build -c ../input.yaml --name lab1 ../input.dot| Platform | What carries the name |
|---|---|
| containerlab | the lab, so a container is clab-lab0-r1
|
| Kathara | the device, so lab.conf holds lab0_r1 and the container is labelled with it |
| TiNET | the node, so spec.yaml holds lab0_r1 — TiNET names a container after the node and nothing else |
What does not change: the nodes are still r1, r2, r3. Their files are
still written to r1/etc/frr/frr.conf, the configuration inside is untouched,
and you still say exec r1 — each entry script knows how its own platform
spells the name.
A name has to be usable as part of a container's name: it must start with a
letter or digit and hold only letters, digits, and _, ., -. dot2net says
so at build time rather than letting the platform fail at deploy.
Output:
- Node-specific configuration directories (e.g.,
r1/,r2/,r3/) - Platform-specific deployment files (
spec.yamlfor TiNET,topo.yamlfor Containerlab)
Lists all parameters available for use in configuration templates.
dot2net params [OPTIONS] <topology.dot>Examples:
# Show basic parameters
dot2net params input.dot
# Show all parameters including relative ones
dot2net params -a input.dot
# Output to file
dot2net params input.dot > params.txtExample with ospf_simple topology:
cd topologies/ospf_simple/
dot2net params -c input.yaml input.dotSample output (excerpt):
network:ospf_simple {{ .name }} = ospf_simple
node:r1 {{ .image }} = quay.io/frrouting/frr:8.5.4
node:r1 {{ .ip_loopback }} = 10.0.255.1
node:r1 {{ .kind }} = linux
node:r1 {{ .name }} = r1
interface:r1.eth0 {{ .conn_name }} = conn0
interface:r1.eth0 {{ .ip_addr }} = 10.0.0.1
interface:r1.eth0 {{ .ip_net }} = 10.0.0.0/24
interface:r1.eth0 {{ .ip_plen }} = 24
interface:r1.eth0 {{ .name }} = eth0
interface:r1.eth0 {{ .node_name }} = r1
interface:r1.eth0 {{ .node_ip_loopback }} = 10.0.255.1
This shows how parameters are automatically assigned:
- IP addresses: Loopback (10.0.255.x) and interface IPs (10.0.0.x/24)
-
Cross-object references:
{{ .conn_name }},{{ .node_name }}, etc. - Platform-specific parameters: Container bind mounts, image specifications
| Flag | Alias | Default | Description |
|---|---|---|---|
--config |
-c |
input.yaml |
Specify the configuration YAML file |
--all |
-a |
false |
Show all parameters including relative ones |
--verbose |
-v |
false |
Enable verbose output |
Output Format:
network {{ .network_name }} = example_network
node:r1 {{ .name }} = r1
node:r1 {{ .ip_loopback }} = 192.168.1.1
Generates a DOT file showing IP address assignments for visualization.
dot2net visual [OPTIONS] <topology.dot>Examples:
# Generate visualization and create PDF
dot2net visual input.dot | dot -Tpdf > addr.pdf
# Visualize specific layer only
dot2net visual -l ipv4 input.dot > ipv4_layout.dot
# SVG output
dot2net visual input.dot | dot -Tsvg > addr.svgExample with ospf_simple topology:
cd topologies/ospf_simple/
dot2net visual -c input.yaml input.dotSample output:
digraph G {
r1->r2 [ dir=none, headlabel="eth0\n10.0.0.2", label="10.0.0.0/24", taillabel="eth0\n10.0.0.1" ];
r2->r3 [ dir=none, headlabel="eth0\n10.0.1.2", label="10.0.1.0/24", taillabel="eth1\n10.0.1.1" ];
r1 [ label="r1\nlo: 10.0.255.1" ];
r2 [ label="r2\nlo: 10.0.255.2" ];
r3 [ label="r3\nlo: 10.0.255.3" ];
}This DOT output shows:
- Node labels: Router names with loopback IP addresses
- Edge labels: Interface names and IP addresses on both ends
- Network labels: Subnet information for each link (10.0.0.0/24, 10.0.1.0/24)
Generate a visual network diagram:
dot2net visual -c input.yaml input.dot | dot -Tpdf > ospf_network.pdf| Flag | Alias | Default | Description |
|---|---|---|---|
--config |
-c |
input.yaml |
Specify the configuration YAML file |
--layer |
-l |
(all layers) | Specify layer name to visualize |
--verbose |
-v |
false |
Enable verbose output |
Requirements: GraphViz must be installed to generate visual output files.
Outputs all calculated parameters in JSON format for external processing.
dot2net data [OPTIONS] <topology.dot>Examples:
# Export to JSON file
dot2net data input.dot > network_data.json
# Use with jq for processing
dot2net data input.dot | jq '.nodes[].name'Example with ospf_simple topology:
cd topologies/ospf_simple/
dot2net data -c input.yaml input.dotSample output (excerpt):
{
"name": "ospf_simple",
"nodes": [
{
"name": "r1",
"params": {
"image": "quay.io/frrouting/frr:8.5.4",
"ip_loopback": "10.0.255.1",
"kind": "linux",
"name": "r1"
},
"interfaces": [
{
"name": "eth0",
"params": {
"conn_name": "conn0",
"ip_addr": "10.0.0.1",
"ip_net": "10.0.0.0/24",
"ip_plen": "24",
"node_name": "r1",
"node_ip_loopback": "10.0.255.1"
}
}
]
}
]
}Extract specific information with jq:
# Get all node names
dot2net data -c input.yaml input.dot | jq -r '.nodes[].name'
# Get all IP addresses
dot2net data -c input.yaml input.dot | jq -r '.nodes[].interfaces[].params.ip_addr'
# Get network topology summary
dot2net data -c input.yaml input.dot | jq '{network: .name, node_count: (.nodes | length)}'| Flag | Alias | Default | Description |
|---|---|---|---|
--config |
-c |
input.yaml |
Specify the configuration YAML file |
Shows what files would be generated by the build command without actually creating them.
dot2net files [OPTIONS] <topology.dot>Examples:
# List all files that would be generated
dot2net files input.dot
# With verbose details
dot2net files -v input.dotExample with ospf_simple topology:
cd topologies/ospf_simple/
dot2net files -c input.yaml input.dotSample output:
r1/etc/frr/daemons
r1/etc/frr/frr.conf
r1/etc/frr/vtysh.conf
r2/etc/frr/daemons
r2/etc/frr/frr.conf
r2/etc/frr/vtysh.conf
spec.yaml
topo.yaml
This shows all files that would be created by dot2net build:
-
Node configuration files: laid out by the path they take inside the
container, so
path: /etc/frr/frr.confis written tor1/etc/frr/frr.conf - Platform deployment files: spec.yaml (TiNET), topo.yaml (containerlab), kathara/lab.conf (Kathara)
With -v each file that reaches a container is listed with where it goes and
how it gets there:
r1/etc/frr/frr.conf /etc/frr/frr.conf mount
r1/staging/etc/motd /etc/motd copy
The plain listing stays a list of paths, because dot2net clean reads it.
| Flag | Alias | Default | Description |
|---|---|---|---|
--config |
-c |
input.yaml |
Specify the configuration YAML file |
--verbose |
-v |
false |
List the container path and how each file is provided |
Removes configuration files that were generated by previous build commands.
dot2net clean [OPTIONS] <topology.dot>Examples:
# Delete all generated files
dot2net clean input.dot
# Preview what would be deleted (dry run)
dot2net clean --dry-run input.dot
# With verbose output
dot2net clean -v input.dotExample with ospf_simple topology:
cd topologies/ospf_simple/
dot2net clean --dry-run -c input.yaml input.dotSample output:
Would delete: r1/etc/frr/daemons
Would delete: r1/etc/frr/frr.conf
Would delete: r1/etc/frr/vtysh.conf
Would delete: r2/etc/frr/daemons
Would delete: r2/etc/frr/frr.conf
Would delete: r2/etc/frr/vtysh.conf
Would delete: r3/etc/frr/daemons
Would delete: r3/etc/frr/frr.conf
Would delete: r3/etc/frr/vtysh.conf
Would delete: spec.yaml
Would delete: topo.yaml
Actually delete the files:
dot2net clean -c input.yaml input.dotThe --dry-run option is especially useful for:
- Verification: Checking what files would be affected before deletion
- Safety: Avoiding accidental deletion of important files
- Debugging: Understanding what files dot2net considers "generated"
| Flag | Alias | Default | Description |
|---|---|---|---|
--config |
-c |
input.yaml |
Specify the configuration YAML file |
--verbose |
-v |
false |
Enable verbose output |
--dry-run |
false |
Show what would be deleted without actually deleting |
With module_config.<module>.generate_scripts: true, dot2net writes a script
beside the lab — containerlab.sh, tinet.sh, kathara/kathara.sh — that
carries the part which differs between platforms and is easy to get wrong.
sudo ./containerlab.sh deploy
sudo ./containerlab.sh exec r1 vtysh -c "show ip ospf neighbor"
sudo ./containerlab.sh collect # copy out what the lab collects
sudo ./containerlab.sh destroy # teardown, collect, destroy, clean upexec reaches the container through docker on all three platforms, so the words
you quoted stay quoted: vtysh -c "show ip ospf neighbor" arrives as two
arguments, not five. The platforms' own exec commands take the whole command as
one string, which loses that.
deploy runs what the lab asked to have done on the machine before handing over
to the platform — see Machine-side hooks below. Anything
you write after deploy is passed on to the platform's own
command — sudo ./containerlab.sh deploy --network lab7 --ipv4-subnet 172.29.7.0/24
places that lab's management network, which is how several labs run side by side.
The management network is the one thing to leave alone if you can.
containerlab's destroy takes no network flags at all, so whatever you place at
deploy time, destroy runs without knowing it — an asymmetry no amount of care at
deploy time removes. The sharp edge: give --ipv4-subnet without
--ipv6-subnet and the lab's own network keeps the default IPv6 range, which
then collides with every later containerlab command, destroy included. The lab
has to be taken apart with docker rm and docker network rm. If you do place
the network, place both families.
TiNET brings a lab up in two commands, so its script has nowhere to put an extra argument and says so rather than dropping it.
destroy takes the lab down in this order:
- the lab's
teardowncommands, run inside each node while it is still up - the files the lab collects, copied to
collected/<node>/<path> - any
worker_destroycommands placed before the platform's own destroy - the platform's own destroy
- the rest of the
worker_destroycommands, run on the machine
A step that fails is named and the rest still run: a lab left standing because something could not be copied is worse than the missing file. The script ends non-zero so that whatever called it knows which steps failed.
startup and teardown run inside a node. Four more blocks run on the machine
the lab is deployed onto, one for each command the script takes, and each named
after it:
| Block | Where it runs |
|---|---|
worker_deploy |
around the command that brings the lab up |
worker_exec |
around the command carried into a node |
worker_collect |
around the copying out of files |
worker_destroy |
around the command that takes the lab down |
They are written on a node class like any other block:
nodeclass:
- name: capture_host
config:
- name: worker_deploy
template:
- "ip link add {{ .name }}-tap type dummy"
- name: worker_destroy
template:
- "ip link del {{ .name }}-tap"worker_destroy here undoes what worker_deploy made, which is what makes the
pair safe to write. A machine's own equipment is not the lab's to remove — a
lab that attaches itself to an interface that was there before it should detach
from it, not delete it. The machine outlives the lab.
Each machine runs what its own nodes asked for, so a lab spanning machines does
not run one machine's commands on another. A block that fails is named the way
every other step is, and deploy stops rather than bringing up a lab whose
machine is not ready.
worker is the same word the worker group class
uses: the machine a lab is deployed onto.
Each of these hooks straddles a command the script gives the platform. Where a
block falls is its priority, and the platform's command is the origin:
below it runs before, above it runs after. Smaller is earlier, as everywhere
else priority is used.
| priority | What sits there |
|---|---|
-100 |
what a module contributes, laid down first |
-50 |
what you write, unless you say otherwise |
0 |
the platform's own command |
+100 |
what a module contributes to worker_destroy, taken up last |
Writing no priority gives the ordinary case: a module's part is the ground
yours stands on — the bridge exists before you attach anything to it — and on
worker_destroy the order reverses, so your cleanup runs before the module
removes the bridge. You only write a priority to say something else.
The one thing that needs saying is a command that could not have run earlier:
- name: worker_deploy
priority: 10 # after containerlab has made the veth
template:
- "tc qdisc add dev {{ .opp_clab_host_port }} root netem delay 1ms"A veth reaching a bridge does not exist until the platform brings the lab up, so a block naming one has to sit above the origin. Leave the priority out and dot2net says so while generating, rather than letting it fail on the machine.
Two names reach the machine's own namespace, and neither is the model's:
| Parameter | What it names |
|---|---|
{{ .clab_bridge }} |
the bridge a switch node became |
{{ .opp_clab_host_port }} |
the veth reaching it, read from the node facing it |
{{ .name }} is not one of these. It is the object's name in the model, and
a command using it builds, passes sh -n, and fails when it runs.
Both are short strings ending in six hex digits — br-a1b2c3, eth0-a1b2c3 —
and the reason is a hard limit rather than a preference. A Linux interface
name is at most 15 characters (IFNAMSIZ is 16 and counts the terminator),
and an OVS bridge is subject to it too, since it comes with an internal device of
the same name. Over-long is worse than refused there: ovs-vsctl reports the
failure and records the bridge anyway, leaving one that no device answers for.
Fifteen characters cannot hold a lab name, a machine name and a node name, so
what they are for — telling one lab's bridge from another's on a machine holding
both — is done by a token derived from them instead. This is the same answer
docker gives when it names a container's host-side veth vethXXXXXXX: a name
nobody types does not have to be readable. Which bridge a token stands for is in
dot2net data, and what you write in a template is the parameter, never the
token.
What is left of the fifteen is eight characters for the interface's own name. An automatic name is a prefix and a number, so a prefix of five leaves room for a thousand ports; a name written in the DOT file has the eight to itself. Longer is reported while generating.
This is what containerlab's bridge setup classes are written in terms of — see Module: containerlab — so a topology's own commands and a module's sit side by side under the same names.
| Variable | Meaning |
|---|---|
DOT2NET_COLLECT_DIR |
Where collected files go (default collected) |
A lab is named by the topology, and the script deploys and destroys it under that name. To run one topology more than once at a time, generate it more than once, giving each build a name of its own:
dot2net build -c input.yaml --name lab0 input.dot # in one directory
dot2net build -c input.yaml --name lab1 input.dot # in anotherEach build's files name their own lab, so the two can be deployed side by side
and each script reaches only its own containers. You still say exec r1: the
script knows how its platform spells the name.
Renaming a lab at deploy time is not offered. containerlab's deploy --name
renames the lab but its destroy takes the name from the topology file whatever
--name says, so a renamed lab cannot be taken down again — and the destroy
reports success while leaving everything running. Naming the lab when it is
generated has no such gap, and works the same way on all three platforms.
The scripts are written executable, and each finds its own files, so they can be run from anywhere. What a lab tears down and collects is declared in the topology — see File Output — and each module's page says what its own script does: containerlab, TiNET, Kathara.
# 1. Design and test
dot2net files input.dot # Preview generated files
dot2net params input.dot # Check parameter assignments
dot2net build -v input.dot # Generate with verbose output
# 2. Deploy and test
sudo containerlab deploy --topo topo.yaml # Deploy with Containerlab
# or
tinet up -c spec.yaml | sudo sh -x # Deploy with TiNET
# 3. Cleanup
dot2net clean input.dot # Remove generated files# Check parameter assignments
dot2net params -a input.dot > debug_params.txt
# Generate with detailed output
dot2net build -v input.dot 2> debug_build.log
# Visualize IP assignments
dot2net visual input.dot | dot -Tpdf > debug_layout.pdf- 0: Success
- 1: General error (invalid arguments, file not found, configuration conflicts, etc.)
- The DOT file is always required as a positional argument
- Configuration files are referenced relative to the current working directory
- Use
--verboseflag for detailed output when troubleshooting - The
--dry-runoption forcleancommand is useful for safely previewing deletions - Default configuration file name is
input.yamlto match project conventions
- Tutorial - These commands in the order you first meet them
- DOT File Syntax / YAML Configuration - What the two input files hold
-
File Output - Where
buildwrites each file, and how it reaches a container -
Module System -
module_config, and what each platform's module adds