Skip to content

NetworkModel Design

sat edited this page Jan 3, 2026 · 2 revisions

NetworkModel Design

This page documents how the concepts from Basic Concepts are implemented in dot2net's NetworkModel structure.

Overview

NetworkModel is the core data structure that realizes the topology-driven configuration approach. Its primary responsibilities are:

  1. Parameter namespace management - Building and managing parameter namespaces for each object
  2. Parameter generation - Computing values needed for configuration templates (including IP address assignment)
  3. 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.

From Concepts to Implementation

Architecture Classes as Object Hierarchy

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 as Labels

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.

Parameter Namespace

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
}

Building Relative Namespace (Bottom-up)

While the object hierarchy is top-down, relative namespaces are built bottom-up. Each object's relativeParams contains:

  1. Its own params (without prefix)
  2. Parent object's params (with node_ prefix for interfaces)
  3. Related objects' params (with conn_, neighbor_, etc. prefixes)
  4. Global place-labeled params (accessed by @name syntax)

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.

Two Types of Object Relationships

This is a critical design distinction:

Childs: Structural Hierarchy (Top-down)

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)

Depends: Processing Dependencies

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).

Why Separation is Necessary

Consider a Group that represents "all spine routers". The Group needs to:

  1. Access parameters from its member Nodes (e.g., list all their loopback IPs)
  2. Generate its own parameters based on members
  3. 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.

Referential Objects and Dependencies

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).

Network Objects

Objects are created from different sources during model building:

Objects from DOT File

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

Objects from IP Assignment

Object Network Role Created When
NetworkSegment Broadcast domain per layer During IP address assignment, grouping interfaces that share the same network segment

Objects from Label Definitions

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.

Interface Abstraction

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 Summary

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

NameSpacer

Location: pkg/types/object.go:84

The interface for objects that own a parameter namespace. Owning a namespace means:

  1. The object can generate config blocks using templates
  2. The object needs to manage parameters that templates can reference
  3. 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
}

Childs vs Depends Implementation by Object

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

BuildRelativeNameSpace Implementation

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

LabelOwner

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)

MemberReferrer

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

FileGenerator

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.

ValueOwner

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.

Structure Composition

dot2net uses Go struct embedding to share common functionality across objects. The pattern is:

  1. Interface defines the required behavior
  2. Common struct provides shared implementation for most methods
  3. Object embeds the common struct to inherit those methods
  4. Object implements remaining methods specific to its role

Interface-Struct Correspondence

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

How Embedding Works

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

Objects and Their Embedded Structures

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

Common Struct Details

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.

Clone this wiki locally