Skip to content

Best Practices

sat edited this page Aug 25, 2026 · 15 revisions

Best Practices

This page provides recommended practices for creating efficient, maintainable, and scalable dot2net configurations. Focus on overall design principles that span both DOT and YAML configuration.

Contents

Topology-Driven Design Principles

Separation of Concerns

✅ Clear role separation:

  • DOT files: Define WHAT the network looks like (topology structure)
  • YAML files: Define HOW each component should be configured (behavior templates)
  • Parameter rules: Define automatic value assignment patterns

❌ Mixing concerns:

  • Avoid hardcoding configuration details in DOT files
  • Avoid topology assumptions in YAML templates
  • Don't duplicate parameter assignments across files

Hierarchical Network Modeling

Network-wide settings → NodeClass values
    ↓
Node-specific config → InterfaceClass inheritance
    ↓
Interface-specific → ConnectionClass sharing
    ↓
Connection parameters → SegmentClass coordination

Design pattern:

  1. Start broad: Define network-wide policies and standards
  2. Specialize gradually: Add device-specific, interface-specific details
  3. Coordinate related objects: Use segments for VLAN management, groups for areas

Where to put a value follows from how many objects have to agree on it. One device: the node class. One end of a link: the interface class. Both ends: the connection class, so they cannot drift apart. Everything on a shared medium: the segment class. A set of devices: the group class.

Automatic Parameter Management

Parameter Assignment Strategy

Maximize automation while maintaining control:

  1. Use Parameter Rules for patterns:

    • VLAN IDs: assign: segment for consistent segment-wide assignment
    • Connection IDs: assign: connection for link identification
    • Interface numbers: assign: object for unique per-interface values
  2. Reserve manual overrides for exceptions:

    • Special-purpose VLANs (management, backup)
    • Backbone connections with specific requirements
    • Legacy integration constraints
  3. Layer parameter coordination:

    • Different layers (IPv4, IPv6, MPLS) can have independent parameter spaces
    • Use layer specification in parameter rules for layer-specific assignments

Cross-Object Reference Design

Leverage parameter symmetry:

  • Interface templates can reference: {{ .node_name }}, {{ .conn_vlan_id }}
  • Connection templates can reference: {{ .name }}, {{ .conn_id }}
  • Avoid complex multi-hop references that reduce maintainability

Scalability and Maintainability

Protocol and Service Modeling

Use layers for protocol separation:

  • Separate IPv4, IPv6, MPLS into different layers
  • Apply different IP policies per layer
  • Enable multi-protocol configurations on same physical topology

Leverage nodes that are not deployed (deploy: none) for complex topologies:

  • Model logical centralized services (BGP route reflectors, DHCP servers)
  • Simplify IP address assignment for hub-and-spoke patterns
  • Exclude components from the deployment while keeping them in the model (deploy: none)

Configuration Template Organization

Choose appropriate template style:

  • Hierarchical: For strict dependency control and embedding relationships
  • Sort: For flexible configuration assembly with priority-based ordering
  • Named templates: For reusable configuration blocks

Manage template complexity:

  • Group related configurations using group and sort_group
  • Use priority to control ordering within groups
  • Let the reference do the ordering: a template that embeds {{ .self_x }} is already saying it needs x first, and depends says it a second time

Running a lab on more than one machine

A lab larger than one machine is still drawn once. Mark the machines as subgraphs of the worker group class and dot2net writes one deployment file per machine — containerlab's topo.yaml, TiNET's spec.yaml, Kathara's lab.conf — works out which links leave a machine, and replaces a shared segment that reaches across with a bridge on each side.

  • Draw the segment you mean, once. Splitting it by hand is what aggregate_crossing_links does for you, and it costs one link leaving a machine instead of one per member on the far side — each of which costs a VLAN from a finite pool
  • The links between machines are yours to make. No platform's file describes anything outside its own machine, so none of them can create them; boundary_crossing_connection_class is what marks them, so a template can configure the ends that sit on either side
  • Copy topologies/ospf_multihost rather than starting from scratch

See Placing nodes on machines.

Working incrementally

The order to build a topology in

  1. Start with topology: Define basic network structure in DOT
  2. Add device types: Assign NodeClass labels to establish device roles
  3. Configure interfaces: Define InterfaceClass behavior per connection type
  4. Coordinate segments: Use SegmentClass for network-wide coordination
  5. Validate frequently: Check parameter assignment and IP allocation at each stage

Checking as you go

Parameter verification workflow:

# 1. Check parameter assignments
dot2net params -c input.yaml input.dot

# 2. Verify IP address allocation
dot2net visual -c input.yaml input.dot | dot -Tpdf > network.pdf

# 3. Test configuration generation
dot2net build -c input.yaml input.dot

# 4. See what would be written, and how each file reaches its container
dot2net files -v -c input.yaml input.dot

Building a configuration up in steps

Recommended development sequence for complex template configurations:

  1. Start Minimal: Create the simplest possible template configuration first

    • Define basic NodeClass with minimal template
    • Test single-node topologies before adding complexity
    • Verify core functionality before expanding
  2. Verify Parameters: Use dot2net params to understand available variables

    • Check parameter availability for each object type
    • Verify cross-object references work as expected
    • Identify missing parameters before template development
  3. Add Incrementally: Introduce one class or feature at a time

    • Add one InterfaceClass, test, then proceed
    • Introduce ConnectionClass after interface templates work
    • Add SegmentClass only after connection handling is verified
  4. Debug Systematically: Resolve all errors before adding complexity

    • Fix template reference errors immediately
    • Resolve parameter assignment issues before proceeding
    • Verify each level works before adding the next
  5. Integrate Gradually: Combine classes only after individual validation

    • Test individual class templates independently
    • Verify cross-object references work correctly
    • Integrate Hierarchical assembly as final step

