Skip to content

Module System

sat edited this page Aug 18, 2026 · 21 revisions

Module System

This page explains how to use modules in dot2net.

What a module is for

A topology says what the network is. A module turns that into something a particular tool can act on. Which module you load depends on what you want done for you, and the modules are not all the same kind of thing:

  • Platform modules — containerlab, tinet, kathara. They write the file their platform reads, so a lab can be deployed by running a command instead of by writing those files by hand: topo.yaml, spec.yaml, kathara/lab.conf, along with the mounts, the links and the commands each of them expresses in its own way. This is where the automation is: the same topology becomes a lab on any of the three.
  • Software modules — frr. They help write the configuration of the software running inside the nodes: a format that turns a block of configuration into vtysh commands, a class that gives a node file logging. They know nothing about how the lab is deployed.
  • Checking modules — assert. They verify what the topology expects of itself, such as a class that must apply to something.
  • builtin is always loaded and supplies the default formats.

Loading a module adds some or all of:

  • File definitions - files the module writes (topo.yaml, spec.yaml, ...)
  • FormatStyles - ready-made formatting rules
  • Classes - some for its own use, some for a topology to use:
  • Requirements - what it needs from the topology, reported when missing

Loading Modules

Modules are loaded via the module section in YAML configuration:

module:
  - containerlab
  - frr

What Happens When a Module is Loaded

1. File Definitions Are Added

Each module defines its output files. For example:

Module File Description
containerlab topo.yaml Containerlab topology file
tinet spec.yaml TiNET specification file

These files are generated automatically based on the network model.

2. FormatStyles Become Available

Modules may provide FormatStyles for specific configuration formats:

# FRR module provides these FormatStyles
interfaceclass:
  - name: ospf
    config:
      - name: ospf_config
        format: FRRVtyshCLI      # Provided by frr module
        template:
          - "router ospf"
          - " network {{ .ip_network }} area 0"
Module FormatStyles
frr FRRVtyshCLI
containerlab clabCmd
tinet tinetSpecCmd

3. Requirements Are Established

Modules may require specific config blocks or parameters:

