Skip to content

Template Assembly

sat edited this page Aug 15, 2026 · 5 revisions

Template Assembly

A device's configuration is written in pieces, and something has to decide where each piece lands. The address lines belong to the interfaces, the OSPF section belongs to the node, the network statements belong to whichever interfaces the topology marked — and frr.conf has to hold all of it, in an order FRR accepts.

Writing one of those pieces is Template System. This page is the next step: putting them together, and getting the result into a file.

Hierarchical and sort

There are two, and the choice is about who decides where a block lands:

Who decides the position Reach for it when
Hierarchical the parent template, by naming the block where it wants it the file has a shape — sections in a fixed order, blocks that must sit inside one of them
Sort the blocks themselves, by naming a group they belong to the file is a list, and what matters is that everything of one kind ends up together

They compose: a node template can name an aggregated group in one place and embed a specific block in another. Most real topologies do both.

Hierarchical assembly

A template names the blocks it wants, at the point it wants them, so the file's shape lives in one readable place and the order is fixed by construction rather than by hoping.

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_ip_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_<layer>_<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_ip_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 FormatStyle 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: FormatStyle 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

Aggregation: one block per interface, gathered by the node

The interface class writes the block for one interface, and says it only applies where the VLAN id exists. The node's template gathers them all.

interfaceclass:
  - name: access_port
    config:
      - name: vlan_entry
        required_params: [conn_vlan_id]
        template:
          - "vlan {{ .conn_vlan_id }}"
          - "name VLAN_{{ .conn_vlan_id }}"

nodeclass:
  - name: switch
    config:
      - name: vlan_config
        template:
          - "{{ .interfaces_vlan_entry }}"

Sort assembly

Each block names a group, and everything in that group is gathered and ordered by priority. Nothing has to know how many contributors there are, which is what makes it right for the repetitive parts: every interface adds its line to the same pile, and adding an interface adds a line.

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"

The blocks collected in ospf_interfaces are merged after this one by the sort style itself — there is nothing to walk and no loop to write. That is the whole point of the style: the template says what it contributes, and the assembly is dot2net's job.

Choosing between hierarchical and sort

Ask what fixes the order. If it is the file's format — this section before that one, this line inside that block — the parent knows it, so name the blocks from the parent: hierarchical. If the order among the blocks does not matter and what matters is that they all arrive, let them name a group: sort.

Hierarchical Sort
Order comes from the parent template's text the group and its priorities
A new contributor has to be named somewhere just appears
Reads well when the file has a shape the file has a list
When it goes wrong a block is in the wrong place, visibly a block is missing from a pile, quietly

Hybrid assembly

Most files need both, and they nest without ceremony: a parent names the groups it wants, and the contributors fill them.

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 FormatStyle

File output

Both approaches produce text. Neither of them writes anything. What puts text on disk is a config entry naming a file:, on the node or the network — so a block that is assembled perfectly and named by no file simply does not appear, which is the usual reason for output that is missing rather than wrong.

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 mix either way round: an aggregated group can be embedded at a chosen point, and a block that was placed by a parent can also contribute to a pile.

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

The punctuation between blocks — the ! FRR wants, the indentation containerlab wants — is not written here: it belongs to the file's FormatStyle.

See Also

Clone this wiki locally