Skip to content

File Output

sat edited this page Aug 14, 2026 · 8 revisions

File Output

This page explains how dot2net generates and controls output files.

Overview

File output in dot2net is controlled by two key concepts:

Concept Role
FileDefinition Defines file properties (name, path, output location)
ConfigTemplate.file Associates a template with a file for output

File Definition

FileDefinitions declare output files with their properties:

file:
  - name: frr.conf
    path: /etc/frr/frr.conf      # where it goes in the container, and where it is written

  - name: check
    output: root                  # Output to lab root directory
    name_suffix: .sh              # Results in "r1.sh", "r2.sh"

A platform's own files are declared by its module, not by your topology. lab.conf, topo.yaml, spec.yaml and Kathara's <device>.startup are output files of the containerlab, TiNET and Kathara modules. You write the content as a template — a node config entry named startup, for instance — and each module puts it where its platform expects it: containerlab in exec:, TiNET in cmds:, Kathara in <device>.startup. Declaring such a file yourself makes two definitions write one file, which dot2net rejects. The file: section is for files that are yours: a check script, an inventory, a report.

FileDefinition Fields

The fields answer three questions: what the file is called, where it ends up, and how it reaches the container.

Field What it decides Default
name The name a config entry uses to say file: <name>. It is the output filename too, unless name_prefix/name_suffix build one Required
path Where the file belongs inside the container — and, from that, where it is written in the output: /etc/frr/frr.conf becomes r1/etc/frr/frr.conf. A file with no path is generated and left in the output; it never reaches a container ""
provide How it gets there. mount shows the container the generated file itself, before the container starts; copy gives the container a copy of its own, once it is up. Which to use, and why it matters mount
scope What one file is generated for: the whole lab (network), each group (group), or each node (node). What each is for node
output Where it is placed, when that is not where its scope would put it. root puts a per-node file at the lab root instead of under the node's own directory — how Kathara's r1.startup, which has to sit beside lab.conf, gets there Inferred from scope
name_prefix Build the filename from the object's own name instead of from name: name_suffix: .startup writes r1.startup, r2.startup ""
name_suffix See name_prefix ""
executable Write the file with the executable bit set. For a script that is meant to be run false

Scope: what a file is generated for

Scope decides how many of this file there are, and what each one can see. A device's own configuration is one per node; a platform's deployment file is one for the whole lab — unless the lab spans machines, in which case it is one per machine, holding that machine's nodes and nothing else. The aggregations follow from the scope: an object can only gather what it owns.

Scope One file per Aggregations it can use
network the whole lab {{ .nodes_<name> }}, {{ .groups_<name> }}
group each group of a class the group's own children — a connection with one end outside the group is not one of them
node (default) each node {{ .interfaces_<name> }}

A group-scoped file is what makes a lab larger than one machine work: each machine's file holds its own nodes and the links it can wire itself. See Placing nodes on machines.

Output Location Control

The output field controls where files are placed:

output_directory/
├── topo.yaml              # scope: network (always in root)
├── r1.sh                  # scope: node, output: root
├── r2.sh                  # scope: node, output: root
├── r1/
│   ├── etc/frr/frr.conf   # a file with path: /etc/frr/frr.conf
│   └── staging/etc/motd   # a file with provide: copy
└── r2/
    └── etc/frr/frr.conf

Examples:

file:
  # Traditional: node config in subdirectory
  - name: frr.conf
    path: /etc/frr/frr.conf
    # output: node (default for node scope)

  # Per-node file in the root directory, named after the node
  - name: check
    output: root
    name_suffix: .sh
    # Results: r1.sh, r2.sh in root

  # Network-scope file (always in root)
  - name: topo.yaml
    scope: network

Filename Generation

When name_prefix or name_suffix is set, the filename is constructed as:

{name_prefix}{object_name}{name_suffix}
Configuration Object Result
name_suffix: .sh r1 r1.sh
name_prefix: config_ r1 config_r1
name_prefix: init_, name_suffix: .sh r1 init_r1.sh

If neither prefix nor suffix is set, the name field is used as the filename.

ConfigTemplate and File Output

A ConfigTemplate generates output to a file when it has a file attribute:

nodeclass:
  - name: router
    config:
      - name: frr_config
        file: frr.conf           # Output to this file
        template:
          - "hostname {{ .name }}"
          - "!"

File Generation Rules

A file is generated for an object only if:

  1. The object has a ConfigTemplate with matching file attribute
  2. The ConfigTemplate conditions are satisfied (required_params, class conditions, etc.)

This means different nodes can generate different sets of files based on their class configurations.

Reaching the container (path and provide)

path says where the file belongs inside the container. Two things follow from it: where the file is written in the output, and how it gets from there into the container.

file:
  - name: frr.conf
    path: /etc/frr/frr.conf    # written to r1/etc/frr/frr.conf

Changed in 0.8.0. A node's files are laid out by the path they take inside the container. path: /etc/frr/frr.conf used to be written to r1/frr.conf and is now written to r1/etc/frr/frr.conf. Anything reading generated files by path has to follow.

provide: mount or copy

Neither is the better one, and which to use follows from what the file is for.

mount (the default) copy
What the container gets the generated file itself its own copy
When before the container's first process runs once the container is up
What the container writes reaches the generated file stays in the container
Can serve a file read while booting yes no
file:
  - name: frr.conf
    path: /etc/frr/frr.conf   # provide: mount, and must be: FRR reads it while booting
  - name: motd
    path: /etc/motd
    provide: copy             # the container may rewrite it; the generated file stays as generated

A mounted file is the generated file, shown to the container. That cuts both ways: what the container writes reaches it, and a container's own startup can take ownership of it.

A copied file waits in a staging directory — r1/staging/etc/motd, mounted read only at /staging — and is copied to its own path by a command the module puts ahead of the topology's own startup commands. Copy cannot serve a file read while booting on any platform: containerlab's exec:, TiNET's cmds: and Kathara's <device>.startup all run after the container has started.

Nothing falls back silently. A combination a platform cannot honour is reported — see Module Kathara, whose mounts are directories rather than files.

Collecting files back

A node class can name files to copy out of the container before the lab is destroyed:

nodeclass:
  - name: router
    collect: ["/var/log/frr.log"]

Each entry is a template, so a path that follows a value stays right when the value is changed. Collected files land in collected/<node>/<the path inside the container>, beside the generated tree rather than in it. A module can declare what it needs back — frrLogFile collects its own log — so a topology that never named the file does not have to name it to get it back.

The entry script does the copying, which is why a topology that collects anything needs one (module_config.<module>.generate_scripts: true). See Command Reference.

See Also

Clone this wiki locally