Module Required Parameters
containerlab image, kind
tinet image
kathara image (a device without one takes Kathara's base image)
frr -

If requirements are not met, dot2net reports an error.

Where a module and a topology meet

A module and a topology have to pass names to each other, and which names may cross is a short list rather than a growing one. Two kinds, and no others:

What crosses Where the topology writes it What it means
the class names a module publishes use: "give this node what that class carries"
the value names a module publishes values: "and set it up like this"
the hook names dot2net itself owns config: - name: "run this, wherever this platform runs such things"

A module's own block names are its business. A topology naming one would be reaching into a module's insides, and the ways the two can talk to each other would multiply with every module and every feature until nobody could say what they are.

The hooks

A hook is a name a topology writes for, without naming a platform. Write startup once and containerlab puts it in exec:, TiNET in cmds:, Kathara in <device>.startup. Without hooks, a topology that runs on three platforms would have to say the same thing three times, in three places, and keep them in step.

Hook When Where each platform puts it
startup once the node is up containerlab exec:, TiNET cmds:, Kathara <device>.startup
teardown while the node is still up, before the lab is destroyed the entry script runs them
worker_deploy before the platform is asked to bring the lab up the entry script, on the machine
worker_exec before a command is carried into a node the entry script, on the machine
worker_collect beside the files copied out of the nodes the entry script, on the machine
worker_destroy after the platform has taken the lab down the entry script, on the machine

The first two run inside a node, the four worker_ ones on the machine it sits on — see Machine-side hooks. A hook is a hook on either side of that line: the topology writes the same way, and which platform is generated for does not change what it wrote.

nodeclass:
  - name: router
    config:
      - name: startup
        template:
          - "ip link set lo up"

A class pulled in with use: can define a template under a hook name too, and what the module has to do wraps what the topology asked for: merged ahead of it where the hook sets something up, and after it where the hook takes something apart (teardown, worker_destroy). A topology's commands run on ground the module has prepared, and the ground is taken up only once they have finished with it. So a topology writes one line and nothing else:

nodeclass:
  - name: router
    use: [frrLogFile]      # the module adds its own commands to this node's startup

Two classes of the topology's own naming one hook is still rejected: nothing would say which of them wins.

Names that begin with an underscore

A module's internal names are _clabNode, _katharaInterface, _clab_src_endpoint and so on. The underscore says the same thing the rule above says: these are not for a topology to name. Everything without one is, and the list below is all of it.

What each module publishes

These are the names a topology may write. Anything a module carries that is not here is its own business, and naming it is reaching inside.

containerlab

Kind Name What it is
class clabOvsBridgeSetup makes and removes an OVS bridge the topology names
class clabLinuxBridgeSetup the same for a Linux bridge
value image the container image
value kind containerlab's own node kind (linux, ovs-bridge, ...)
parameter clab_bridge what a switch node is called on the machine, as opposed to in the model
parameter clab_host_port what the veth reaching that bridge is called on the machine. On a switch node's interfaces, so the node facing one reads {{ .opp_clab_host_port }}
format clabCmd a command as containerlab's exec: takes it
format clabCopy a file copy as containerlab writes one
setting module_config.containerlab.generate_scripts write containerlab.sh
setting module_config.containerlab.management_network give nodes containerlab's management network (off by default)

TiNET

Kind Name What it is
value image the container image
format tinetSpecCmd a command as TiNET's cmds: takes it
setting module_config.tinet.generate_scripts write tinet.sh

Kathara

Kind Name What it is
param_rule kathara_volumes the directories mounted into a device
param_rule kathara_copies the files placed once a device is up
setting module_config.kathara.mount_dirs which directories Kathara mounts
setting module_config.kathara.generate_scripts write kathara/kathara.sh

FRR

Kind Name What it is
class frrLogFile makes the log file and names it to a running FRR
parameter frr_log_path where that log is written
parameter frr_log_level how much FRR writes to it
format FRRVtyshCLI a block as vtysh takes it

assert

Kind Name What it is
value key assert_used states that a value is meant to be read, so that one never read is reported

The hook names — startup, teardown, worker_deploy, worker_exec, worker_collect, worker_destroy — belong to dot2net rather than to any module, and are listed under The hooks above.

{{ .name }} is not one of these. It is the object's name in the model, and for a switch node that is not what the machine calls the bridge - write {{ .clab_bridge }} for that. A command using the wrong one builds, passes sh -n, and fails when it runs.

What the three platforms differ in

A topology is written once, but the platforms are not alike, and where they differ dot2net either hides it or reports it. This table is what was measured while the modules were built; it is here because the differences decide what a topology can ask for.

containerlab TiNET Kathara
Entry file topo.yaml at the output root spec.yaml at the output root kathara/lab.conf — the lab is a directory
How a file reaches a container bind mount, per file bind mount, per file volume, per directory only — hence mount_dirs
Mount source paths relative, resolved against the topology file's directory absolute: the output is piped to a shell, so $PWD/ is prefixed relative, resolved against the lab directory
Where startup commands go exec: cmds: <device>.startup
Are commands run through a shell no — the line is split into words, so &&, pipes and redirection do not work no yes — the startup file is a shell script
Management network attached by default, off in dot2net's output unless asked for none none
Interface names dot2net's own dot2net's own ethN only for the interfaces it lists (deploy: link), derived from their index in lab.conf. A device the node builds for itself keeps the topology's name
A shared medium a bridge node a switches: entry a collision domain, which has no line of its own
More than one machine one topology file per machine one spec file per machine one lab.conf per machine, in a directory of its own. Joining the machines needs Kathara's bridge plugin — see Module: Kathara
A place to describe tests none a test: section in spec.yaml, which dot2net does not write yet ltest was removed in 3.8.0, replaced by the separate kathara-lab-checker

The one that catches people is the shell. A startup command with && in it works on Kathara, whose startup file is a shell script, and goes wrong on the other two in different ways: containerlab splits the line into words and runs it directly, so everything after && becomes an argument and the command still exits 0; TiNET's output is piped to a shell on the host, so the part after && runs there instead of in the container. Write one command per line.

The assert module

assert checks that classes the topology expects to matter really are applied. A class that nothing carries produces nothing, and a class label that never matched is invisible — a typo in a DOT file looks exactly like a class with nothing to say.

module:
  - assert

nodeclass:
  - name: router
    values:
      assert_used: "true"     # at least one object must carry this class

If no object carries it, dot2net reports it rather than generating a lab with a piece silently missing. The mark lives in values so that no core vocabulary is spent on a checking concern.

Combining Modules

Multiple modules can be loaded together. Each module's files are generated simultaneously:

module:
  - containerlab    # Generates topo.yaml
  - frr             # Provides FormatStyles for FRR config

This generates:

  • topo.yaml - Containerlab topology with node definitions
  • r1/etc/frr/frr.conf, r2/etc/frr/frr.conf - FRR configuration files (user-defined)

Example: Containerlab + FRR

module:
  - containerlab
  - frr

file:
  - name: frr.conf
    path: /etc/frr/frr.conf

nodeclass:
  - name: router
    values:
      image: quay.io/frrouting/frr:8.5.4
      kind: linux
    config:
      - file: frr.conf        # From frr module
        template:
          - "hostname {{ .name }}"
          - "!"

      - name: startup
        template:
          - "vtysh -b"

Result:

output/
├── topo.yaml                    # From containerlab module
├── r1/
│   └── etc/frr/frr.conf         # User-defined, laid out by its path in the container
└── r2/
    └── etc/frr/frr.conf

Available Modules

Module Purpose Documentation
containerlab Containerlab topology generation Module: Containerlab
tinet TiNET specification generation Module: TiNET
kathara Kathara lab.conf generation Module: Kathara
frr FRR FormatStyle and file logging Module: FRR
assert Checks that classes the topology expects really are applied Module System

Writing a module

The interfaces a module implements, and when each one runs, are on Module Development.

See Also

Clone this wiki locally