-
Notifications
You must be signed in to change notification settings - Fork 1
Template System
This page provides comprehensive documentation of dot2net's template system, including syntax, variable embedding, cross-object references, and advanced features for generating network configuration files.
dot2net uses Go's standard text/template engine to generate configuration files from network models. The template system enables dynamic configuration generation based on network topology, automatic parameter assignment, and cross-object relationships.
Key Features:
-
Go template syntax: Full support for
{{ }}expressions, conditionals, and loops - Cross-object references: Access parameters from related network objects
- Parameter inheritance: Automatic propagation of parameters between objects
- Template types: Support for named, group, and sorter templates
- Format flexibility: Generate any configuration format (FRR, Juniper, Cisco, etc.)
In dot2net, each network object (node, interface, connection, etc.) has a parameter namespace containing all variables accessible within templates. This namespace includes:
-
Self parameters: Object's own properties (
{{ .name }},{{ .ip_addr }}) -
Inherited parameters: Properties from parent/related objects (
{{ .node_name }},{{ .group_as }}) -
Cross-object references: Parameters from connected objects (
{{ .opp_ip_addr }},{{ .conn_vlan_id }}) -
Referential parameters: Parameters from neighbor/member objects (
{{ .n_node_as }},{{ .m_ipv4_net }})
When writing templates, use the dot2net params command to discover available variables and validate your template references. This command shows the actual parameter namespace for each object after dot2net processes your configuration.
Template development workflow:
- Draft initial templates based on your configuration goals
-
Check available variables using
dot2net params - Refine templates using discovered parameters
- Iterate configuration and templates as needed
-
Test final result with
dot2net build
See the Command Reference for detailed usage examples and filtering options.
Each network object (node, interface, connection, group) has its own set of parameters before considering relationships with other objects. Understanding these base parameters helps you predict what will be available in templates.
Each object's base parameters come from five sources:
These parameters are automatically assigned by dot2net based on object structure and naming rules:
Node examples:
{{ .name }} // Node name (e.g., "r1", "r2")
Interface examples:
{{ .name }} // Interface name (e.g., "net0", "eth1")
{{ .node_name }} // Parent node name (e.g., "r1")
{{ .opp_name }} // Opposite interface name (e.g., "net0")
{{ .opp_node_name }} // Opposite node name (e.g., "r2")
Connection examples:
{{ .name }} // Connection name (e.g., "r1--r2", "vlan_trunk0")
{{ .conn_id }} // Connection ID (e.g., "0", "1")
These parameters are generated based on layer policies and automatic IP assignment:
Interface IP parameters:
{{ .ip_addr }} // IP address (e.g., "10.0.0.1", "fc00::1")
{{ .ip_plen }} // Prefix length (e.g., "24", "64")
{{ .ip_net }} // Network address (e.g., "10.0.0.0/24")
{{ .opp_ip_addr }} // Opposite interface IP (e.g., "10.0.0.2")
Node IP parameters:
{{ .ip_loopback }} // Loopback IP (e.g., "10.255.0.1")
{{ .ipv4_loopback }} // IPv4 loopback (dual-stack scenarios)
{{ .ipv6_loopback }} // IPv6 loopback (dual-stack scenarios)
These parameters come from DOT file labels, including Value Labels and custom attributes:
Value Labels (name=value syntax):
r1 [xlabel="router"; stub_network="192.168.1.0/24"];
r2 -> r3 [label="trunk"; vlan="100"];Resulting parameters:
{{ .stub_network }} // "192.168.1.0/24" (from DOT Value Label)
{{ .vlan }} // "100" (from DOT edge label)
Place Labels (@name syntax):
r1 [xlabel="router"; @region="us-west"];
r2 [xlabel="router"; @region="us-east"];Cross-references:
{{ .region }} // "us-west" (for r1), "us-east" (for r2)
These parameters come from class definitions in YAML configuration files:
From NodeClass values:
nodeclass:
- name: router
values:
kind: linux
image: quay.io/frrouting/frr:8.5.0
mgmt_ip: dhcpResulting parameters:
{{ .kind }} // "linux"
{{ .image }} // "quay.io/frrouting/frr:8.5.0"
{{ .mgmt_ip }} // "dhcp"
From Group parameters:
groupclass:
- name: as65001
params: [as]
values:
as: 65001
region: us-westResulting parameters:
{{ .group_as }} // "65001" (inherited from group)
{{ .group_region }} // "us-west" (inherited from group)
These parameters are automatically generated based on parameter rules and assignment policies:
Parameter rules definition:
param_rule:
- name: vlan_id
min: 100
max: 199
assign: segment
layer: switchingResulting parameters:
{{ .vlan_id }} // "100", "101", "102"... (auto-assigned by segment)
{{ .conn_vlan_id }} // VLAN ID from connection (cross-reference)
AS number assignment:
param_rule:
- name: as
min: 65000
max: 65535Resulting parameters:
{{ .as }} // "65000", "65001"... (auto-assigned to groups)
{{ .group_as }} // AS number inherited from group
{{ .opp_group_as }} // Opposite node's AS number
Node Object Parameters:
{{ .name }} // Auto: Node name (e.g., "r1")
{{ .ip_loopback }} // IP: Loopback address (e.g., "10.255.0.1")
{{ .kind }} // YAML: Container type (e.g., "linux")
{{ .image }} // YAML: Container image (e.g., "quay.io/frrouting/frr:8.5.0")
{{ .group_as }} // YAML: AS number from group (e.g., "65001")
Interface Object Parameters:
{{ .name }} // Auto: Interface name (e.g., "net0")
{{ .ip_addr }} // IP: Interface IP (e.g., "10.0.0.1")
{{ .ip_plen }} // IP: Prefix length (e.g., "24")
{{ .vlan_tag }} // DOT: VLAN tag from Value Label
Connection Object Parameters:
{{ .name }} // Auto: Connection name (e.g., "vlan_trunk0")
{{ .conn_id }} // Auto: Connection ID (e.g., "0")
{{ .vlan_id }} // Policy: Auto-assigned VLAN (e.g., "100")
Group Object Parameters:
{{ .name }} // Auto: Group name (e.g., "as65001")
{{ .as }} // Policy: Auto-assigned AS number (e.g., "65001")
{{ .region }} // YAML: User-defined region (e.g., "us-west")
The complete namespace for each object is formed by combining its individual parameters with parameters from related objects through cross-object references. This creates a rich parameter space that enables complex template logic.
When dot2net processes the network model, it adds relationship-based parameters to each object's namespace using namespace prefixes:
Direct Relationships (always available):
-
Interface → Node:
node_prefix accesses parent node parameters -
Interface → Connection:
conn_prefix accesses connection parameters -
Interface → Opposite Interface:
opp_prefix accesses peer interface parameters
Iterative Relationships (generated dynamically):
-
Neighbor Objects:
n_prefix for adjacent interface parameters (requiresneighborsdefinition) -
Member Objects:
m_prefix for same-class object parameters (requiresclassmembersdefinition)
Common prefix examples:
{{ .node_name }} // Parent node name
{{ .node_image }} // Parent node container image
{{ .conn_name }} // Connection name
{{ .conn_vlan_id }} // Connection VLAN ID
{{ .opp_ip_addr }} // Opposite interface IP
{{ .opp_node_name }} // Opposite node name
{{ .n_ip_addr }} // Neighbor interface IP (in neighbor templates)
{{ .m_ipv4_net }} // Member network (in member templates)
For interface r1.net0 in a BGP scenario, the complete namespace combines:
Stage 1 (Individual parameters):
name: net0 // Auto: Interface name
ip_addr: 10.0.0.1 // IP: Interface address
ip_plen: 24 // IP: Prefix length
Stage 2 (Cross-object additions):
# From parent node (node_ prefix)
node_name: r1 // Parent node name
node_image: quay.io/frrouting/frr:8.5.0 // Parent node image
node_group_as: 65001 // Parent node's group AS
# From connection (conn_ prefix)
conn_name: r1--r2 // Connection name
conn_vlan_id: 100 // Connection VLAN (if applicable)
# From opposite interface (opp_ prefix)
opp_name: net0 // Opposite interface name
opp_ip_addr: 10.0.0.2 // Opposite interface IP
opp_node_name: r2 // Opposite node name
opp_node_group_as: 65000 // Opposite node's AS
Final result: Rich namespace enabling complex BGP neighbor configuration:
template:
- "interface {{ .name }}"
- "ip address {{ .ip_addr }}/{{ .ip_plen }}"
- "router bgp {{ .node_group_as }}"
- "neighbor {{ .opp_ip_addr }} remote-as {{ .opp_node_group_as }}"From example/basic_bgp/, interface r1.net0 demonstrates the complete 2-stage namespace formation:
Stage 1 - Individual object parameters:
interface:r1.net0
# Automatic parameters
name: net0
# IP-related parameters
ip_addr: 10.0.0.1
ip_plen: 24
ip_net: 10.0.0.0/24
Stage 2 - Cross-object relationship additions:
interface:r1.net0
# From parent node (node_ prefix)
node_name: r1
node_kind: linux
node_image: quay.io/frrouting/frr:8.5.0
node_group_as: 65001
# From opposite interface (opp_ prefix)
opp_name: net0
opp_ip_addr: 10.0.0.2
opp_node_name: r2
opp_node_group_as: 65000
# From connection (conn_ prefix - if applicable)
conn_name: r1--r2
This rich namespace enables the BGP interface template to access all necessary information for complete neighbor configuration.
Templates use Go's text/template syntax with double curly braces:
config:
- name: startup
template:
- "hostname {{ .name }}"
- "ip address {{ .ip_addr }}/{{ .ip_plen }}"
- "{{ if .loopback }}loopback {{ .ip_loopback }}{{ end }}"Direct property access:
{{ .name }} // Object name
{{ .ip_addr }} // IP address
{{ .ip_plen }} // IP prefix length
{{ .image }} // Container image (nodes)Conditional rendering:
{{ if .loopback }}
loopback {{ .ip_loopback }}
{{ end }}Loop iteration:
{{ range .interfaces }}
interface {{ .name }}
ip address {{ .ip_addr }}/{{ .ip_plen }}
{{ end }}Neighbor and Member objects are special referential objects that generate multiple configuration blocks dynamically based on network topology and class relationships.
Key characteristics:
- Multiple instances: One config block generated per related object
- Namespace inheritance: Inherit parent object's full namespace
-
Extended parameters: Add object-specific parameters with
n_orm_prefixes - Conditional generation: Only created when relevant objects exist
Neighbor objects iterate over adjacent interfaces within a specific network layer:
interfaceclass:
- name: ospf_interface
neighbors:
- layer: ip # Network layer for adjacency
config:
- name: static_routes
node: router # Target node class for config block
template:
- "ipv6 route {{ .n_node_stubnet }} {{ .n_ip_addr }}"-
For each interface with class
ospf_interface -
Find adjacent interfaces in the
iplayer - Generate one config block per adjacent interface
-
Add to target node (specified by
node: router)
interfaceclass:
- name: to_stub
neighbors:
- layer: ip
config:
- group: staticd.conf
node: router
template:
- "ipv6 route {{ .n_node_stubnet }} {{ .n_ip_addr }}"
- "!"Result: For each to_stub interface, generates static routes pointing to neighbor stub networks.
Member objects iterate over objects of the same class as specified in classmembers:
interfaceclass:
- name: bgp_peer
classmembers:
- interface: advertised_networks # Target class name
config:
- name: network_advertisement
template:
- " network {{ .m_ipv4_net }}"-
For each interface with class
bgp_peer -
Find all interfaces with class
advertised_networks - Generate one config block per found interface
- Merge into parent object's configuration
interfaceclass:
- name: ibgp
config:
- name: ibgp_afconf
node: bgp
template:
- "{{ .neighbors_ipv4_ibgp_afconf_nb }}"
- "{{ .members_interface_adv_ibgp_afconf_adv }}"
classmembers:
- interface: adv
config:
- name: ibgp_afconf_adv
template:
- " network {{ .m_ipv4_net }}" # advertised networkResult: For each ibgp interface, includes network advertisements from all adv class interfaces.
Neighbor Object Namespace = Parent Interface Namespace + Neighbor-specific Parameters
Example: Interface r1.net0 with neighbor r2.net0
-
Inherited:
{{ .name }}=r1.net0,{{ .ip_addr }}=10.0.0.1 -
Neighbor-specific:
{{ .n_name }}=r2.net0,{{ .n_ip_addr }}=10.0.0.2
Member Object Namespace = Parent Object Namespace + Member-specific Parameters
Example: Interface r1.net0 with member r3.adv0
-
Inherited:
{{ .name }}=r1.net0,{{ .node_as }}=65001 -
Member-specific:
{{ .m_name }}=r3.adv0,{{ .m_ipv4_net }}=192.168.3.0/24
interfaceclass:
- name: ibgp_peer
config:
- name: bgp_base
node: bgp
template:
- "router bgp {{ .node_as }}"
- "{{ .neighbors_ipv4_ibgp_neighbor_config }}"
- "{{ .members_interface_adv_network_config }}"
# Generate neighbor configurations
neighbors:
- layer: ipv4
config:
- name: neighbor_config
node: bgp
template:
- " neighbor {{ .n_node_ipv4_loopback }} remote-as {{ .n_node_as }}"
- " neighbor {{ .n_node_ipv4_loopback }} update-source lo"
- " neighbor {{ .n_node_ipv4_loopback }} description {{ .n_node_name }}"
# Include advertised networks from other interfaces
classmembers:
- interface: adv
config:
- name: network_config
template:
- " network {{ .m_ipv4_net }}"Configuration flow:
- Base template sets up BGP router and references neighbor/member configs
- Neighbor templates generate one neighbor statement per adjacent router
- Member templates generate one network statement per advertising interface
- Final config combines all generated blocks into complete BGP configuration
interfaceclass:
- name: dual_stack
neighbors:
- layer: ipv4
config:
- name: ipv4_neighbor
template:
- "neighbor {{ .n_ipv4_addr }} description IPv4-{{ .n_node_name }}"
- layer: ipv6
config:
- name: ipv6_neighbor
template:
- "neighbor {{ .n_ipv6_addr }} description IPv6-{{ .n_node_name }}"Result: Generates separate neighbor configurations for both IPv4 and IPv6 layers.
# Good
neighbors:
- layer: ip
config:
- name: ospf_neighbor_hello
- name: static_route_to_stub
# Avoid
neighbors:
- layer: ip
config:
- name: config1
- name: template# Use different layers for different protocols
neighbors:
- layer: ipv4
config:
- name: bgp_ipv4_neighbor
- layer: ipv6
config:
- name: bgp_ipv6_neighborclassmembers:
- interface: advertised_routes
config:
- name: route_advertisement
template:
- "{{ if .m_advertise }}"
- " network {{ .m_ipv4_net }}"
- "{{ end }}"interfaceclass:
- name: ospf_interface
neighbors:
- layer: ip
config:
- group: ospf_neighbors # Collect all neighbor configs
template:
- "neighbor {{ .n_ip_addr }} area {{ .n_ospf_area }}"
# Later processed by sorter template
nodeclass:
- name: router
config:
- file: ospf.conf
style: sort
sort_group: ospf_neighborsinterfaceclass:
- name: bgp_interface
config:
- name: frr_cmds
template:
- "interface {{ .name }}"
- "ip address {{ .ip_addr }}/{{ .ip_plen }}"
- "router bgp {{ .node_group_as }}"
- "neighbor {{ .opp_ip_addr }} remote-as {{ .opp_node_group_as }}"Variable breakdown:
-
{{ .node_group_as }}- Parent node's AS number from group -
{{ .opp_ip_addr }}- Opposite interface IP address -
{{ .opp_node_group_as }}- Opposite node's AS number
connectionclass:
- name: vlan_trunk
prefix: "vlan_trunk"
params: [vlan_id]
interfaceclass:
- name: trunk_port
config:
- name: switch_config
template:
- "interface {{ .name }}"
- "switchport mode trunk"
- "switchport trunk allowed vlan {{ .conn_vlan_id }}"
- "description {{ .conn_name }} (VLAN {{ .conn_vlan_id }})"Parameters flow through the network model hierarchy:
Group → Node → Interface
↘ ↗
Connection
-
{{ .name }}- Object's own name -
{{ .ip_addr }}- Object's IP address -
{{ .<custom_param> }}- Object's custom parameters
-
{{ .node_<param> }}- Parent node parameter (from interface) -
{{ .conn_<param> }}- Connection parameter (from interface) -
{{ .opp_<param> }}- Opposite interface parameter
-
{{ .opp_node_<param> }}- Opposite node parameter -
{{ .group_<param> }}- Group parameter (inherited)
- Object's direct parameters
- Inherited parameters (group → node → interface)
- Cross-object references (node_, conn_, opp_)
- Default values (if specified in parameter rules)
dot2net provides two fundamental approaches for assembling configuration templates, each with distinct advantages for different scenarios.
Concept: Templates are embedded directly within other templates using explicit dependency relationships and precise positioning control.
Characteristics:
- Explicit structure: Parent-child relationships are clearly defined
- Precise control: Exact embedding positions are specified
- Strict dependencies: Template dependencies are explicitly managed
- Deterministic output: Configuration order is predictable
When to use:
- Complex dependency relationships between templates
- When exact positioning of configuration blocks is critical
- For structured, hierarchical configuration formats
- When template relationships need to be clearly visible
Hierarchical assembly is particularly effective for generating structured configuration files that require multiple coordinated sections:
# NetworkClass orchestrates the entire file structure
networkclass:
- name: infrastructure_config
config:
# Individual section templates
- name: file_header
template:
- "# Configuration Header"
- "version: {{ .version }}"
# Organize network-level sections
- name: networks_section
template:
- "networks:"
- "{{ .segments_network_config }}"
# Organize device-level sections
- name: devices_section
template:
- "devices:"
- "{{ .nodes_device_config }}"
# Final file assembly with precise ordering
- file: config.yaml
template:
- "{{ .self_file_header }}"
- "{{ .self_networks_section }}"
- "{{ .self_devices_section }}"Key concepts of this pattern:
- NetworkClass orchestration: Single control point for file structure
- Section-based organization: Each major section has dedicated templates
- Cross-object integration: Different object types contribute to different sections
-
Template embedding:
{{ .self_section_name }}provides precise positioning -
Child object collection:
{{ .segments_template_name }},{{ .nodes_template_name }}gather contributions
nodeclass:
- name: router
config:
- name: frr_cmds
template:
- "hostname {{ .name }}"
- "ip forwarding"
- name: startup
depends: ["frr_cmds"]
template:
- "/usr/lib/frr/frr start"
- "{{ .self_frr_cmds }}" # Same object template embedding
- "{{ .interfaces_frr_cmds }}" # Child object template mergingHierarchical templates use specific syntax patterns for embedding other templates:
Self-Reference Embedding:
- "{{ .self_template_name }}" # Embed another template from same object- Embeds templates defined in the same class using
name:attribute - Maintains exact positioning control
- Enables modular template composition
-
Important: Template must be defined with
name:(notgroup:) to be referenceable
Child Object Embedding:
- "{{ .interfaces_template_name }}" # Embed template from all interfaces
- "{{ .segments_network_entry }}" # Embed template from all segments
- "{{ .nodes_node_entry }}" # Embed template from all nodes- Collects templates from child objects
- Automatically merges all matching templates
- Uses object-specific FileFormat for merging
Object Type Prefixes:
-
interfaces_- Collects from all interfaces of parent object -
segments_- Collects from all network segments -
nodes_- Collects from all nodes -
connections_- Collects from all connections -
groups_- Collects from all groups
Template Resolution Process:
- Self-templates: Resolved first within same object
- Child templates: Collected from related objects
- Format application: FileFormat applied during merge
- Final embedding: Result embedded at specified position
For Hierarchical assembly, templates must be defined with appropriate attributes:
Named Templates (Hierarchical):
config:
- name: "template_name" # Required for template embedding
template:
- "configuration content"
- name: "main_config"
template:
- "{{ .self_template_name }}" # Can reference above templateGroup Templates (Sort):
config:
- group: "group_name" # Used for sort-based collection
template:
- "configuration content"Key distinction:
- Use
name:when templates need to be referenced in Hierarchical assembly - Use
group:only for Sort assembly where templates are collected and merged
Concept: Configuration blocks are collected in groups and then merged with automatic ordering based on priority values.
Characteristics:
- Flexible collection: Templates contribute to shared groups
- Automatic ordering: Priority-based sorting handles sequence
- Distributed generation: Multiple objects can contribute to same configuration
- Dynamic aggregation: Final structure emerges from individual contributions
When to use:
- Repetitive configuration patterns across multiple objects
- When configuration blocks should be aggregated and sorted
- For scenarios where exact positioning is less critical than logical grouping
- When multiple objects need to contribute to shared configuration sections
Example pattern:
# Multiple interfaces contribute to group
interfaceclass:
- name: ospf_interface
config:
- group: "ospf_interfaces"
priority: 10
template:
- "interface {{ .name }}"
- "ip ospf area 0"
# Node processes collected group
nodeclass:
- name: router
config:
- file: "ospf.conf"
style: sort
sort_group: "ospf_interfaces"
template:
- "router ospf"
- "{{ range .ospf_interfaces }}"
- "{{ . }}"
- "{{ end }}"| Aspect | Hierarchical | Sort |
|---|---|---|
| Control | Precise positioning | Priority-based ordering |
| Complexity | Explicit dependencies | Automatic aggregation |
| Scalability | Manual relationship management | Dynamic collection |
| Use Cases | Structured configs, complex dependencies | Repetitive patterns, aggregation |
| Debugging | Clear template relationships | Group-based troubleshooting |
Complex scenarios often benefit from combining both approaches:
nodeclass:
- name: advanced_router
config:
# Hierarchical for main structure
- name: main_config
depends: ["base_config"]
template:
- "{{ .self_base_config }}"
- "# OSPF Configuration"
- "{{ .self_ospf_sorted_config }}"
- "# BGP Configuration"
- "{{ .self_bgp_sorted_config }}"
# Sort for aggregating interface contributions
- name: ospf_sorted_config
style: sort
sort_group: "ospf_interfaces"
- name: bgp_sorted_config
style: sort
sort_group: "bgp_interfaces"Template merging behavior:
-
{{ .self_template_name }}: Embeds same-object template at exact position -
{{ .interfaces_template_name }}: Merges all interface template results using the specified FileFormat
File output mechanism:
Both Hierarchical and Sort approaches are template assembly methods that prepare configuration content. Actual file generation requires a file template with a file: attribute at the Network or Node level.
-
Template Assembly Phase:
-
Hierarchical: Templates embed other templates via
{{ .self_template_name }} - Sort: Templates contribute config blocks to groups, then sorter templates collect them
-
Hierarchical: Templates embed other templates via
-
File Output Phase:
- NetworkClass/NodeClass file template reads assembled content and writes to target files
-
Required:
file:attribute specifying target file path -
Content source: References assembled templates via
{{ .self_template_name }}
Hierarchical file output:
nodeclass:
- name: router
config:
- name: main_config # Assembly phase
template: ["router bgp {{ .group_as }}"]
- file: "bgp.conf" # File output phase
template: ["{{ .self_main_config }}"]Sort file output:
nodeclass:
- name: router
config:
- name: collected_config # Assembly phase
style: sort
sort_group: "bgp_config"
- file: "bgp.conf" # File output phase
template: ["{{ .self_collected_config }}"]The two approaches can be combined bidirectionally, enabling flexible template organization:
Sort blocks embedded in Hierarchical templates:
nodeclass:
- name: router
config:
# Hierarchical main structure
- name: main_config
depends: ["base_config"]
template:
- "{{ .self_base_config }}"
- "# Interface configurations (collected via Sort)"
- "{{ .self_interface_aggregation }}"
- "# Static configuration"
- "no ip forwarding"
# Sort approach for collecting interface configs
- name: interface_aggregation
style: sort
sort_group: "interface_configs"
# Final file output (required for actual file generation)
- file: "router.conf"
template:
- "{{ .self_main_config }}"Hierarchical blocks used in Sort templates:
interfaceclass:
- name: complex_interface
config:
# Hierarchical for interface-specific structure
- name: base_interface
template:
- "interface {{ .name }}"
- "{{ .self_protocol_config }}"
- name: protocol_config
template:
- "ip address {{ .ip_addr }}/{{ .ip_plen }}"
- "ip ospf area 0"
# Contribute to node-level Sort group
- group: "all_interfaces"
template:
- "{{ .self_base_interface }}"Complete bidirectional scenario where both approaches complement each other:
# Interface uses Hierarchical for structure, contributes to Sort groups
interfaceclass:
- name: bgp_interface
config:
# Hierarchical assembly of interface-specific config
- name: interface_base
template:
- "interface {{ .name }}"
- "{{ .self_interface_address }}"
- "{{ .self_interface_routing }}"
- name: interface_address
template: ["ip address {{ .ip_addr }}/{{ .ip_plen }}"]
- name: interface_routing
template: ["ip ospf area {{ .group_ospf_area }}"]
# Contribute hierarchical result to node-level Sort group
- group: "interface_configs"
template: ["{{ .self_interface_base }}"]
# Node uses Sort to collect interfaces, embeds in Hierarchical structure
nodeclass:
- name: bgp_router
config:
# Sort collection of all interface configs
- name: all_interfaces
style: sort
sort_group: "interface_configs"
# Hierarchical main structure embedding Sort result
- name: main_config
template:
- "{{ .self_router_header }}"
- "# Interface configurations (collected via Sort)"
- "{{ .self_all_interfaces }}"
- "{{ .self_routing_protocols }}"
- name: router_header
template: ["hostname {{ .name }}"]
- name: routing_protocols
template:
- "router bgp {{ .group_as }}"
- "bgp router-id {{ .ip_loopback }}"
# Final file output combining both approaches
- file: "router.conf"
template: ["{{ .self_main_config }}"]This demonstrates how Sort collection (interface configs) can be embedded within Hierarchical structure (main config), and conversely how Hierarchical assembly (interface structure) can contribute to Sort groups for node-level aggregation.
For detailed configuration syntax and implementation examples, see YAML Configuration - Template Types. For template merging and FileFormat details, see YAML Configuration - File Formats.
nodeclass:
- name: router
config:
- name: routing_config
template:
- "{{ if .group_ospf_enabled }}"
- "router ospf"
- "router-id {{ .ip_loopback }}"
- "{{ end }}"
- "{{ if .group_bgp_enabled }}"
- "router bgp {{ .group_as }}"
- "bgp router-id {{ .ip_loopback }}"
- "{{ end }}"nodeclass:
- name: switch
config:
- name: vlan_config
template:
- "{{ range .interfaces }}"
- "{{ if .conn_vlan_id }}"
- "vlan {{ .conn_vlan_id }}"
- "name VLAN_{{ .conn_vlan_id }}"
- "{{ end }}"
- "{{ end }}"interfaceclass:
- name: bgp_peer
config:
- name: bgp_config
template:
- "router bgp {{ .node_group_as }}"
- "neighbor {{ .opp_ip_addr }} remote-as {{ .opp_node_group_as }}"
- "neighbor {{ .opp_ip_addr }} description {{ .opp_node_name }}_{{ .opp_name }}"
- "{{ if eq .node_group_as .opp_node_group_as }}"
- "neighbor {{ .opp_ip_addr }} next-hop-self"
- "{{ end }}"| Variable | Description | Example |
|---|---|---|
{{ .name }} |
Node name | r1 |
{{ .image }} |
Container image | quay.io/frrouting/frr:8.5.0 |
{{ .kind }} |
Node type | linux |
{{ .ip_loopback }} |
Loopback IP | 10.255.0.1 |
{{ .group_<param> }} |
Group parameter | {{ .group_as }} |
| Variable | Description | Example |
|---|---|---|
{{ .name }} |
Interface name | net0 |
{{ .ip_addr }} |
IP address | 10.0.0.1 |
{{ .ip_plen }} |
Prefix length | 24 |
{{ .node_name }} |
Parent node name | r1 |
{{ .conn_name }} |
Connection name | r1--r2 |
{{ .opp_ip_addr }} |
Opposite IP | 10.0.0.2 |
| Variable | Description | Example |
|---|---|---|
{{ .name }} |
Connection name | vlan_trunk0 |
{{ .vlan_id }} |
VLAN ID | 100 |
{{ .conn_id }} |
Connection ID | 0 |
| Variable | Description | Example |
|---|---|---|
{{ .n_name }} |
Neighbor interface name | net0 |
{{ .n_ip_addr }} |
Neighbor IP address | 10.0.0.2 |
{{ .n_node_name }} |
Neighbor node name | r2 |
{{ .n_node_as }} |
Neighbor node AS number | 65002 |
{{ .n_<param> }} |
Any neighbor parameter | {{ .n_ospf_area }} |
| Variable | Description | Example |
|---|---|---|
{{ .m_name }} |
Member object name | adv0 |
{{ .m_ip_addr }} |
Member IP address | 192.168.1.1 |
{{ .m_ipv4_net }} |
Member network | 192.168.1.0/24 |
{{ .m_<param> }} |
Any member parameter | {{ .m_advertise }} |
Use dot2net params to discover available variables and validate template references as you develop.
Why this matters:
- Prevents template errors: Avoid referencing non-existent variables
- Discovers available parameters: Find useful variables you might not know about
-
Validates cross-references: Confirm that
opp_,node_,conn_references work - Shows actual values: See computed IPs, names, and derived parameters
See Command Reference for detailed usage and filtering examples.
# Good
- name: bgp_neighbor_config
- name: ospf_interface_setup
- name: vlan_trunk_config
# Avoid
- name: config1
- name: template# Leverage connection information in interface templates
interfaceclass:
- name: trunk_port
config:
- name: vlan_config
template:
- "interface {{ .name }}"
- "description {{ .conn_name }} (VLAN {{ .conn_vlan_id }})"
- "switchport trunk allowed vlan {{ .conn_vlan_id }}"# Collect interface configs for later processing
interfaceclass:
- name: default
config:
- group: "interface_configs"
template:
- "interface {{ .name }}"
- "ip address {{ .ip_addr }}/{{ .ip_plen }}"
# Process all interfaces in node template
nodeclass:
- name: router
config:
- file: "interfaces.conf"
style: sort
sort_group: "interface_configs"# Use clear conditional structures
- name: routing_protocols
template:
- "{{ if .group_ospf_area }}"
- "router ospf"
- "router-id {{ .ip_loopback }}"
- "network {{ .ip_network }} area {{ .group_ospf_area }}"
- "{{ end }}"nodeclass:
- name: frr_router
config:
- name: frr_cmds
template:
- "ip forwarding"
- "{{ .self_router_protocol }}"
- name: startup
depends: ["frr_cmds"]
format: FRRVtyshCLI
template:
- "{{ .self_frr_cmds }}"
- "{{ .interfaces_frr_cmds }}"nodeclass:
- name: containerlab_node
config:
- name: clab_topo
template:
- "{{ .name }}:"
- " kind: {{ .kind }}"
- " image: {{ .image }}"
- " binds:"
- "{{ range .binds }}"
- " - {{ . }}"
- "{{ end }}"connectionclass:
- name: vlan_connection
prefix: "vlan"
params: [vlan_id]
interfaceclass:
- name: vlan_interface
config:
- name: vlan_setup
template:
- "interface {{ .name }}"
- "switchport access vlan {{ .conn_vlan_id }}"
- "description VLAN_{{ .conn_vlan_id }}_{{ .conn_name }}"Problem: template: executing template: map has no entry for key "xyz"
Solutions:
- Check parameter name spelling
- Verify parameter is defined in class params list
- Ensure cross-object reference uses correct prefix (
node_,conn_,opp_)
Problem: Connection or node parameters not accessible
Solutions:
- Verify objects are properly connected in DOT file
- Check that referenced object has the required parameters
- Ensure parameter rules are defined for custom parameters
Problem: Template syntax errors during execution
Solutions:
- Validate Go template syntax
- Check for unmatched
{{ if }}/{{ end }}pairs - Verify all referenced variables exist in scope
Problem: Configuration blocks appear in wrong order
Solutions:
- Use
priorityin group templates for ordering - Use
dependsin named templates for dependencies - Check sorter template
sort_groupmatches group template names
Problem: Neighbor or member references not working
Solutions:
- For neighbors: Verify interfaces are connected in the specified layer
- For members: Ensure target class objects actually exist in the network
-
Namespace: Check that
n_orm_prefix is used correctly - Layer mismatch: Verify neighbor layer matches connection layer
- Class existence: Confirm referenced classes are defined and assigned
Problem: No neighbor/member config blocks generated
Solutions:
- Check that adjacent interfaces exist for neighbor references
- Verify that member class objects are present in the network model
- Ensure layer specification matches actual network connections
- Confirm that referential objects have proper class assignments
From example/basic_bgp/input.yaml:
nodeclass:
- name: default
config:
- name: frr_cmds
template:
- "router bgp {{ .group_as }}"
- "bgp router-id {{ .ip_loopback }}"
interfaceclass:
- name: default
config:
- name: frr_cmds
template:
- "int {{ .name }}"
- "ip addr {{ .ip_addr }}/{{ .ip_plen }}"
- "router bgp {{ .group_as }}"
- "neighbor {{ .opp_ip_addr }} remote-as {{ .opp_group_as }}"Key features:
-
{{ .group_as }}- AS number from group class -
{{ .opp_ip_addr }}- Opposite interface IP -
{{ .opp_group_as }}- Opposite node's group AS number
From example/vlan_multihost/input.yaml:
connectionclass:
- name: vlan_trunk
prefix: "vlan_trunk"
params: [vlan_id]
interfaceclass:
- name: trunk
config:
- name: frr_cmds
template:
- "int {{ .name }}"
- "ip addr {{ .ip_addr }}/{{ .ip_plen }}"
- "# VLAN: {{ .conn_vlan_id }}, Connection: {{ .conn_name }}"Key features:
-
{{ .conn_vlan_id }}- VLAN ID from connection class -
{{ .conn_name }}- Connection name with prefix
This template system provides the foundation for dot2net's flexible and powerful configuration generation capabilities, enabling complex network configurations through simple, reusable templates.