# Template System **One template, written once, becomes every device's configuration.** You write what a router of a given kind needs — `router ospf`, an address on an interface, a BGP session to a neighbour — with the parts that differ per device left as `{{ }}`. dot2net fills them in from the topology, so adding a router means adding a line to the DOT file and nothing else. This page is about writing one of those templates: what it can reach, and how to reach the objects around it. Putting the blocks together into a file is [Template Assembly](Template-Assembly). ## Overview Templates are [Go `text/template`](https://pkg.go.dev/text/template), used for `{{ }}` substitution only. **`if` and `range` are not how you write conditions and loops here.** A parameter that is missing is an error rather than an empty value, so a conditional fails on exactly the objects it was written for, and there is nothing for `range` to walk. Both needs are answered another way, and both are worth knowing before you start: | You want | Write | |---|---| | The same block for every interface of a node, gathered into one place | [aggregation](Template-Assembly#hierarchical-assembly) — `{{ .interfaces_ }}` | | A block that appears only for the objects that have something to say | [`required_params`](YAML-Configuration#conditional-template-output-required_params) | | A value from a neighbouring object, not this one | a [cross-object prefix](#namespace-prefix-reference) — `{{ .node_* }}`, `{{ .conn_* }}`, `{{ .opp_* }}` | | One block per neighbour, or per member of a class | [neighbor and member objects](#referential-objects-neighbors-and-members) | ## Namespace Every object — node, interface, connection, group — carries a set of named values, and a template attached to that object can read any of them. That set is its **namespace**, and knowing what is in it is most of writing a template. It holds four kinds of thing: - **the object's own** — `{{ .name }}`, `{{ .ip_addr }}` - **the objects it belongs to** — `{{ .node_name }}`, `{{ .group_as }}` - **the objects it touches** — `{{ .opp_ip_addr }}` (the far end), `{{ .conn_vlan_id }}` (the connection) - **objects generated for it** — `{{ .n_node_as }}` (a neighbour), `{{ .m_ipv4_net }}` (a class member) ### Namespace inspection with `dot2net params` Rather than writing a name and finding out at build time whether it exists, ask. [`dot2net params`](Command-Reference#params---list-available-parameters) prints the namespace of every object as dot2net actually built it. Draft, run `dot2net params`, write against what it printed, then `dot2net build`. Filtering options are in the [Command Reference](Command-Reference#params---list-available-parameters). ## Object parameters and namespace formation A namespace is built in two stages: first each object collects what is its own, then it gains what its neighbours have. Reading them in that order is the fastest way to predict what a template can use — and to know **which file to edit** when a value you want is missing. ### Stage 1: an object's own parameters Five sources, and each is somewhere different: | Source | Where it comes from | Edit it in | |---|---|---| | Automatic | dot2net assigns it from the topology | nowhere — it follows the graph | | Address | an address policy hands it out | `layer:` / `policy:` | | DOT label | you wrote it on the node or edge | the DOT file | | Class value | a class's `values:` | the YAML | | Parameter rule | a `param_rule` computes it | the YAML | #### 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., "eth0", "eth1") {{ .node_name }} // Parent node name (e.g., "r1") {{ .opp_name }} // Opposite interface name (e.g., "eth0") {{ .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 topologies) {{ .ipv6_loopback }} // IPv6 loopback (dual-stack topologies) ``` #### 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.4 mgmt_ip: dhcp ``` **Resulting parameters:** ``` {{ .kind }} // "linux" {{ .image }} // "quay.io/frrouting/frr:8.5.4" {{ .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.4") {{ .group_as }} // YAML: AS number from group (e.g., "65001") ``` **Interface Object Parameters:** ``` {{ .name }} // Auto: Interface name (e.g., "eth0") {{ .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 parameters **Most configuration needs a value the object does not own.** An address on an interface needs the prefix length the segment agreed on; a BGP session needs the far end's AS number; a `network` statement needs the subnet the connection was given. Writing those by hand is what topology-driven configuration exists to avoid, so dot2net puts them in the namespace too, under a prefix that says where each came from. #### 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.eth0` in a BGP topology, the complete namespace combines: **Stage 1 (Individual parameters):** ``` name: eth0 // 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.4 // 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: eth0 // 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 `topologies/basic_bgp/`, interface `r1.eth0` demonstrates the complete 2-stage namespace formation: **Stage 1 - Individual object parameters:** ``` interface:r1.eth0 # Automatic parameters name: eth0 # 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.eth0 # From parent node (node_ prefix) node_name: r1 node_kind: linux node_image: quay.io/frrouting/frr:8.5.4 node_group_as: 65001 # From opposite interface (opp_ prefix) opp_name: eth0 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 ### Basic syntax There is very little to learn: text, with `{{ .name }}` where a value goes. ```yaml config: - name: startup template: - "hostname {{ .name }}" - "ip address {{ .ip_addr }}/{{ .ip_plen }}" ``` ### 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 }} {{/* does not work - see below */}} loopback {{ .ip_loopback }} {{ end }} ``` **`if` and `range` do not work here, and the reason is worth knowing.** Every parameter is a string, and a parameter that was never set is *missing*, not empty. A template that reads a missing key fails the build: map has no entry for key "loopback" So `{{ if .loopback }}` builds on a node that has a loopback and **fails on the node that does not** — the one case a conditional exists for. And `{{ range .interfaces }}` fails always: there is no list to walk, only strings. What to write instead: | Instead of | Write | |---|---| | a loop over an object's children | an [aggregation](Template-Assembly#hierarchical-assembly): `{{ .interfaces_ }}` gathers what every interface produced | | a conditional on a parameter | [`required_params`](YAML-Configuration#conditional-template-output-required_params) on the config entry — the whole block is skipped when the parameter is absent | | a conditional on a role | a class. Objects that carry it get the block; objects that do not, do not | This is not a limitation dot2net works around — it is what [deterministic templating](Basic-Concepts#core-architecture) means. A template that cannot branch produces the same text for the same object every time, and what varies is decided by the model rather than by logic embedded in the text. ## Passing `{{ }}` through to another tool A generated file is sometimes itself a template for another tool — TENTOU's `infra.yml` holds `{{ip.r1.eth0}}`, and Ansible and Helm use the same marks. The braces then have to survive dot2net untouched. What happens if you just write them: | What you write | What happens | |---|---| | `{{ip.r1.eth0}}` | **An error while the config is read** (`function "ip" not defined`). You find out | | `{{ .name }}`, meant literally | **Filled in without a word** — it becomes `r1`. No error, no warning | | the same in a `sourcefile:` | **Also filled in.** Reading from a file is not a way past this | The second row is the dangerous one: the file is generated, looks right, and carries a value dot2net chose where the downstream tool was meant to choose one. Two ways to write it out: ```yaml - '{{ printf "{{ip.%s.eth0}}" .name }}' # easier to read - '{{"{{"}}ip.{{ .name }}.eth0{{"}}"}}' # the plain form ``` Or take the marks away from dot2net for that file entirely: ```yaml config: - file: infra.yml delimiters: ["[[", "]]"] # dot2net reads [[ ]], and {{ }} passes through template: - "bindip: '{{ip.r1.eth0}}'" - "name: [[ .name ]]" ``` And for a file dot2net has nothing to fill in at all, `raw: true` hands the source file through as it is. See [ConfigTemplate Field Reference](#configtemplate-field-reference). > This is a sharp edge, not a feature: whether a literal `{{ }}` survives > depends on the author noticing. `delimiters` and `raw` exist because escaping > by hand is easy to get wrong. ## Referential objects: neighbors and members **Some configuration has one line per other device, and you do not know how many there are.** A BGP section needs a `neighbor` line for each peer; a static route needs one line per destination; a route reflector needs one per client. The count follows from the graph, and it changes when the graph does — which is exactly what you must not have to write out by hand. Two mechanisms cover it, differing in **what they count**: | You want one block per | Use | Reached with | |---|---|---| | Adjacent device on a layer — whoever ends up next to this one | **neighbor** | `{{ .n_* }}` | | Object carrying a given class — whoever is in this set, adjacent or not | **member** | `{{ .m_* }}` | Both produce a block per object found, each with the writing object's whole namespace plus the found object's values under the prefix. Both produce nothing when nothing is found, so a router with no peers writes no `neighbor` lines rather than an empty section. ### Neighbor objects Adjacency is per layer, so a topology carrying IPv4 and IPv6 on the same wires counts them separately. #### 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 Where a neighbor follows the wires, a member follows a **class**: it finds every object that carries the named one, whether or not it is next to the writer. That is what a BGP speaker advertising its own networks needs — the networks are whichever interfaces the topology marked as advertised, and they are not adjacent to anything. #### 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.eth0` with neighbor `r2.eth0` - **Inherited**: `{{ .name }}` = `r1.eth0`, `{{ .ip_addr }}` = `10.0.0.1` - **Neighbor-specific**: `{{ .n_name }}` = `r2.eth0`, `{{ .n_ip_addr }}` = `10.0.0.2` #### Member Namespace ``` Member Object Namespace = Parent Object Namespace + Member-specific Parameters ``` **Example**: Interface `r1.eth0` with member `r3.adv0` - **Inherited**: `{{ .name }}` = `r1.eth0`, `{{ .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. **Members that only sometimes contribute** A member that should produce nothing unless a parameter is set says so with `required_params`. The whole block is skipped when the parameter is absent — which a conditional could not do, since reading a missing parameter fails the build. ```yaml classmembers: - interface: advertised_routes config: - name: route_advertisement required_params: [m_advertise] template: - " network {{ .m_ipv4_net }}" ``` #### 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 }})" ``` ## Namespace prefix reference When a template needs a value it does not own, the prefix says where to look. The whole vocabulary: | Written | Reads | |---|---| | `{{ .name }}` | the object's own — its name, address, or anything a class gave it | | `{{ .node_* }}` | the node this interface belongs to | | `{{ .conn_* }}` | the connection this interface sits on | | `{{ .opp_* }}` | the interface at the far end | | `{{ .opp_node_* }}` | the node at the far end | | `{{ .group_* }}` | a group the object belongs to — an AS number, an OSPF area, or [something about the machine it sits on](#a-groups-value-read-from-a-node-on-it) | | `{{ .n_* }}` | a [neighbour](#neighbor-objects), inside its block | | `{{ .m_* }}` | a [class member](#member-objects), inside its block | Values reach an object before it is asked for them: a group's parameters are there for its nodes, a node's for its interfaces, and a connection's for both of its ends. Nothing has to be passed along by hand. ### A group's value, read from a node on it **Whatever is true of the machine is true for the nodes placed on it**, and `{{ .group_* }}` is how a node's template reads it. This is the usual way to get an environment-specific value — the address of a machine's own interface, a VLAN its switch was given — into the output of the nodes that sit there, without any node knowing which machine it landed on. ```yaml # The value belongs to the machine, so it is written on the machine: # subgraph host1 { label = "worker; inter_ip=192.168.101.2"; ... } nodeclass: - name: platform_sw deploy: platform config: - name: attach_entry template: ["{{ .group_inter_ip }} {{ .name }}"] file: - name: attach.txt scope: group # one per machine groupclass: - name: worker config: - file: attach.txt template: ["{{ .nodes_attach_entry }}"] ``` One file per machine, each naming that machine's own value: ``` host1/attach.txt : 192.168.101.2 br1 host2/attach.txt : 192.168.101.3 br2 ``` The value is written once, in the DOT file, and reaches both the configuration and the instructions for wiring the machine up — so there are not two places to keep in step. ## Assembling the blocks into a file Everything above is about **one block**: what it can read, and what it says. A device's configuration is many of them, and what decides where each one lands — and which file it reaches — is a question of its own: **[Template Assembly](Template-Assembly)**. ## Advanced template features ### `required_params`: output that appears only sometimes Two blocks, each written for a parameter that not every node has. `required_params` decides whether each one is produced; the template itself never asks. ```yaml nodeclass: - name: router config: - name: ospf_config required_params: [group_ospf_enabled] template: - "router ospf" - "router-id {{ .ip_loopback }}" - name: bgp_config required_params: [group_as] template: - "router bgp {{ .group_as }}" - "bgp router-id {{ .ip_loopback }}" ``` A class does the same thing when the condition is a role rather than a value: give the routers that speak BGP a `bgp_router` class and put the block there. ### Composing a value from several parameters ```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 }}" ``` The line that should appear only for an iBGP session belongs in a class of its own — one attached to the interfaces whose two ends share an AS. Comparing the two values in the template would put the decision in the text, where dot2net cannot see it. ## ConfigTemplate Field Reference Every entry under a class's `config:` is a ConfigTemplate. These are its fields. ### What it produces | Field | Meaning | |---|---| | `file` | Write the rendered text to this file definition. An entry with `file` produces a file | | `name` | Make the rendered text available to other templates as `{{ .self_ }}`, and to a parent as `{{ .nodes_ }}`, `{{ .interfaces_ }}` and so on. An entry with `name` produces a block, not a file | A name that dot2net owns — `startup`, `teardown`, `worker_deploy` and the other machine-side ones — is a **hook**: a module reads it and puts it where its own platform runs such things. See [Module System](Module-System#where-a-module-and-a-topology-meet). ### Where the text comes from | Field | Meaning | |---|---| | `template` | The lines themselves | | `sourcefile` | A file to read them from. Naming both `template` and `sourcefile` is rejected: write two entries and order them with `blocks:` | | `raw` | Hand the source file through as read, without expanding it. For a file that is material rather than a template — one dot2net has nothing to fill in, and that may carry `{{` of its own meant for whoever reads it later | | `delimiters` | Replace `{{` and `}}` for this template alone, e.g. `delimiters: ["[[", "]]"]`. For generating a file that is itself a template for another tool: the downstream syntax passes through untouched while dot2net's own values are still filled in | ```yaml config: - file: daemons sourcefile: ./daemons raw: true # FRR's daemons file, handed through as it is - file: playbook.yml delimiters: ["[[", "]]"] template: - " host: {{ ansible_host }}" # left for Ansible - " name: [[ .name ]]" # filled in by dot2net ``` ### When it applies | Field | Meaning | |---|---| | `node` / `nodes` | Only for interfaces or connections of nodes carrying these classes | | `neighbor_node` / `neighbor_nodes` | Only when the neighbour is of these classes | | `required_params` | Only when every named parameter exists and is not empty. This is how a section is left out without an `if` in the template | | `required_link` | Only where the connection is `deploy: link` — wiring the platform actually lays. Written on the templates that ask a platform to make a link, so that a connection built by the configuration instead (a tunnel, an overlay) produces no wiring for the platform to do. Works on interface-scoped and connection-scoped templates alike, since both describe the same wire | ### How it is combined | Field | Meaning | |---|---| | `depends` | Names of blocks on the same object that must be rendered first | | `blocks.before` / `blocks.after` | Place this block before or after named blocks in the file | | `priority` | Order among blocks of the same group. Smaller comes first | | `style` / `sort_group` / `group` | See [Template Assembly](Template-Assembly) | | `format` / `formats` | The FormatStyle applied when the block is registered in a namespace | | `assembly_format` / `assembly_formats` | The FormatStyle applied when blocks are assembled into a file | ## Template variable reference What follows is the full list, for looking things up. `dot2net params` on your own topology is the better answer while writing — these are what exists in principle, not what exists in yours. ### Node Template Variables | Variable | Description | Example | |----------|-------------|---------| | `{{ .name }}` | Node name | `r1` | | `{{ .image }}` | Container image | `quay.io/frrouting/frr:8.5.4` | | `{{ .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 | `eth0` | | `{{ .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 | `eth0` | | `{{ .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. Look the parameters up rather than remembering them Use [`dot2net params`](Command-Reference#params---list-available-parameters) 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#params---list-available-parameters) 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. **Let `required_params` decide, not the template** ```yaml - name: routing_protocols required_params: [group_ospf_area] template: - "router ospf" - "router-id {{ .ip_loopback }}" - "network {{ .ip_net }} area {{ .group_ospf_area }}" ``` The block is produced for nodes in a group that has an OSPF area, and not produced for the others. Written as `{{ if .group_ospf_area }}`, it would instead fail the build for the others. ## Common patterns Three configurations that come up constantly, written out end to end. Copying one of these is usually faster than assembling it from the pieces above. ### 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** **You do not write this one.** The containerlab module produces `topo.yaml`, including the bind list, from the files your topology declares — see [Module: Containerlab](Module-Containerlab). It is shown here because the shape is worth recognising: the list of binds is an aggregation the module assembles, not a loop in a template. ```yaml # what the module's own template looks like, in outline - name: clab_topo template: - "{{ .name }}:" - " kind: {{ .kind }}" - " image: {{ .image }}" - " binds:" - "{{ .values_clab_bind_entry }}" # one entry per file, gathered ``` ### 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 Every message below names the object and the template it came from, so the first move is always to read the whole line rather than the first half. ### 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 - Verify all referenced variables exist in scope — a name dot2net did not put there fails the build, which is what `{{ if }}` and `{{ range }}` run into (see [Variable Access Patterns](#variable-access-patterns) for why `{{ if }}` and `{{ range }}` run into exactly this) ### 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 topologies These are not written for this page: they are the shipped topologies, and `dot2net build` in their directory produces the output described. ### Basic BGP Configuration From `topologies/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 ### One value, read from both ends From `example/param_share/input.yaml`. A rule assigns a VLAN id per segment and a name per connection; an interface asks for both and reads them as its own: ```yaml param_rule: - name: vlan assign: segment # everything on one medium gets the same value layer: ip min: 100 max: 1001 - name: cname assign: connection # both ends of one link get the same value interfaceclass: - name: default params: [vlan, cname] config: - group: params.txt template: - "connection name {{ .cname }} for vlan {{ .vlan }} ({{ .node_name }}.{{ .name }})" ``` Both interfaces of a link produce the same `cname`, and every interface on a segment the same `vlan` — without either end being told what the other got: ``` connection name conn0 for vlan 100 (r1.eth0) connection name conn0 for vlan 100 (r2.eth0) ``` **Key features:** - `params:` — what this class asks to be given - `{{ .node_name }}` — the parent node, through the `node_` prefix This template system provides the foundation for dot2net's flexible and powerful configuration generation capabilities, enabling complex network configurations through simple, reusable templates.