Architecture
This document describes NetLoom's internal architecture and design.
Overview
NetLoom follows a controller-based architecture with a singleton Application class coordinating multiple specialized controllers. Each controller handles a specific domain of functionality.
flowchart TB
subgraph CLI["CLI Package (netloom/cli/)"]
group["_group.py\n(load + convert topology)"]
cmds["infra.py · lifecycle.py\nmanage.py · show.py"]
end
subgraph App["Application (core/application.py)"]
direction LR
IC[Infrastructure\nController]
CC[Config\nController]
end
subgraph External["External Systems"]
direction LR
VBox[VirtualBox\nVBoxManage]
Jinja[Jinja2\nTemplates]
end
CLI --> App
App --> External
Core Components
Application
The Application class (netloom/core/application.py) is the central singleton that:
- Manages controller instances (lazy initialization)
- Provides a shared
Consolefor Rich terminal output - Tracks
workdir,debugstate, andvbox_settings
Controllers are accessed as properties and created on first access. They must never import each other at module level - siblings are accessed via self.app.other_controller inside methods.
Controllers
InfrastructureController
Location: netloom/controllers/infrastructure.py
Manages VirtualBox VM lifecycle via VBoxManage:
| Method | Description |
|---|---|
init(internal, workdir) |
Import OVA and create golden snapshot |
create(internal) |
Create linked clones with config-drives |
start(internal) |
Start all VMs |
stop(internal) |
Stop all VMs (ACPI shutdown) |
destroy(internal, destroy_base) |
Remove VMs (hard shutdown + unregister) |
ConfigController
Location: netloom/controllers/config.py
Handles configuration generation and deployment:
| Method | Description |
|---|---|
list_template_sets() |
List available template sets |
generate(internal) |
Render Jinja2 templates; auto-detects sets per node |
attach(internal) |
Copy configs to VM config-drives |
save(internal) |
Pull configs from config-drives to host |
restore(internal) |
Restore saved configs to staging area |
generate() always renders the networkd set and auto-detects additional sets per node:
bird— whenrouting.engineisbirdnftables— whenservices.firewallis configuredwireguard— whenservices.wireguardis configured
Data Flow
1. Topology Loading
YAML File → load_topology() → Topology (Pydantic, mirrors YAML schema)
│
▼
convert_topology() → InternalTopology (runtime representation)
Both functions are called in the CLI group (netloom/cli/_group.py) before dispatching to any subcommand. The result is stored in ctx.obj["internal"].
The conversion adds:
- Deterministic MAC addresses per interface (seeded from
<topo-id>-<node>-<iface>) - VirtualBox NIC index allocation per node
- Peer node relationships
- Merged sysctl settings (defaults + node-specific)
- Resolved service configurations
2. Configuration Generation
InternalTopology → ConfigController.generate()
│
├─→ networkd/ (always)
├─→ bird/ (if routing.engine == bird)
├─→ nftables/ (if services.firewall set)
└─→ wireguard/ (if services.wireguard set)
│
▼
workdir/configs/<node>/etc/...
3. VM Deployment
InternalTopology → InfrastructureController.create()
│
├─→ VBoxManage clonevm (linked clone per node)
├─→ VBoxManage createmedium (config-drive VMDK)
└─→ VBoxManage modifyvm (NICs → internal networks)
Models
External Models (netloom/models/config.py)
Pydantic models that match the YAML schema exactly:
Topology— root model (meta,networks,nodes,defaults)Meta— topology metadataDefaults— global defaults (ip_forwarding,sysctl,vbox)Network— named L2 network segmentNode— node configurationInterfaceConfig— named interface (dict value innode.interfaces)VLANConfig,TunnelConfig,BridgeConfigStaticRoute—{destination, gateway}objectRoutingConfig,OSPFConfig,RIPConfigServicesConfig,WireguardConfig,FirewallConfig
Internal Models (netloom/models/internal.py)
Runtime representations with computed and enriched fields:
InternalTopology— full topology with node/link indexesInternalNode— node with resolved interfaces, computed config dirsInternalInterface— interface withmac_address,vbox_nic_index,peer_node,networkInternalNetwork— L2 segment with VirtualBox network name and participant listInternalLink— point-to-point connection (subset of networks with exactly 2 participants)InternalBridge,InternalVLAN,InternalTunnelInternalRouting,InternalServices,InternalSysctlInternalVBoxSettings— per-node or topology-level VirtualBox settings
Template System
Templates live in netloom/templates/ organized by technology:
templates/
├── _base/
│ └── _macros.j2 # Shared Jinja2 macros
├── networkd/ # systemd-networkd (always rendered)
│ ├── hostname.j2
│ ├── interface.link.j2 # MAC-based interface rename rule
│ ├── interface.network.j2
│ ├── bridge.netdev.j2
│ ├── bridge.network.j2
│ ├── bridge-port.network.j2
│ ├── vlan.netdev.j2
│ ├── vlan.network.j2
│ ├── vlan-parent.network.j2
│ ├── tunnel.netdev.j2
│ ├── tunnel.network.j2
│ ├── routes.network.j2
│ └── sysctl.conf.j2
├── bird/ # BIRD routing daemon
│ ├── bird.conf.j2
│ ├── ospf.conf.j2
│ ├── rip.conf.j2
│ └── static.conf.j2
├── nftables/ # nftables firewall
│ └── nftables.conf.j2
├── wireguard/ # WireGuard VPN
│ └── wg0.conf.j2
└── services/
└── services.list.j2
Template Context
Each template receives:
node—InternalNodefor the node being configuredtopology— the fullInternalTopologyiface—InternalInterface(for per-interface templates)
Output Path Mapping
Template filenames determine output paths (resolved by ConfigController._OUTPUT_PATHS):
| Template pattern | Output path |
|---|---|
hostname.j2 |
etc/hostname |
*.network.j2 |
etc/systemd/network/<name>.network |
*.netdev.j2 |
etc/systemd/network/<name>.netdev |
*.link.j2 |
etc/systemd/network/<name>.link |
sysctl.conf.j2 |
etc/sysctl.d/99-netloom.conf |
*.conf.j2 |
etc/<name>.conf |
VirtualBox Integration
NetLoom uses VBoxManage CLI for all VM operations.
Linked Clones
VMs are created as linked clones from a golden snapshot:
Benefits:
- Fast creation (no full disk copy)
- Minimal disk usage
- Shared base image
Config-Drives
Each VM has a FAT-formatted VMDK attached as a secondary disk:
The config-drive contains network configurations that the VM applies on boot.
Data Layer (netloom/data/)
Config-drive I/O is handled by two modules:
configdrive.py — ConfigDrive
ConfigDrive is a dataclass wrapping a VirtualBox split VMDK pair:
| File | Role |
|---|---|
<node>-cfg.vmdk |
VirtualBox descriptor (metadata only) |
<node>-cfg-flat.vmdk |
Raw data file — the actual FAT16 filesystem |
_fat.py operates directly on the flat file; the descriptor is only used by VirtualBox itself.
drive = ConfigDrive(vmdk=Path("R1-cfg.vmdk"))
drive.copy_in(Path("workdir/configs/R1")) # staging → config-drive
drive.copy_out(Path("workdir/saved/R1")) # config-drive → host
| Method | Description |
|---|---|
copy_in(src_dir) |
Write a directory tree into the FAT filesystem |
copy_out(dst_dir) |
Read all files from the FAT filesystem to host |
.flat |
Property returning the -flat.vmdk path |
_fat.py — FAT16 helpers
Internal helpers that operate on the raw flat VMDK using fattools:
| Function | Description |
|---|---|
open_fat_fs(path, mode) |
Context manager — opens the flat file as a FAT fs |
format_fat16(path, size_mb) |
Format a raw file as FAT16 |
makedirs(fs, path) |
Create directory hierarchy inside the FAT fs |
copy_dir_recursive(fs, dst, copied) |
Recursively copy FAT fs tree to a local directory |
Internal Networks
Networks are implemented as VirtualBox internal networks named <topology-id>_<network-name>:
All interfaces on different nodes that share the same network name in the YAML are connected to the same VirtualBox internal network, acting as a virtual L2 switch.
Extension Points
Custom Template Sets
Create a new directory in netloom/templates/ with your templates:
Then register it in ConfigController._get_output_path and add a _render_template_set call in ConfigController.generate.
Adding Controllers
- Create a new class inheriting from
BaseController(netloom/core/controller.py) - Add a
@cached_propertyinApplicationreturning the new controller - Add CLI commands in the appropriate module under
netloom/cli/