Skip to content

Template System

sat edited this page Sep 17, 2025 · 2 revisions

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.

Overview

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

Namespace Concept Overview

What is a Namespace?

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

Namespace Inspection with dot2net params

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:

  1. Draft initial templates based on your configuration goals
  2. Check available variables using dot2net params
  3. Refine templates using discovered parameters
  4. Iterate configuration and templates as needed
  5. Test final result with dot2net build

See the Command Reference for detailed usage examples and filtering options.

Object Parameters and Namespace Formation

Stage 1: Individual Object Parameters

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.

Individual Object Parameter Sources

Each object's base parameters come from five sources:

1. Automatic Parameters (Generated by dot2net)

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

2. IP Address Related Parameters (Calculated by address policies)

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)

3. DOT File Parameters (User-specified in topology)

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)

4. YAML Configuration Parameters (User-defined class properties)

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

Resulting 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-west

Resulting parameters:

{{ .group_as }}      // "65001" (inherited from group)
{{ .group_region }}  // "us-west" (inherited from group)

5. Policy-Driven Parameters (Generated by parameter rules)

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

Resulting 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: 65535

Resulting parameters:

{{ .as }}            // "65000", "65001"... (auto-assigned to groups)
{{ .group_as }}      // AS number inherited from group
{{ .opp_group_as }}  // Opposite node's AS number

Object-Specific Parameter Examples

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

Stage 2: Cross-Object Namespace Formation

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.

Relationship-Based Parameter Addition

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 (requires neighbors definition)
  • Member Objects: m_ prefix for same-class object parameters (requires classmembers definition)

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)

Complete Namespace Example

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

Real Example: Complete Namespace Formation

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.

Template Syntax Fundamentals

Basic Template Structure

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

Variable Access Patterns

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 }}

Referential Objects: Neighbors and Members

Dynamic Object Generation

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_ or m_ prefixes
  • Conditional generation: Only created when relevant objects exist

Neighbor Objects

Neighbor objects iterate over adjacent interfaces within a specific network layer:

Definition Syntax

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

Behavior

  1. For each interface with class ospf_interface
  2. Find adjacent interfaces in the ip layer
  3. Generate one config block per adjacent interface
  4. Add to target node (specified by node: router)

Real Example from ospf6_topo1

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

Member objects iterate over objects of the same class as specified in classmembers:

Definition Syntax

interfaceclass:
  - name: bgp_peer
    classmembers:
      - interface: advertised_networks  # Target class name
        config:
          - name: network_advertisement
            template:
              - "  network {{ .m_ipv4_net }}"

Behavior

  1. For each interface with class bgp_peer
  2. Find all interfaces with class advertised_networks
  3. Generate one config block per found interface
  4. Merge into parent object's configuration

Real Example from bgp_features

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 network

Result: For each ibgp interface, includes network advertisements from all adv class interfaces.

Namespace Inheritance Pattern

Neighbor Namespace

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 Namespace

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

Advanced Examples

Complex BGP Configuration

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:

  1. Base template sets up BGP router and references neighbor/member configs
  2. Neighbor templates generate one neighbor statement per adjacent router
  3. Member templates generate one network statement per advertising interface
  4. Final config combines all generated blocks into complete BGP configuration

Multi-Layer Neighbor 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.

Best Practices for Referential Objects

1. Use Descriptive Names

# Good
neighbors:
  - layer: ip
    config:
      - name: ospf_neighbor_hello
      - name: static_route_to_stub

# Avoid
neighbors:
  - layer: ip
    config:
      - name: config1
      - name: template

2. Layer-Specific Configurations

# Use different layers for different protocols
neighbors:
  - layer: ipv4
    config:
      - name: bgp_ipv4_neighbor
  - layer: ipv6
    config:
      - name: bgp_ipv6_neighbor

3. Conditional Member References

classmembers:
  - interface: advertised_routes
    config:
      - name: route_advertisement
        template:
          - "{{ if .m_advertise }}"
          - "  network {{ .m_ipv4_net }}"
          - "{{ end }}"

4. Combine with Group Templates

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_neighbors

Cross-Object Reference Examples

