-
Notifications
You must be signed in to change notification settings - Fork 1
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.
- Designing the topology — what belongs in the DOT file and what belongs in classes
- Letting dot2net assign the values — addresses, identifiers, and references between objects
- Keeping it readable as it grows — modelling protocols and organising templates
- Running a lab on more than one machine
- Working incrementally — the order to build in, and how to check as you go
- Generating a file that is itself a template
- Common design patterns
- When something is wrong
✅ 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
Network-wide settings → NodeClass values
↓
Node-specific config → InterfaceClass inheritance
↓
Interface-specific → ConnectionClass sharing
↓
Connection parameters → SegmentClass coordination
Design pattern:
- Start broad: Define network-wide policies and standards
- Specialize gradually: Add device-specific, interface-specific details
- 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.
Maximize automation while maintaining control:
-
Use Parameter Rules for patterns:
- VLAN IDs:
assign: segmentfor consistent segment-wide assignment - Connection IDs:
assign: connectionfor link identification - Interface numbers:
assign: objectfor unique per-interface values
- VLAN IDs:
-
Reserve manual overrides for exceptions:
- Special-purpose VLANs (management, backup)
- Backbone connections with specific requirements
- Legacy integration constraints
-
Layer parameter coordination:
- Different layers (IPv4, IPv6, MPLS) can have independent parameter spaces
- Use
layerspecification in parameter rules for layer-specific assignments
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
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)
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
groupandsort_group - Use
priorityto control ordering within groups - Let the reference do the ordering: a template that embeds
{{ .self_x }}is already saying it needsxfirst, anddependssays it a second time
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_linksdoes 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_classis what marks them, so a template can configure the ends that sit on either side -
Copy
topologies/ospf_multihostrather than starting from scratch
See Placing nodes on machines.
- Start with topology: Define basic network structure in DOT
- Add device types: Assign NodeClass labels to establish device roles
- Configure interfaces: Define InterfaceClass behavior per connection type
- Coordinate segments: Use SegmentClass for network-wide coordination
- Validate frequently: Check parameter assignment and IP allocation at each stage
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.dotRecommended development sequence for complex template configurations:
-
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
-
Verify Parameters: Use
dot2net paramsto understand available variables- Check parameter availability for each object type
- Verify cross-object references work as expected
- Identify missing parameters before template development
-
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
-
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
-
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
Systematic approach to resolving template issues:
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
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 not found:
- Verify template is defined with
name:attribute (notgroup: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
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
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.
-
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 - 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
-
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 }} -
If the file needs nothing from dot2net, do not template it.
raw: truehands a source file through as it is — the bundled examples use it for FRR'sdaemonsandvtysh.conf
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.
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.
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
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.