-
Notifications
You must be signed in to change notification settings - Fork 1
YAML Configuration
This page explains the YAML configuration system in dot2net, including class definitions, template syntax, and configuration patterns.
YAML configuration files define the generalized configuration part of topology-driven configuration. While DOT files describe "what" the network topology is, YAML files describe "how" each type of device should be configured.
Global settings control project-wide behavior:
global:
path: local # "local" = files in config dir, "default" = working dir
nodeautoname: true # Enable automatic node renaming
max_address_count: 65536 # Cap on addresses/prefixes enumerated per pool
ignore_undefined_class: false # Skip (true) or error (false) on undefined class labels
output_group_class: worker # One output directory per group of this class
aggregate_crossing_links: true # Replace a segment reaching across machines with bridges
split_module_output: false # Put each module's own files in a directory named after itKey attributes:
-
path: File path specification for config files referenced in YAML-
"local": Paths relative to input.yaml directory -
"default": Paths relative to current working directory (shell execution location)
-
-
nodeautoname: Automatic node renaming-
true: Ignore DOT file node names, generate names using NodeClass prefix settings -
false: Use DOT file node names as-is - Uses same mechanism as automatic interface naming
-
-
max_address_count: Upper bound on how many addresses/prefixes are enumerated when an address pool is expanded fully (loopback / segment / reservation handling). Bounds internal work so an oversized (e.g. IPv6) pool cannot blow up memory.- Default (
0, negative, or unset):65536(comfortably fits a/16) - Increase it if a single pool legitimately needs more than 65536 entries
- Default (
-
ignore_undefined_class: How a class label that matches no defined class is handled-
false(default): it is an error (catches typos in class names) -
true: it is silently skipped — useful when a label is meant for display rather than as a class (e.g. a subgraphlabelused only as a caption)
-
dot2net supports automatic naming for multiple object types using prefix-based naming schemes. This ensures consistent, predictable object names across your network configuration.
Always Enabled:
- Interfaces: All interfaces are automatically named using InterfaceClass prefix
- Connections: All connections are automatically named using ConnectionClass prefix
- Network Segments: All detected network segments are automatically named using SegmentClass prefix
Configurable:
-
Nodes: When
nodeautoname: truein global settings, uses NodeClass prefix
| Object Type | Default Prefix | Example Names |
|---|---|---|
| Interface | "eth" |
eth0, eth1, eth2
|
| Connection | "conn" |
conn0, conn1, conn2
|
| Segment | "seg" |
seg0, seg1, seg2
|
| Node | "node" |
node0, node1, node2
|
You can customize naming prefixes in class definitions using the prefix attribute:
# Custom interface naming
interfaceclass:
- name: mgmt_interface
prefix: "mgmt" # Generates: mgmt0, mgmt1, mgmt2...
# Custom connection naming
connectionclass:
- name: vlan_trunk
prefix: "trunk" # Generates: trunk0, trunk1, trunk2...
# Custom segment naming
segmentclass:
- name: network_segment
prefix: "mgmt" # Generates: mgmt0, mgmt1, mgmt2...
# Custom node naming (when nodeautoname: true)
nodeclass:
- name: router
prefix: "rtr" # Generates: rtr0, rtr1, rtr2...All automatically named objects can reference their assigned names in templates using {{ .name }}:
# Connection template using auto-assigned name
connectionclass:
- name: vlan_connection
prefix: "vlan"
config:
- name: connection_setup
template:
- "# Connection: {{ .name }}" # Outputs: vlan0, vlan1, etc.
# Segment template using auto-assigned name
segmentclass:
- name: network_segment
prefix: "seg"
config:
- name: network_entry
template:
- "- name: {{ .name }}" # Outputs: seg0, seg1, etc.- Consistency: Sequential numbering ensures predictable naming patterns
-
Template compatibility: All objects support
{{ .name }}template references - Cross-object references: Reliable names enable stable inter-object relationships
- Configuration portability: Generated names work consistently across environments
Define output files and their properties. For detailed explanation, see File Output.
file:
- name: frr.conf
path: /etc/frr/frr.conf # Target path on nodes
scope: node # "node" (per-node, default), "group" (per group) or "network" (one file)
format: frr_format # Reference to format definition
output: node # "node" (subdirectory) or "root" (lab root)
name_prefix: "" # Prefix for output filename
name_suffix: "" # Suffix for output filenameDefine text formatting for generated files. dot2net uses a two-phase formatting approach:
format:
- name: frr_format
# Format Phase (applied during config block generation)
format_lineprefix: " "
format_linesuffix: ""
format_lineseparator: "\n"
format_blockprefix: ""
format_blocksuffix: "!"
# Merge Phase (applied when combining multiple blocks)
merge_blockseparator: "\n"
merge_resultprefix: "" # Optional: wrap entire merged result
merge_resultsuffix: "" # Optional: wrap entire merged resultTwo-Phase Processing:
-
Format Phase - Individual config block formatting:
-
format_lineprefix: Added before each line -
format_linesuffix: Added after each line -
format_lineseparator: Used to join lines (default:"\n") -
format_blockprefix: Added before entire config block -
format_blocksuffix: Added after entire config block
-
-
Merge Phase - Combining multiple formatted blocks:
-
merge_blockseparator: Used to join multiple config blocks (default:"\n") -
merge_resultprefix: Added before the entire merged result (optional) -
merge_resultsuffix: Added after the entire merged result (optional)
-
Backward Compatibility (v0.6.x):
-
lineprefix→format_lineprefix -
linesuffix→format_linesuffix -
lineseparator→format_lineseparator -
blockprefix→format_blockprefix -
blocksuffix→format_blocksuffix -
blockseparator→merge_blockseparator
Real-world examples:
# FRR vtysh command format
format:
- name: frr_vtysh
format_lineseparator: "\" -c \""
format_blockprefix: "vtysh -c \"conf t\" -c \""
format_blocksuffix: "\""
# Containerlab YAML array format
format:
- name: clab_yaml
merge_blockseparator: ", "
# Containerlab command list format
format:
- name: clab_cmd
format_lineprefix: " - "
merge_blockseparator: "\n"Complete example demonstrating all attributes:
format:
- name: comprehensive_format
# Format Phase
format_lineprefix: " " # Indent each line
format_linesuffix: ";" # Add semicolon to each line
format_lineseparator: " \\\n" # Line continuation with backslash
format_blockprefix: "START {\n" # Block opening
format_blocksuffix: "\n} END" # Block closing
# Merge Phase
merge_blockseparator: "\n---\n" # Separator between blocks
merge_resultprefix: "# BEGIN\n" # Wrap entire result
merge_resultsuffix: "\n# END" # Wrap entire resultProcessing flow example:
Input template: ["router ospf", "network 10.0.1.0/24 area 0"]
↓ Format Phase - formatConfigLines (comprehensive_format)
Step 1 - Apply line formatting (format_lineprefix/suffix/separator):
" router ospf; \\\n network 10.0.1.0/24 area 0;"
↓ Format Phase - formatSingleConfigBlock
Step 2 - Apply block formatting (format_blockprefix/suffix):
"START {\n router ospf; \\\n network 10.0.1.0/24 area 0;\n} END"
↓ Merge Phase - mergeConfigBlocks (multiple blocks)
Step 3 - Join blocks with merge_blockseparator:
"Block1\n---\nBlock2\n---\nBlock3"
↓ Merge Phase - wrap result (merge_resultprefix/suffix)
Step 4 - Wrap entire result:
"# BEGIN\nBlock1\n---\nBlock2\n---\nBlock3\n# END"
When Merge Phase is used:
- Sorter templates (
style: sort) that combine multiple config blocks -
blocks.before/blocks.afterfeatures that merge child configs
Define protocol layers and IP policies:
layer:
- name: ip
default_connect: true # Default layer for connections
policy:
- name: ip
range: 10.0.0.0/16
prefix: 24
- name: lo
type: loopback
range: 10.0.255.0/24Key attributes:
-
name: Layer identifier (used in template variables) -
default_connect: Default connection layer for IP segment discovery-
true: ConnectionClasses without explicit layer specification are considered connected on this layer -
false: Not used for default connections - Used by
DefaultConnectionLayer()function for segment exploration
-
-
policy: Array of IP address policies for this layer
IP Policy attributes:
-
name: Policy identifier -
type: Policy type ("ip"for normal interfaces,"loopback"for loopback interfaces) -
range: IP address range (CIDR notation) -
prefix: Default prefix length for subnets
Generated template variables: Each layer automatically provides template variables:
-
{{ .{name}_addr }}- IP address -
{{ .{name}_net }}- Network address -
{{ .{name}_plen }}- Prefix length -
{{ .{name}_protocol }}- Protocol identifier -
{{ .{name}_loopback }}- Loopback address
Example usage:
# Template can use:
template:
- "interface {{ .name }}"
- " ip address {{ .ip_addr }}/{{ .ip_plen }}"
- " description Connected to {{ .ip_net }}"Configure management network independent of main topology (optional):
mgmt_layer:
name: mgmt # Layer name identifier
range: 192.168.1.0/24 # IP address range for management interfaces
gateway: 192.168.1.1 # External gateway (reserved from auto-assignment)
interface_name: mgmt0 # Management interface name (default: "mgmt0")Key attributes:
-
name: Management layer identifier (used in template variables) -
range: IP address range for automatic assignment to management interfaces -
gateway: External gateway address (automatically reserved from assignment pool) -
interface_name: Name for management interface on each node (default:"mgmt0")
Behavior:
- Independent of topology: Management interfaces are automatically created on every node
- Separate IP space: Uses dedicated IP range independent of main network layers
- External connectivity: Designed for out-of-band management (Containerlab external networks)
-
Automatic creation: Management interfaces added during
addSpecialInterfaces()processing
Generated template variables:
-
{{ .{name}_addr }}- Management IP address -
{{ .{name}_net }}- Management network address -
{{ .{name}_plen }}- Management prefix length
Usage with Containerlab: Management layer integrates with Containerlab's external network connectivity for out-of-band device management.
Example:
mgmt_layer:
name: mgmt
range: 10.255.255.0/24
gateway: 10.255.255.1
interface_name: eth0This creates an eth0 management interface on each node, addressed from
10.255.255.0/24.
containerlab keeps
eth0for its management interface, and since 0.8.0 it is not attached by default — see module_config. A topology that turns it back on has to name its data interfaces something other thaneth0, through an interface classprefix; dot2net says so while generating rather than letting containerlab refuse at deploy time.
Define automatic parameter assignment:
param_rule:
- name: vlan_id
assign: segment # "object" (default), "segment", "connection"
layer: ip # Required when assign: "segment"
type: integer # "integer" (default) or "file"
min: 100
max: 1001
header: vlan # Prefix for generated values
footer: "" # Suffix for generated values
- name: conn_id
assign: connection
header: conn
min: 0
- name: hostname_list
type: file
sourcefile: ./hostnames.txt # Read values from fileKey attributes:
-
name: Parameter rule identifier (must match parameter names in class definitions) -
assign: Assignment scope-
"object"(default): Individual assignment per node/interface -
"segment": Same value for interfaces in the same network segment -
"connection": Same value for interfaces connected by the same connection
-
-
layer: Protocol layer specification (required whenassign: "segment") -
type: Parameter generation type-
"integer"(default): Generate numeric sequences -
"file": Read values from external file
-
Integer type attributes:
-
min: Starting value (default: 0) -
max: Maximum value (optional, for validation) -
header: Prefix string (default: "") -
footer: Suffix string (default: "") -
Generation pattern:
{header}{min+i}{footer}
File type attributes:
-
sourcefile: Path to source file (relative to config file) - Format: One value per line, read sequentially
Assignment behavior examples:
# Example: VLAN assignment per segment
param_rule:
- name: vlan_id
assign: segment
layer: ip
min: 100
header: vlan
# Result: vlan100, vlan101, vlan102... (same value for all interfaces in each segment)
# Example: Connection ID per connection
param_rule:
- name: conn_id
assign: connection
header: conn
min: 0
# Result: conn0, conn1, conn2... (same value for both ends of each connection)
# Example: Individual interface numbering
param_rule:
- name: if_num
assign: object # or omit (default)
min: 1
# Result: 1, 2, 3... (unique value per interface)The Value class allows attaching multiple parameter sets to a single object. This is useful for topologies like:
- Multiple VLAN configurations per switch
- Multiple static routes per router
- Dynamic file mount configurations
Basic Structure:
param_rule:
- name: vlan_ids
mode: attach # Required for Value class
source:
type: range # "range", "sequence", "list", or "file"
start: 100
end: 103
param_format:
vlan_id: "{{ .value }}"
config:
- name: vlan_entry
template:
- "vlan {{ .vlan_id }}"Mode:
-
distribute(default): One value per object (legacy behavior) -
attach: Multiple Values attached to one object
Source Types:
| Type | Description | Fields |
|---|---|---|
range |
Numbers counted off one per object |
start, end
|
sequence |
The same, put into a format string — for a name or an identifier built around a number |
start, end, format
|
list |
Values written out, handed out in order |
values (array) |
file |
Values read from a file, for a list too long or too external to write in the topology | file |
Template Reference:
In class config templates, reference Values using values_ prefix:
nodeclass:
- name: switch
params: [vlan_ids] # Reference the param_rule
config:
- name: startup
template:
- "! VLAN Configuration"
- "{{ .values_vlan_entry }}" # Reference formatted Value outputComplete Example:
# Define param_rule with attach mode
param_rule:
- name: static_routes
mode: attach
source:
type: list
values:
- { network: "10.0.0.0/8", gateway: "192.168.1.1" }
- { network: "172.16.0.0/12", gateway: "192.168.1.2" }
config:
- name: route_entry
template:
- "ip route {{ .network }} {{ .gateway }}"
# Reference in node class
nodeclass:
- name: router
params: [static_routes]
config:
- file: router.conf
template:
- "! Static Routes"
- "{{ .values_route_entry }}"Module Generators:
Modules can provide generators for dynamic Value creation:
# Containerlab module provides clab.filemounts generator
# TiNET module provides tinet.filemounts generator
# These are used internally for bind mount generationLoad external modules. For details on how modules work, see Module System.
module:
- tinet # TiNET spec.yaml generation
- containerlab # Containerlab topo.yaml generation
- kathara # Kathara lab.conf generation
- frr # FRR configuration helpersSettings that belong to one platform rather than to the topology go under
module_config, in a section named after the module. global: is for what
every platform shares; what a module offers is its own, and a section naming a
module the topology does not load is rejected, since it does nothing and is
nearly always a typo.
module_config:
containerlab:
management_network: false # default: no management network at all
generate_scripts: false # write containerlab.sh beside the lab
kathara:
mount_dirs: [/etc/frr] # directories dot2net supplies entirely
generate_scripts: false
tinet:
generate_scripts: false| Setting | Meaning |
|---|---|
containerlab.management_network |
Put containerlab's management network back. It is off by default: a second path between every pair of nodes is one a reachability test can pass through without anyone noticing. Turning it on means naming the data interfaces something other than eth0. |
<module>.generate_scripts |
Write an entry point script (containerlab.sh, tinet.sh, kathara.sh) that deploys, destroys, collects and execs. See Command Reference. |
kathara.mount_dirs |
Directories inside a device that dot2net supplies entirely. See Module: Kathara. |
virtual: true says the object shapes the topology without being deployed. What
that means depends on what carries it, and each is decided on its own:
| On a | What it means |
|---|---|
| node class | Nothing is deployed for the node — and no link to it is either, since there is nothing to connect. It still holds parameters and still shapes the topology, which is what makes it useful for laying out addresses. deploy: none is the fuller spelling |
| interface class | The interface is not written into the platform's file. It does not remove the link it sits on — whether a link is emitted follows from the connection, not from this |
| connection class | The connection is not written as a link. This is what a shared medium modelled without a real link uses |
Changed in 0.8.0. A virtual interface used to take its connection with it, so marking an interface virtual silently removed the link. It no longer does. A topology that relied on that gets the link — say it on the connection instead.
Whether a platform is told to make a link is a property of the template that
writes it (required_link), not of virtual. See
ConfigTemplate Field Reference.
virtual: false claims nothing at all, which is how the boolean has always
behaved: it does not override a class that says deploy: none. Writing both
virtual: true and a deploy that contradicts it is an error.
An object usually carries more than one class: what the DOT file names, what
class_policy adds, what a class pulls in with use:, and what a module
attaches. They are combined rather than one overriding the rest, and where two
of them say different things, the stronger one wins.
Weakest to strongest:
| Where it comes from | |
|---|---|
| 1. module | classes a module attaches, and classes pulled in with use: from a module |
| 2. base | classes named in class_policy.<type>.base
|
| 3. the author's own | classes named in the DOT file, and class_policy.<type>.default
|
| 4. value labels in the DOT file |
r1 [image=...] — applied before any class, so they beat all of them |
Modules are the weakest on purpose: they supply defaults, and what the author
writes has to win over them. A module that must force a value does not win
silently — it checks and reports, which is why asking Kathara for an interface
prefix other than eth is an error rather than a setting quietly ignored.
Two classes of the same strength saying different things is an error, not a race. There is nothing to say which of them the author meant.
class_policy:
node:
base: [common]
nodeclass:
- name: common
values: {image: quay.io/frrouting/frr:8.5.4} # weaker
- name: edge
values: {image: quay.io/frrouting/frr:9.0.0} # written on the node: winsWhat "wins" means depends on the field:
| Field | How they combine |
|---|---|
values, params, collect
|
added together. Every class contributes; two of the same strength setting the same key differently is the error above |
deploy, prefix, and other single-valued fields |
the strongest wins. A class that says nothing makes no claim, so it never conflicts with anything |
config |
added together, and names must not collide at all. Two classes defining a config entry of the same name is an error whatever their strength: a template is not a value to be overridden, and a topology cannot replace a module's. The exception is a hook name, where a module's part and the topology's are merged on purpose |
use |
attaches the named class. It is not inheritance: the attached class stands beside this one, keeping the strength it came with — so a module's class stays weaker than what you write |
Two class names used to be magic: all applied to every object, and default
applied to objects with no class label. class_policy says the same thing where
it can be seen, and frees both names for ordinary use.
class_policy:
node:
base: [common] # applies to every node
default: [router] # applies to a node that carries no class label at all
interface:
default: [ip]
group:
default: [worker]-
baseclasses apply to every object of that type, and sit below anything the object names itself: a class written on the object wins. -
defaultclasses stand in for a class the author did not write, so they count as the author's own.
The old names still work, with a warning, and will be removed in a later release.
dot2net uses a comprehensive class system to organize network objects and their configurations:
NetworkClass defines network-wide configuration:
networkclass:
- name: main_network
values:
project: "example_network"
version: "1.0"
config:
- file: network_info.txt
template:
- "# Network: {{ .project }} v{{ .version }}"NodeClass defines device types and their basic properties:
nodeclass:
- name: router
use: [frrLogFile] # pull in another class, module-provided or your own
deploy: container # container (default) | platform | none
values:
kind: linux
image: quay.io/frrouting/frr:8.5.4
interface_policy: [ip]
params: [lo]
collect: ["/var/log/frr.log"] # copy out before the lab is destroyed
config:
- file: frr.conf
template:
- "router ospf"
- " ospf router-id {{ .ip_loopback }}"Key attributes:
-
use- Attach another class to whatever carries this one. It attaches, rather than overriding: the two classes' fields are combined by the ordinary rules, and a module's value loses to yours. See Module System -
deploy- What the platform puts in place for this node:-
container(the default) - a container of its own -
platform- something the platform provides rather than deploys: a bridge on containerlab, aswitches:entry on TiNET, a collision domain on Kathara -
none- nothing. The node shapes the topology and is not deployed -
virtual: trueis the older spelling ofdeploy: noneand still works
-
-
values- Default parameter values -
interface_policy- IP assignment policies for interfaces -
params- Parameter rules for automatic assignment -
collect- Files to copy out of the node before the lab is destroyed. Each is a template. See File Output -
config- Configuration template blocks
InterfaceClass defines interface types and behaviors:
interfaceclass:
- name: vlan
layers: [ip] # Restrict to specific layers
params: [vlan]
config:
- group: params.txt
priority: -1 # Processing priority (lower = earlier)
template:
- "vlan {{ .vlan }} for {{ .node_name }}.{{ .name }}"Key attributes:
-
layers- Specify which protocol layers this class applies to -
priority- Control processing order within groups -
group- Accumulate configuration blocks for later merging -
neighbors- Reference adjacent interfaces for iterative configuration -
classmembers- Reference objects in the same class for member-based configuration
ConnectionClass defines connection types and properties (new specification):
connectionclass:
- name: vlan_conn
prefix: "vlan_trunk" # Auto-naming prefix for connections
params: [conn_id, vlan_id] # Auto-assigned by parameter rules
config:
- group: network_config.txt
priority: -2
template:
- "# VLAN Connection: {{ .name }} (ID: {{ .conn_id }}, VLAN: {{ .vlan_id }})"
- "connection {{ .name }} type vlan_trunk vlan {{ .vlan_id }}"Key attributes:
-
prefix- Automatic naming prefix (generates names likevlan_trunk0,vlan_trunk1) -
params- Parameter rules for automatic value assignment - Connection templates use
{{ .name }}for self-reference
SegmentClass defines network segment configurations (new specification):
segmentclass:
- name: network_segment
layer: ip
prefix: "net" # Auto-naming prefix for segments
params: [segment_id] # Auto-assigned by parameter rules
config:
- name: network_entry
template:
- "- name: {{ .name }}" # Auto-assigned segment name
- " vlan: auto"
- " nodes:"
- "{{ .interfaces_segment_nodes }}"Key attributes:
-
layer- Protocol layer for this segment type -
prefix- Automatic naming prefix (generates names likeseg0,seg1) -
params- Parameter rules for automatic value assignment - Segments are automatically detected from network topology
- Segment templates use
{{ .name }}for self-reference - Used with relational class labels (
segment#class_name)
GroupClass defines logical groupings (subnets, ASes, clusters):
groupclass:
- name: backbone_area
virtual: false # a group that shapes parameters without being deployed
params: [ospf_area_id] # Parameter rules for automatic assignment
values:
area_type: "backbone"
priority: "high"
config:
- file: area_config.txt
template:
- "# OSPF Area {{ .ospf_area_id }} ({{ .area_type }})"
- "area {{ .ospf_area_id }} authentication"Key attributes:
-
virtual- Exclude from final output if true -
params- Parameter rules for automatic assignment -
values- Default parameter values -
boundary_crossing_connection_class- A connection class for the links that leave a group of this class. Which links those are follows from the topology, so no edge carries an annotation saying it a second time
A lab larger than one machine is drawn as one topology, and the machines are
subgraphs of a group class named worker — the one name dot2net reserves for a
placement unit.
global:
output_group_class: worker # one output directory per machine
groupclass:
- name: worker
boundary_crossing_connection_class: crosses_hostgraph {
subgraph host1 { xlabel="worker"; r1; r2; }
subgraph host2 { xlabel="worker"; r3; r4; }
r2 -- r3; # this link leaves a machine
}
What follows from naming it:
-
containerlab writes one
topo.yamlper machine, and TiNET onespec.yaml. Each holds the nodes of that machine and the links it can wire itself; a link that leaves a machine appears in neither, since no topology file can make it -
A node in two
workergroups is rejected — it cannot be in two places -
A shared segment reaching across machines is replaced with one bridge per
machine, linked to each other, so the segment costs one link leaving a
machine instead of one per member on the far side. Each such link costs a VLAN
from a finite pool, which is what makes the difference worth having.
global.aggregate_crossing_links: falseturns it off -
Kathara cannot express this at all and says so: a
lab.confdescribes one machine
The links that leave a machine are yours to make — that is what
boundary_crossing_connection_class is for. example/ospf_multihost writes
them into a file for whoever wires the machines together.
dot2net supports two template description styles for assembling configuration templates. For conceptual understanding of when to use each approach, see Template System - Template Assembly Approaches.
ConfigTemplates can be coordinated in several ways:
| Mechanism | Purpose | Description |
|---|---|---|
| Hierarchical | Template embedding | Embed child templates via {{ .self_xxx }} or {{ .interfaces_xxx }}
|
| Sort | Group aggregation | Collect templates into groups, then merge with sorting |
| blocks.before/after | Output ordering | Control position of blocks relative to other templates |
| required_params | Conditional output | Skip entire block if parameters are missing |
| depends | Processing order | Required for same-object template embedding (e.g., {{ .self_xxx }}) |
Typical combinations:
- Hierarchical + blocks: Use Hierarchical for embedding, blocks for cross-class ordering (e.g., interface configs before router section)
- Sort + blocks: Aggregate by priority, with additional ordering constraints
- Any style + required_params: Add conditional output to optional configuration sections
Default approach with strict template relationship management:
config:
- file: ospfd.conf
template:
- "router ospf"
- " ospf router-id {{ .RouterID }}"
- " {{ .interfaces_ospf_network }}" # Embedded template positionCharacteristics:
- Strict ordering: Template embedding positions must be explicitly defined
- Precise relationships: Template dependencies are strictly managed
- Complex control: Supports intricate dependency relationships
Advantages:
- Strict order control
- Clear template relationships
- Accurate expression of complex dependencies
Disadvantages:
- More complex to write
- Requires detailed position specification
Alternative approach using loose relationship management with Group Templates + Sorter Templates:
Accumulate configuration blocks for later processing:
config:
- group: "ospf6d.conf"
priority: -1 # Order control via priority only
template:
- "log file /var/log/frr.log"Merge and output accumulated group configurations:
config:
- file: "ospf6d.conf"
style: sort
sort_group: "ospf6d.conf" # References group name
template:
- "router ospf6"
- " ospf6 router-id {{ .RouterID }}"Characteristics:
- Simple addition: Just add templates to defined groups
- Loose management: Template relationships are loosely controlled
- Priority-based ordering: Order control through priority values only
Advantages:
- Simple description
- Easy template addition
- Flexible management approach
Disadvantages:
- Difficult to express strict dependencies
- Not suitable for complex order control
Core mechanism for template embedding relationships in Hierarchical style:
config:
- name: "base_config"
template:
- "base settings"
- name: "advanced_config"
depends: ["base_config"] # Required for same-object template dependencies
template:
- "advanced settings"
- "{{ .base_config }}" # Template embeddingDependency Requirements:
-
Cross-object embedding (Parent-Child relationships):
# NodeClass template embedding InterfaceClass templates template: - "interface configuration:" - "{{ .interfaces_config }}" # No depends needed - hierarchy is clear
-
Same-object embedding (Same-level templates):
# Templates within the same object class config: - name: "base" template: ["base settings"] - name: "extended" depends: ["base"] # Required - processing order matters template: ["{{ .base }}", "extended settings"]
Purpose:
- Essential for Hierarchical style: All template relationships require named templates
- depends required only for same-object template dependencies (reorderConfigTemplates processing)
- Parent-child class relationships have implicit dependencies through object hierarchy
The required_params field allows templates to be conditionally skipped when specified parameters are missing or empty:
nodeclass:
- name: router
config:
# Always output
- name: base_config
template: ["hostname {{ .name }}"]
# Only output if 'mem' parameter exists and is non-empty
- name: memory_config
required_params: [mem]
template: ["memory {{ .mem }}"]
# Only output if both 'cpus' AND 'sysctl' exist
- name: resource_config
required_params: [cpus, sysctl]
template:
- "cpus {{ .cpus }}"
- "sysctl {{ .sysctl }}"Behavior:
- If all parameters in
required_paramsexist and are non-empty → template is processed - If any parameter is missing or empty → entire template block is skipped
- Empty string values are treated as "not exists"
Use cases:
- Optional parameters (mem, cpus, sysctl) in container configs
- Module-provided templates that depend on user-defined parameters
- Avoiding if-statements in templates for cleaner, declarative configuration
Comparison with template if-statements:
| Approach | Pros | Cons |
|---|---|---|
required_params |
Declarative, no template logic | Block-level only |
{{ if .param }} |
Fine-grained control | Template becomes complex |
Note: required_params checks both regular parameters and relative parameters (e.g., self_startup, values_*).
The blocks field controls where a template's output is positioned relative to other templates:
nodeclass:
- name: router
config:
# Main router config - other blocks position relative to this
- name: router_base
file: frr.conf
template:
- "router ospf"
- " router-id {{ .ip_loopback }}"
interfaceclass:
- name: default
config:
# Interface config should appear BEFORE router_base in frr.conf
- name: interface_config
file: frr.conf
blocks:
before: [router_base]
template:
- "interface {{ .name }}"
- " ip address {{ .ip_addr }}/{{ .ip_plen }}"
# Network statements should appear AFTER router_base
- name: ospf_network
file: frr.conf
blocks:
after: [router_base]
template:
- " network {{ .ip_net }} area 0"Result in frr.conf:
interface eth0
ip address 10.0.0.1/24
interface eth1
ip address 10.0.0.5/24
router ospf
router-id 10.255.0.1
network 10.0.0.0/24 area 0
network 10.0.0.4/24 area 0
Behavior:
-
blocks.before: [name1, name2]- This template's output appears before the listed templates -
blocks.after: [name1, name2]- This template's output appears after the listed templates - Multiple templates can be listed; ordering is determined by dependency resolution
- Works across different object classes (e.g., InterfaceClass referencing NodeClass templates)
Use cases:
- FRR/Quagga configs where interface definitions must precede router sections
- Any config format requiring specific section ordering
- Separating logical config sections while maintaining correct output order
Relationship with depends:
-
dependscontrols processing order (template A must be evaluated before template B) -
blocks.before/aftercontrols output order (template A's output appears before/after template B's output) - Both can be used together when needed
Templates have access to a symmetric parameter namespace:
-
{{ .name }}- Object name -
{{ .ip_addr }}- IP address -
{{ .ip_plen }}- IP prefix length -
{{ .vlan_id }}- VLAN identifier
Templates can reference related objects with consistent prefixes:
template:
- "interface {{ .name }}"
- " description Connected via {{ .conn_name }}"
- " vlan {{ .conn_vlan_id }}"template:
- "interface {{ .name }}"
- " description Interface on {{ .node_name }}"
- " ip address {{ .ip_addr }}/{{ .ip_plen }}"template:
- "connection {{ .name }}"
- " type vlan_trunk"
- " vlan {{ .vlan_id }}"-
Connection templates: Use
{{ .name }}for self-reference -
Interface templates: Use
{{ .conn_name }}for connection reference -
Consistent prefixing: All connection references use
conn_prefix, node references usenode_prefix
Network → Node → Connection → Interface → Group → Segment
Critical: Connection parameters must be assigned before Interface parameters to enable cross-object references.
-
Dependency Resolution:
reorderConfigTemplatesresolves template dependencies - Group Processing: Group templates accumulate configuration blocks
- Sorter Processing: Sorter templates merge and output final configurations
- Priority Control: Lower priority values (-3, -2, -1) process first
- Interface namespace includes connection parameters with
conn_prefix - Interface namespace includes node parameters with
node_prefix - References are established during parameter assignment phase
-
Virtual Nodes: Use
virtual: truefor topology modeling without deployment - Modular Extension: Use multiple classes for additional functionality
- Clear Naming: Use descriptive class names that reflect their purpose
-
Deterministic Templates: Avoid control syntax (
for,if) - use parameter symmetry instead -
Consistent Prefixing: Use
conn_andnode_prefixes for cross-object references - Priority Management: Use negative priorities for foundational configurations
- Automatic Assignment: Leverage parameter rules for consistent value assignment
- Manual Override: Use direct value assignment in DOT files when needed
- Cross-Object Consistency: Maintain consistent naming across related objects
nodeclass:
- name: router
values:
kind: linux
image: quay.io/frrouting/frr:8.5.4
params: [lo]
connectionclass:
- name: vlan_conn
prefix: "vlan_trunk"
params: [conn_id, vlan_id]
config:
- group: network_config.txt
priority: -2
template:
- "# Connection: {{ .name }} VLAN {{ .vlan_id }}"
interfaceclass:
- name: trunk_interface
config:
- group: interface_config.txt
priority: -1
template:
- "interface {{ .name }}"
- " description Trunk to {{ .opp_node_name }} via {{ .conn_name }}"
- " switchport mode trunk"
- " switchport trunk allowed vlan {{ .conn_vlan_id }}"
segmentclass:
- name: trunk_segment
layer: ip
config:
- group: network_config.txt
priority: -3
template:
- "# Trunk Segment: {{ .segment_name }} with {{ .segment_interface_count }} interfaces"digraph {
r1 [xlabel="router"];
r2 [xlabel="router"];
r1 -> r2 [
dir="none",
class="vlan_conn; segment#trunk_segment",
taillabel="trunk_interface",
headlabel="trunk_interface"
];
}This configuration demonstrates the complete workflow from class definition to cross-object references, showcasing dot2net's powerful template and parameter system.
Interfaces can reference adjacent interfaces through neighbors configuration for iterative template processing:
interfaceclass:
- name: ospf_interface
neighbors:
- layer: ip # Process neighbors on IP layer
config:
- name: neighbor_config
template:
- "# Neighbor: {{ .opp_node_name }} via {{ .conn_name }}"
- "neighbor {{ .opp_ip_addr }} area {{ .ospf_area }}"Key features:
-
Interface-specific: Only InterfaceClass supports
neighbors - Layer-based: Process neighbors on specific protocol layers
- Automatic iteration: Template executes for each adjacent interface
-
Neighbor parameters: Access
{{ .opp_* }}parameters for opposite interface
Multiple object types can reference other objects in the same class through classmembers:
nodeclass:
- name: router_cluster
classmembers:
- nodes: ["router"] # Reference other nodes with class "router"
config:
- name: cluster_peers
template:
- "# Cluster peer: {{ .name }} at {{ .ip_loopback }}"
- "peer {{ .ip_loopback }} cluster-member"interfaceclass:
- name: trunk_ports
classmembers:
- interfaces: ["trunk_interface"] # Reference interfaces in same class
include_self: false # Exclude current interface from iteration
config:
- name: trunk_aggregation
template:
- "# Trunk peer: {{ .node_name }}.{{ .name }}"
- "trunk-peer {{ .node_name }} interface {{ .name }}"connectionclass:
- name: vlan_connection
classmembers:
- connections: ["vlan_conn"] # Reference connections in same class
config:
- name: vlan_coordination
template:
- "# VLAN peer connection: {{ .name }} VLAN {{ .vlan_id }}"segmentclass:
- name: network_segment
classmembers:
- nodes: ["router"] # Reference nodes within the segment
config:
- name: segment_routing
template:
- "# Segment node: {{ .name }} in segment {{ .segment_name }}"
- "segment-id {{ .segment_id }} node {{ .name }}"Supported object types for classmembers:
-
NodeClass: Can reference
nodes,interfaces,connections -
InterfaceClass: Can reference
nodes,interfaces,connections -
ConnectionClass: Can reference
nodes,interfaces,connections -
SegmentClass: Can reference
nodes,interfaces,connections
Key attributes:
-
nodes/interfaces/connections- Specify which object classes to reference -
node/interface/connection- Single class name (alternative syntax) -
include_self- Whether to include the current object in iteration (default: true) -
config- Template blocks that execute for each referenced object