When a template does not do what you meant

Systematic approach to resolving template issues:

Error Message Analysis

Read error messages carefully to identify context:

  • Look for object identification (e.g., "for node:r1", "for interface:eth0")
  • Note the specific template name causing the error
  • Identify whether the issue is parameter-related or template reference-related

Parameter Issues

Missing parameters:

Error: map has no entry for key "segment_id"
Solution: Add parameter to class definition: params: [segment_id]

Wrong parameter names:

  • Check spelling of parameter names in templates
  • Verify cross-object prefixes are correct (node_, conn_, opp_)
  • Confirm parameter rules are defined for custom parameters

Template Reference Issues

Template not found:

  • Verify template is defined with name: attribute (not group: for Hierarchical)
  • Check template name consistency across references
  • Ensure {{ .self_template_name }} matches actual template name

Cross-object references failing:

  • Verify objects are properly connected in DOT file
  • Check that referenced object types actually exist
  • Confirm layer specifications match network topology

Development Tools

Use debugging commands:

  • dot2net params - Verify available parameters for each object
  • dot2net build -v - Enable verbose output for detailed processing information
  • Test with minimal DOT files before expanding topology

Generating a file that is itself a template

Sometimes what dot2net writes is read by another tool that has its own {{ }} — TENTOU's infra.yml, an Ansible playbook, a Helm chart. Two sets of braces then share one file.

  1. Check whether the downstream tool uses {{ }} before you start. If it does, the notations collide, and the collision is quiet: a literal {{ .name }} is filled in by dot2net without a word. See Passing {{ }} through
  2. Decide which side fills in each value, and do not let both. The values inside the downstream template belong to the downstream tool at run time. dot2net cannot know a machine's NIC name, for instance — so let the other tool find it, and do not generate a guess
  3. Keep the two kinds of value apart. A line that mixes something dot2net fills in with something passed through is hard to read; if they must share a line, put the passing-through part in one action: {{ printf "{{ip.%s.eth0}}" .name }}
  4. If the file needs nothing from dot2net, do not template it. raw: true hands a source file through as it is — the bundled examples use it for FRR's daemons and vtysh.conf

Common design patterns

Each of these has a topology in the repository that does it. Reading the one nearest to what you want is faster than assembling it from the pieces.

Spine and leaf — roles on the node class, and interfaces named per direction. See topologies/basic_clos.

Something to reach from everywhere (a route server, a controller): a place label names it, and every template reads its parameters by that name — no adjacency needed. See topologies/basic_bgp.

A shared medium several devices sit on: draw the switch as a node the platform provides rather than deploys, and configure what forms around it with a segment class. See example/switching.

One value per segment (a VLAN id): param_rule with assign: segment. See example/param_share, where the value assigned per segment is then read from both ends of every link on it.

More than one address family over the same links: a layer each, and templates that name the layer they mean. See topologies/bgp_evpn_vxlan_topo1.

A lab larger than one machine: topologies/ospf_multihost is the one to copy; topologies/aggregate_crossing shows what a segment reaching across machines costs and how the count is brought down.

Error Prevention and Troubleshooting

A block is missing from a generated file

Read the messages first. Most of the ways a block goes astray stop the build and name themselves: a group: no sorter gathers, a placed: label nothing carries, two blocks level with an anchor, a value overwritten by a name built from a prefix. None of these leave a file with a gap in it — the file is not written, and the message says which name is the problem. (Files finished before the failure are already on disk, so a directory holding some of what you expected is a build that stopped, not a build that skipped something.)

What stays quiet is the block that was never generated, because nothing asked for it:

Why What to look at
the class carrying it is on no object the labels in the DOT file, and class_policy for what is applied by default
the object is virtual:, or hangs under a node nobody deploys deploy: on the node and the connection. Nothing is written for an object the platform does not put in place
the entry's own conditions did not match node: / nodes: / neighbor_node: on the config entry — a template scoped to a class the object does not have is skipped
required_params: was not satisfied the parameters the entry names. The whole entry is left out, not emptied

dot2net build -v says which of these it took, for every object and every entry.

A block written into a hook reaches the files of whichever platform is being generated for. If no module is writing files — generate_scripts off, for a worker_ hook — dot2net says so rather than dropping it.

Configuration Consistency

Avoid conflicting class assignments:

  • Don't assign contradictory InterfaceClass types to same interface
  • Ensure ParameterRule ranges don't overlap unintentionally
  • Validate ConnectionClass and SegmentClass parameter compatibility

Parameter scope management:

  • Use appropriate parameter assignment scope (object, connection, segment)
  • Coordinate parameter rules across different object types
  • Leverage cross-object references for parameter sharing

Performance Considerations

Optimize for large networks:

  • Minimize complex cross-object template references
  • Use appropriate parameter assignment scope to reduce computation
  • Choose efficient template organization (Hierarchical vs Sort) based on dependency complexity

Template efficiency:

  • Prefer direct parameter access over multi-hop references
  • Group related configurations for batch processing
  • Use named templates for reusable configuration blocks

Following these best practices ensures that your dot2net configurations are maintainable, scalable, and leverage the full power of the topology-driven configuration approach.

Clone this wiki locally