# 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-Reference#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`](Command-Reference#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](Command-Reference#dot2net-params) 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):** ```dot 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):** ```dot 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:** ```yaml 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:** ```yaml 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:** ```yaml 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:** ```yaml 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: ```yaml 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: ```yaml config: - name: startup template: - "hostname {{ .name }}" - "ip address {{ .ip_addr }}/{{ .ip_plen }}" - "{{ if .loopback }}loopback {{ .ip_loopback }}{{ end }}" ``` ### Variable Access Patterns **Direct property access:** ```go {{ .name }} // Object name {{ .ip_addr }} // IP address {{ .ip_plen }} // IP prefix length {{ .image }} // Container image (nodes) ``` **Conditional rendering:** ```go {{ if .loopback }} loopback {{ .ip_loopback }} {{ end }} ``` **Loop iteration:** ```go {{ 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 ```yaml 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 ```yaml 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 ```yaml 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 ```yaml 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 ```yaml 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 ```yaml 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** ```yaml # 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** ```yaml # 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** ```yaml classmembers: - interface: advertised_routes config: - name: route_advertisement template: - "{{ if .m_advertise }}" - " network {{ .m_ipv4_net }}" - "{{ end }}" ``` #### 4. **Combine with Group Templates** ```yaml 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 ```yaml 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 ```yaml 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 - `{{ . }}` - Object's custom parameters #### 2. **Cross-Object Parameters** - `{{ .node_ }}` - Parent node parameter (from interface) - `{{ .conn_ }}` - Connection parameter (from interface) - `{{ .opp_ }}` - Opposite interface parameter #### 3. **Special Parameters** - `{{ .opp_node_ }}` - Opposite node parameter - `{{ .group_ }}` - 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: ```yaml # 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 ```yaml 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:** ```yaml - "{{ .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:** ```yaml - "{{ .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):** ```yaml 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):** ```yaml 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:** ```yaml # 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: ```yaml 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:** ```yaml 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:** ```yaml 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:** ```yaml 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:** ```yaml 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: ```yaml # 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](YAML-Configuration#template-types). For template merging and FileFormat details, see [YAML Configuration - File Formats](YAML-Configuration#file-formats). ## Advanced Template Features ### 1. Conditional Configuration ```yaml 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 ```yaml 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 ```yaml 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_ }}` | 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_ }}` | 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_ }}` | Any member parameter | `{{ .m_advertise }}` | ## Best Practices ### 1. **Use `dot2net params` During Template Development** Use [`dot2net params`](Command-Reference#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](Command-Reference#dot2net-params) for detailed usage and filtering examples. ### 2. **Use Descriptive Template Names** ```yaml # Good - name: bgp_neighbor_config - name: ospf_interface_setup - name: vlan_trunk_config # Avoid - name: config1 - name: template ``` ### 3. **Leverage Cross-Object References** ```yaml # 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** ```yaml # 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** ```yaml # 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** ```yaml 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** ```yaml nodeclass: - name: containerlab_node config: - name: clab_topo template: - "{{ .name }}:" - " kind: {{ .kind }}" - " image: {{ .image }}" - " binds:" - "{{ range .binds }}" - " - {{ . }}" - "{{ end }}" ``` ### 3. **VLAN Configuration Pattern** ```yaml 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`: ```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`: ```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.