-
Notifications
You must be signed in to change notification settings - Fork 1
NetworkModel Design
This page documents how the concepts from Basic Concepts are implemented in dot2net's NetworkModel structure.
NetworkModel is the core data structure that realizes the topology-driven configuration approach. Its primary responsibilities are:
- Parameter namespace management - Building and managing parameter namespaces for each object
- Parameter generation - Computing values needed for configuration templates (including IP address assignment)
- Dependency management - Tracking object relationships for correct processing order during config generation
The most important aspect is the parameter namespace. Everything else—object hierarchy, dependency tracking, IP assignment—serves the goal of building correct namespaces for template execution.
Basic Concepts introduces Architecture Classes (AC) as the role-independent ownership structure. In NetworkModel, ACs are implemented as a top-down object hierarchy:
NetworkModel
├── Nodes[]
│ └── Interfaces[]
├── Connections[]
├── Groups[]
└── NetworkSegments[]
This hierarchy defines the structural parent-child relationships:
- A Node owns its Interfaces
- NetworkModel owns all top-level objects
The Childs() method on each object returns its structural children in this hierarchy.
Configuration Classes (CC) define role and behavior. In NetworkModel, CCs are implemented as class labels attached to objects:
Object = AC instance + CC labels
For example, a Node object might have labels ["router", "ospf", "bgp"], meaning it's a router running both OSPF and BGP.
Labels are parsed from DOT attributes and matched against class definitions in YAML configuration.
Basic Concepts describes the Parameter Namespace Design with three types of parameters:
- Self parameters: Object's own properties
- Relative parameters: Related objects' properties with prefixes
-
Cross-object references:
{{ .node_name }},{{ .conn_vlan_id }}, etc.
In NetworkModel, this is implemented through two fields in each object:
type NameSpace struct {
params map[string]string // Self parameters
relativeParams map[string]string // Complete namespace for templates
}While the object hierarchy is top-down, relative namespaces are built bottom-up. Each object's relativeParams contains:
- Its own
params(without prefix) - Parent object's params (with
node_prefix for interfaces) - Related objects' params (with
conn_,neighbor_, etc. prefixes) - Global place-labeled params (accessed by
@namesyntax)
This bottom-up construction happens in BuildRelativeNameSpace(), which each object implements.
Why bottom-up? Templates execute at the leaf level (e.g., per-interface), but need access to parent and related object parameters. The relative namespace aggregates all relevant parameters into a single flat map that templates can access directly.
This is a critical design distinction:
ChildClasses() and Childs() define the data structure—which objects contain which other objects:
| Object | Children |
|---|---|
| NetworkModel | Node, Connection, Group, NetworkSegment |
| Node | Interface |
| Interface | Neighbor, Member |
| Group | (none) |
DependClasses() and Depends() define the processing order—which objects must be processed before others during parameter generation:
| Object | Depends On |
|---|---|
| NetworkModel | Node |
| Node | (none) |
| Interface | (none) |
| Group | Node (its members) |
| NetworkSegment | Interface (its members) |
| Neighbor | (none) |
| Member | (none) |
Key insight: These are different relationships. A Group has no structural children (Childs returns empty), but depends on its member Nodes for parameter calculation (Depends returns the member Nodes).
Consider a Group that represents "all spine routers". The Group needs to:
- Access parameters from its member Nodes (e.g., list all their loopback IPs)
- Generate its own parameters based on members
- Provide these parameters to templates
The Group doesn't own the Nodes (they belong to NetworkModel), but it depends on them. This dependency must be tracked separately from ownership to ensure correct processing order.
Basic Concepts distinguishes Substantial Objects (directly from topology) and Referential Objects (auto-generated):
| Substantial | Referential |
|---|---|
| Node, Interface, Connection, Group, NetworkSegment | Neighbor, Member |
Referential objects exist to avoid control syntax in templates. Instead of:
# Hypothetical: loop over neighbors
{% for neighbor in interface.neighbors %}
neighbor {{ neighbor.ip }}
{% endfor %}dot2net generates a Neighbor object for each adjacency, with its own namespace:
# Actual: template applied per Neighbor object
neighbor {{ .ip_addr }}Referential objects are reached through Depends, not Childs. An Interface's Neighbors are processing dependencies (we need their parameters), not structural children (Interface doesn't "own" the neighbor relationship).
Objects are created from different sources during model building:
These objects are constructed directly from the DOT topology graph:
| Object | Network Role | DOT Source |
|---|---|---|
| Node | Network device (router, switch, host) | Graph vertex |
| Interface | Device port | Edge endpoint |
| Connection | Link between interfaces | Graph edge |
| Group | Logical grouping of nodes | Subgraph |
| Object | Network Role | Created When |
|---|---|---|
| NetworkSegment | Broadcast domain per layer | During IP address assignment, grouping interfaces that share the same network segment |
These referential objects are generated based on class definitions in YAML:
| Object | Purpose | Parent | Created When |
|---|---|---|---|
| Neighbor | Adjacency relationship | Interface | For each interface pair per layer |
| Member | Cross-object reference | Any object except NetworkModel | When a class defines member_class
|
Note: Member is special—it can be created under Node, Interface, Connection, or NetworkSegment, depending on which object defines the MemberClass.
Go interfaces enable uniform processing across different object types. When introducing a new object type, you need to determine which interfaces to implement based on the object's role.
| Interface | Purpose | When to Implement |
|---|---|---|
| NameSpacer | Parameter namespace and config generation | When the object owns a namespace and generates config blocks |
| LabelOwner | DOT label parsing and class assignment | When object receives labels from DOT file |
| MemberReferrer | Cross-object references via MemberClass | When object can reference other objects |
| FileGenerator | Determine generated file names | When object controls file generation |
| ValueOwner | Attach mode parameter support (Values) | When object can have Values attached via param_rule with mode: attach
|
Location: pkg/types/object.go:84
The interface for objects that own a parameter namespace. Owning a namespace means:
- The object can generate config blocks using templates
- The object needs to manage parameters that templates can reference
- The object participates in parent-child and dependency relationships that determine config block embedding order
From the AC/CC perspective, objects that don't affect parameter namespaces arguably shouldn't be part of NetworkModel. In practice, all current NetworkModel objects have namespaces and implement NameSpacer.
Historical note: In earlier versions, Connection did not implement NameSpacer—configuration for connections was generated by the endpoint Interfaces instead. This design was deprecated, and Connection now implements NameSpacer with its own config templates. This history demonstrates that not all objects necessarily need NameSpacer; future objects may exist that don't own namespaces.
NameSpacer defines:
type NameSpacer interface {
// Structural hierarchy (parent-child ownership)
ChildClasses() ([]string, error)
Childs(c string) ([]NameSpacer, error)
// Processing dependencies (may differ from hierarchy)
DependClasses() ([]string, error)
Depends(c string) ([]NameSpacer, error)
// Parameter flags (which parameters this object needs)
setParamFlag(k string)
hasParamFlag(k string) bool
IterateFlaggedParams() <-chan string
// Self parameters (object's own computed values)
AddParam(k, v string)
HasParam(k string) bool
GetParams() map[string]string
// Relative namespace (complete namespace for templates)
BuildRelativeNameSpace(globalParams map[string]map[string]string) error
SetRelativeParam(k, v string)
GetRelativeParams() map[string]string
GetParamValue(string) (string, error)
// Config templates from this object's classes
GetConfigTemplates(cfg *Config) []*ConfigTemplate
GetPossibleConfigTemplates(cfg *Config) []*ConfigTemplate
// Debug
StringForMessage() string
}| Object | ChildClasses() | DependClasses() | Notes |
|---|---|---|---|
| NetworkModel | Node, Group, Connection, Segment | Same as Childs | Top-level container |
| Node | Interface, Member_* | Same as Childs | Owns interfaces |
| Interface | Neighbor_, Member_ | Same as Childs | Neighbors are children |
| Connection | Member_* | Member_* + Interface | Depends on Src/Dst interfaces |
| NetworkSegment | Member_* | Member_* + Interface, Connection | Depends on member interfaces |
| Group | (empty) | Node | Key example: no children, but depends on member nodes |
| Neighbor | (empty) | (empty) | Leaf object |
| Member | (empty) | (empty) | Leaf object |
Each object builds its relative namespace differently:
| Object | Namespace Contents |
|---|---|
| NetworkModel | Self params, global place labels |
| Node | Self params, group params (group_*), meta value labels |
| Interface | Self params, node params (node_*), opposite interface (opposite_*), group params |
| Connection | Self params, global place labels |
| NetworkSegment | Self params, global place labels |
| Neighbor | Self interface params, opposite params, neighbor params (neighbor_*), neighbor opposite params |
| Member | Referrer params, member params (member_*), node params (if interface) |
| Group | Self params as group_* prefix |
Location: pkg/types/object.go:196
Purpose: Manage labels parsed from DOT graph attributes. Labels determine which YAML class definitions apply to each object, controlling configuration generation behavior.
Label types managed:
-
Class labels: Assign YAML class definitions (e.g.,
router,ospf_interface) -
Relational class labels: Assign classes to related objects (e.g.,
segment#vlan_seg) -
Place labels: Create globally-referenceable namespaces (
@spine1) -
Value labels: Direct parameter assignment (
hostname=core-router)
type LabelOwner interface {
// Label access
ClassLabels() []string // e.g., ["router", "ospf"]
RelationalClassLabels() []RelationalClassLabel // e.g., segment#segclass
PlaceLabels() []string // e.g., ["@spine1"]
ValueLabels() map[string]string // e.g., {"ipv4_addr": "10.0.0.1"}
MetaValueLabels() map[string]string // e.g., {"$ref": "@spine1.loopback"}
// Label setting
SetLabels(cfg *Config, labels []string, moduleLabels []string) error
AddClassLabels(labels ...string)
// Class access
HasClass(string) bool
GetClasses() []ObjectClass
ClassDefinition(cfg *Config, cls string) (interface{}, error)
// Virtual object flag (for objects that don't generate config)
SetVirtual(bool)
IsVirtual() bool
}Implementers: Node, Interface, Connection, Group, NetworkSegment
Note on NetworkSegment: While NetworkSegment objects are created during IP assignment (not directly from DOT), they can still receive class labels via Relational Class Labels syntax. For example, segment#vlan_seg in a connection's label assigns a SegmentClass to the segment containing that connection. See DOT File Syntax - Relational Class Labels for details.
Not implemented by: Neighbor, Member (they don't receive labels directly from DOT)
Location: pkg/types/object.go:290
Purpose: Enable objects to reference other objects that share a specified class, and generate config blocks for each reference. This is similar to Neighbor (which references adjacent interfaces), but Member references objects by class membership rather than topology adjacency.
Use case: In BGP configuration, a node needs to list all BGP peer nodes and generate neighbor statements for each:
node_class:
- name: bgp
classmembers:
- node: bgp # Reference all nodes with "bgp" class
config:
- name: nbconfig
template:
- " neighbor {{ .member_ipv4_loopback }} remote-as {{ .member_as_number }}"The template is applied for each Member, with member_* prefix accessing the referenced object's parameters.
type MemberReferrer interface {
LabelOwner // Must also be a LabelOwner
NameSpacer // Must also be a NameSpacer
AddMemberClass(*MemberClass)
GetMemberClasses() []*MemberClass
AddMember(*Member)
GetMembers() []*Member
}Implementers: Node, Interface, Connection, NetworkSegment
Not implemented by: Group (references nodes directly via subgraph membership), Neighbor, Member
Location: pkg/types/object.go:74
Purpose: Determine which output files an object will generate. While NameSpacer handles config block generation, FileGenerator identifies which files those blocks ultimately produce.
Current status: This interface provides minimal abstraction—it simply returns file names. The actual file generation logic is handled elsewhere. NetworkModel and Node implement this interface because they are the granularities at which files are generated (network-level files vs per-node files).
type FileGenerator interface {
FilesToGenerate(cfg *Config) []string
}Implementers:
- NetworkModel: Returns network-level files (from NetworkClass templates, e.g., topology files)
- Node: Returns per-node files (from NodeClass templates based on the node's class labels)
Use case: Modules (e.g., containerlab) use this to determine which files to mount for each node, avoiding mounting files that a node doesn't actually generate.
Location: pkg/types/object.go:349
Purpose: Enable objects to have Values attached. Values are virtual objects generated by param_rule with mode: attach, allowing multiple parameter sets to be attached to a single object.
Relationship with NameSpacer: ValueOwner extends NameSpacer. While NameSpacer provides basic parameter management (AddParam for single key-value pairs), ValueOwner adds support for multiple parameter sets through Values.
type ValueOwner interface {
NameSpacer
// SortKey returns a string key for deterministic sorting of objects.
// Used to ensure stable parameter assignment order.
SortKey() string
// AddValue adds a Value to this owner
AddValue(v *Value)
// GetValues returns all Values attached to this owner
GetValues() []*Value
// GetValuesByParamRule returns Values generated by the specified param_rule
GetValuesByParamRule(paramRuleName string) []*Value
}Implementers: NetworkModel, Node, Interface, Connection, Group, NetworkSegment
Use case: Attach mode in param_rule generates multiple Values per object. For example, a switch node might have multiple VLANs attached, each represented as a Value with its own parameters (vlan_id, vlan_name, etc.). Templates can then iterate over these Values to generate repeated config blocks.
See Parameter Generation - Attach Mode for usage details.
dot2net uses Go struct embedding to share common functionality across objects. The pattern is:
- Interface defines the required behavior
- Common struct provides shared implementation for most methods
- Object embeds the common struct to inherit those methods
- Object implements remaining methods specific to its role
| Interface | Common Struct | Relationship |
|---|---|---|
| NameSpacer | NameSpace |
NameSpace provides parameter storage methods; objects implement hierarchy methods (ChildClasses, Childs, DependClasses, Depends, BuildRelativeNameSpace, GetConfigTemplates) |
| LabelOwner | ParsedLabels |
ParsedLabels provides label storage and query methods; objects implement SetLabels, SetClasses, ClassDefinition
|
| MemberReferrer | memberReference |
memberReference provides complete implementation of all MemberReferrer-specific methods |
| ValueOwner | valueReference |
valueReference provides Value management methods; objects implement SortKey()
|
| (none) | addressedObject |
No corresponding interface; provides layer awareness and IP policy management |
Example with Node:
type Node struct {
Name string
Interfaces []*Interface
*NameSpace // Embeds parameter methods
*ParsedLabels // Embeds label methods
*memberReference // Embeds member reference methods
addressedObject // Embeds layer/policy methods (not pointer)
}- Methods from
NameSpace(e.g.,GetParams(),AddParam()) are automatically available on Node - Node must still implement
ChildClasses(),Childs(),BuildRelativeNameSpace(), etc. because these require object-specific logic - The combination of embedded methods + object-specific methods satisfies the NameSpacer interface
| Object | Embedded | Implements |
|---|---|---|
Node |
NameSpace, ParsedLabels, memberReference, valueReference, addressedObject | NameSpacer, LabelOwner, MemberReferrer, ValueOwner, FileGenerator |
Interface |
NameSpace, ParsedLabels, memberReference, valueReference, addressedObject | NameSpacer, LabelOwner, MemberReferrer, ValueOwner |
Connection |
NameSpace, ParsedLabels, memberReference, valueReference, addressedObject | NameSpacer, LabelOwner, MemberReferrer, ValueOwner |
Group |
NameSpace, ParsedLabels, valueReference | NameSpacer, LabelOwner, ValueOwner |
NetworkSegment |
NameSpace, ParsedLabels, memberReference, valueReference | NameSpacer, LabelOwner, MemberReferrer, ValueOwner |
Neighbor |
NameSpace | NameSpacer |
Member |
NameSpace | NameSpacer |
NameSpace (pkg/types/object.go:114):
type NameSpace struct {
paramFlags mapset.Set[string] // Which parameters this object needs
params map[string]string // Self parameters
relativeParams map[string]string // Complete namespace for templates
}Provides: AddParam, GetParams, SetRelativeParam, GetRelativeParams, GetParamValue, etc.
ParsedLabels (pkg/types/object.go:222):
type ParsedLabels struct {
classLabels []string
rClassLabels []RelationalClassLabel
placeLabels []string
valueLabels map[string]string
metaValueLabels map[string]string
Classes []ObjectClass
virtual bool
}Provides: ClassLabels, PlaceLabels, ValueLabels, HasClass, GetClasses, IsVirtual, etc.
memberReference (pkg/types/object.go:301):
type memberReference struct {
memberClasses []*MemberClass
members []*Member
}Provides: AddMemberClass, GetMemberClasses, AddMember, GetMembers
addressedObject (pkg/types/object.go:336):
type addressedObject struct {
layerPolicy map[string]*IPPolicy
layers mapset.Set[string]
}Provides: AwareLayer, GetLayerPolicy, setPolicy
Note: addressedObject has no corresponding interface. An addressOwner interface was previously considered but commented out as having "no abstracted usage"—the methods are called explicitly on specific object types rather than through polymorphism.
valueReference (pkg/types/object.go:362):
type valueReference struct {
values []*Value
}Provides: AddValue, GetValues, GetValuesByParamRule
This struct provides the common implementation for ValueOwner interface methods. Each Value represents a parameter set generated by param_rule with mode: attach.