Skip to content

Command Reference

sat edited this page Aug 18, 2026 · 17 revisions

Command Reference

This page provides a complete reference for all dot2net commands and their options.

Global Usage

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.

Commands

build - Generate Configuration Files

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.dot

Example with ospf_simple topology:

cd topologies/ospf_simple/
dot2net build -c input.yaml input.dot

This generates:

  • Node directories: r1/, r2/, r3/ containing device-specific configuration files
  • TiNET deployment: spec.yaml for TiNET network emulation
  • Containerlab deployment: topo.yaml for 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
!

Options

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

Running one topology twice

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.yaml for TiNET, topo.yaml for Containerlab)

params - List Available Parameters

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.txt

Example with ospf_simple topology:

cd topologies/ospf_simple/
dot2net params -c input.yaml input.dot

Sample 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

Options

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

visual - Visualize IP Address Assignment

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.svg

Example with ospf_simple topology:

cd topologies/ospf_simple/
dot2net visual -c input.yaml input.dot

Sample 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

Options

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.


data - Export Parameter Data

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.dot

Sample 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)}'

Options

Flag Alias Default Description
--config -c input.yaml Specify the configuration YAML file

files - List Generated Files

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.dot

Example with ospf_simple topology:

cd topologies/ospf_simple/
dot2net files -c input.yaml input.dot

Sample 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.conf is written to r1/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.

Options

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

clean - Delete Generated Files

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.dot

Example with ospf_simple topology:

cd topologies/ospf_simple/
dot2net clean --dry-run -c input.yaml input.dot

Sample 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.dot

The --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"

Options

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

Entry Point Scripts

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 up

exec 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:

  1. the lab's teardown commands, run inside each node while it is still up
  2. the files the lab collects, copied to collected/<node>/<path>
  3. any worker_destroy commands placed before the platform's own destroy
  4. the platform's own destroy
  5. the rest of the worker_destroy commands, 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.

Machine-side hooks

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.

Before or after the platform's own command

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.

Naming what is 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 another

Each 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.

Common Workflows

Development Workflow

# 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

Debugging Workflow

# 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

Exit Codes

  • 0: Success
  • 1: General error (invalid arguments, file not found, configuration conflicts, etc.)

Notes

  • The DOT file is always required as a positional argument
  • Configuration files are referenced relative to the current working directory
  • Use --verbose flag for detailed output when troubleshooting
  • The --dry-run option for clean command is useful for safely previewing deletions
  • Default configuration file name is input.yaml to match project conventions

See Also

Clone this wiki locally