BGP Neighbor Configuration

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

VLAN Trunk Configuration

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 }})"

Parameter Inheritance and Namespace

Parameter Flow

Parameters flow through the network model hierarchy:

Group → Node → Interface
     ↘     ↗
      Connection

Namespace Rules

1. Direct Parameters

  • {{ .name }} - Object's own name
  • {{ .ip_addr }} - Object's IP address
  • {{ .<custom_param> }} - Object's custom parameters

2. Cross-Object Parameters

  • {{ .node_<param> }} - Parent node parameter (from interface)
  • {{ .conn_<param> }} - Connection parameter (from interface)
  • {{ .opp_<param> }} - Opposite interface parameter

3. Special Parameters

  • {{ .opp_node_<param> }} - Opposite node parameter
  • {{ .group_<param> }} - Group parameter (inherited)

Parameter Resolution Order

  1. Object's direct parameters
  2. Inherited parameters (group → node → interface)
  3. Cross-object references (node_, conn_, opp_)
  4. Default values (if specified in parameter rules)

Template Assembly Approaches

dot2net provides two fundamental approaches for assembling configuration templates, each with distinct advantages for different scenarios.

Hierarchical Assembly

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

Structured File Generation Pattern

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

Basic Example Pattern

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 merging

Template Embedding Syntax

Hierarchical 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: (not group:) 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:

  1. Self-templates: Resolved first within same object
  2. Child templates: Collected from related objects
  3. Format application: FileFormat applied during merge
  4. Final embedding: Result embedded at specified position

Template Definition Requirements

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 template

Group 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

Sort Assembly

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

Choosing the Right Approach

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

Hybrid Approaches

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:

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.

File Output Process

  1. Template Assembly Phase:

    • Hierarchical: Templates embed other templates via {{ .self_template_name }}
    • Sort: Templates contribute config blocks to groups, then sorter templates collect them
  2. 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 }}

File Template Examples

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 }}"]

Cross-Approach Template Usage

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

Bidirectional Usage Examples

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.

Advanced Template Features

1. Conditional Configuration

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

2. Dynamic Interface Processing

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

3. Complex Parameter Composition

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

Template Variable Reference

Node Template Variables

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 }}

Interface Template Variables

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

Connection Template Variables

Variable Description Example
{{ .name }} Connection name vlan_trunk0
{{ .vlan_id }} VLAN ID 100
{{ .conn_id }} Connection ID 0

Neighbor Template Variables

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 }}

Member Template Variables

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 }}

Best Practices

1. Use dot2net params During Template Development

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.

2. Use Descriptive Template Names

# Good
- name: bgp_neighbor_config
- name: ospf_interface_setup
- name: vlan_trunk_config

# Avoid
- name: config1
- name: template

3. Leverage Cross-Object References

# 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 }}"

4. Use Group Templates for Repetitive Config

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

5. Handle Conditional Logic Gracefully

# 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 }}"

Common Patterns

1. FRR Configuration Pattern

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

2. Containerlab Integration Pattern

nodeclass:
  - name: containerlab_node
    config:
      - name: clab_topo
        template:
          - "{{ .name }}:"
          - "  kind: {{ .kind }}"
          - "  image: {{ .image }}"
          - "  binds:"
          - "{{ range .binds }}"
          - "  - {{ . }}"
          - "{{ end }}"

3. VLAN Configuration Pattern

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

Troubleshooting Templates

1. Variable Not Found Errors

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_)

2. Missing Cross-Object References

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

3. Template Execution Errors

Problem: Template syntax errors during execution

Solutions:

  • Validate Go template syntax
  • Check for unmatched {{ if }} / {{ end }} pairs
  • Verify all referenced variables exist in scope

4. Priority and Dependency Issues

Problem: Configuration blocks appear in wrong order

Solutions:

  • Use priority in group templates for ordering
  • Use depends in named templates for dependencies
  • Check sorter template sort_group matches group template names

5. Neighbor/Member Reference Issues

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_ or m_ 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

Examples from Real Scenarios

Basic BGP Configuration

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

VLAN Multi-Host Configuration

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.

Clone this wiki locally