A vendor-agnostic network and compute lab automation platform combining NetBox inventory, Nornir network device configuration, and a modern React + FastAPI web interface for provisioning and reserving Proxmox VMs and LXC containers, managing physical network hardware via Unimus Pro and Git, and orchestrating virtual network labs with ContainerLab.
┌──────────────────────────────────────────────────────────────────────────────┐
│ React Frontend (Vite 7) │
│ Dashboard · Resources · Provision Wizard · Hardware · ContainerLab · │
│ Lab Topology · Reservation Calendar │
└──────────────┬────────────────────────────────────────────┬─────────────────┘
│ REST API │
┌──────────────▼────────────────────────────────────────────▼─────────────────┐
│ FastAPI Backend (api/) │
├──────────────┬───────────────┬────────────────┬────────────┬────────────────┤
│ Proxmox │ Reservations │ NetBox Proxy │ Hardware │ ContainerLab │
│ (proxmoxer) │ (SQLAlchemy) │ (httpx) │ (Unimus + │ (local/SSH/ │
│ services/ │ models.py │ services/ │ Git) │ REST API) │
│ proxmox/ │ │ netbox/ │ │ services/ │
│ │ │ │ │ containerlab/ │
└──────────────┴───────────────┴────────────────┴────────────┴────────────────┘
│ │ │ │
Proxmox VE API NetBox API Unimus Pro clab binary /
(QEMU VMs + LXC) (Device inventory) (Config backup) SSH / REST API
│
Git HTTPS + PAT
(Config + topology repos)
Real-time cluster overview — one card per Proxmox node showing CPU, RAM, and disk utilization with auto-refresh every 30 seconds.
Browse all VMs and LXC containers across nodes. Start, stop, and delete resources directly from the table. Click any resource row for a detailed modal view.
7-step wizard to provision a new VM or LXC container:
- Type — choose VM (QEMU/KVM) or LXC Container, select target node
- Template — select ISO or container template from node storage
- User — set username, password, and optional SSH public key
- Resources — set name, VMID, CPU cores, RAM, disk size
- Network & Storage — configure storage pool and network interfaces:
- Storage: select from available Proxmox storage pools (shows type and free space)
- NICs: add up to 8 network interfaces per VM/LXC — each with:
- Bridge selection (live list from the node's configured bridges)
- Optional VLAN ID (1–4094; empty = untagged)
- NIC model (VMs: VirtIO / E1000 / RTL8139)
- Dual-stack IP: IPv4 / CIDR (or
dhcp) + IPv6 / Prefix (orauto/dhcp6) - Default gateways (IPv4 / IPv6) and DNS servers
- ☁ NetBox IPAM Integration: Connects to your NetBox instance to browse and select IP addresses, Prefix gateways, and VLANs directly in the wizard.
- Automatically allocates the next available IP address from a selected Prefix.
- Smart IPv6 handling (avoids network addresses like
::and.0). - Auto-detects explicit default gateways from NetBox records, or gracefully assumes
.1/::1as gateways when absent.
- Review — confirm all settings including per-NIC summary table
- Progress — live task progress bar polling the Proxmox UPID
Interactive drag-and-drop canvas (powered by React Flow / @xyflow/react) for visualizing and designing your infrastructure topology:
- Drag VMs and LXC containers from the sidebar onto the canvas
- Draw connections between resources to document network relationships
- Snap-to-grid layout with dark-mode dot background
- Node selection highlights with cyan glow; edge selection with stroke emphasis
- Prevents duplicate resources from being placed on the canvas
FullCalendar week/month/day view. Click any time slot to reserve a resource window. Conflict detection prevents double-booking the same node.
Manage physical network devices (routers, switches, firewalls) that are registered in NetBox. Devices are discovered automatically from NetBox DCIM, and the page provides:
- Device table — searchable/filterable by name, type, IP, or site; shows both IPv6 and IPv4 management IPs (IPv6 preferred when available) with colour-coded family badges
- Per-device drawer with four tabs:
- Overview — NetBox device details: type, manufacturer, site, rack, role, all management IPs, interface table with mgmt-only indicators
- Backup — trigger a real-time backup via Unimus Pro; selectively archive to Unimus, Git, or both; shows git commit SHA and a config preview after completion
- Config History — browsable git log per device; click any commit to view the full config at that point in time with one-click copy
- Provision — push a "golden config" to the live device via Unimus Pro; choose source of truth (Git or Unimus latest backup); Git source lets you pick any historical commit
- Status badges (header) — real-time reachability indicators for Unimus (shows device count) and Git repo (shows configured branch)
- Integration warnings — actionable alerts when
UNIMUS_URLorGIT_CONFIG_REPOare not configured
Orchestrate virtual network topologies powered by ContainerLab. The page is hidden from the sidebar unless CLAB_MODE is set in .env. Three backends are supported:
| Mode | How it works |
|---|---|
local |
Calls the clab binary directly on the Direttore host. The process user must be a member of the clab_admins group. |
ssh |
Connects via SSH (using paramiko) to a remote host running ContainerLab. |
rest |
Calls the clab-api-server HTTP REST API on a remote host. |
Page features:
- Status badge — shows backend mode (local / ssh / rest) and live reachability
- Running labs table — name, node count, topology path, and lab age (how long ago the lab was deployed); click any row to open the lab detail drawer
- Lab detail drawer — per-node table with kind, image, IPv4/IPv6, and colour-coded container state (running / stopped / error)
- Per-node actions: restart, stop, or start individual nodes without tearing down the whole lab
- In-browser terminal: click the console button on any running node to open a full xterm.js terminal session over WebSocket — no SSH client or ProxyJump needed
- SSH ProxyJump commands: copy a ready-made
ssh -J ...command for direct terminal access outside the browser
- Deploy panel — select any
.ymltopology file and deploy with configurable options:- Reconfigure toggle — choose whether to run
clab deploy --reconfigure(redeploys existing nodes) or a plain deploy - Pre-deploy validation — validate the topology file with
clab deploy --checkbefore committing; result shown inline - Topology graph preview — an interactive React Flow canvas renders the node/link graph parsed directly from the YAML before you deploy
- YAML editor — edit the topology file in-browser with syntax highlighting before deploying
- Real-time deploy log — streaming SSE output with manual dismissal
- Reconfigure toggle — choose whether to run
- Destroy confirmation dialogue — confirm before destroying a running lab to prevent accidental teardown
- Topology Workspace — a fully interactive file manager for
CLAB_TOPO_DIR:- Create, rename, duplicate, and delete folders and files in-browser
- Create and edit text files (
.cfg,.cli,.json,.yml) directly in the browser - Navigate directories using dynamic breadcrumbs
- Git history tab — per-file commit log shown when
CLAB_TOPO_GIT_REPOis configured - Deploy directly from any
.ymltopology file within the workspace
- Topology upload — file-picker upload of
.ymland config files targeting the current workspace directory - All sub-features (Git history tab, SSH/REST indicators) are hidden when their respective env vars are not set
ContainerLab exposes native SSH on the mgmt network bridge of the deployment host (usually 172.20.20.0/24). You can find the exact node IPs within the Direttore "Inspect" drawer.
Local Mode: If Direttore and your labs run locally on your machine, simply connect directly:
ssh admin@172.20.20.3SSH Remote Mode (ProxyJump):
If Direttore deploys labs to a remote SSH worker (e.g. CLAB_MODE=ssh), your workstation does not have a direct route to 172.20.20.x. Direttore addresses this by surfacing a Console ProxyJump Command directly in the Lab drawer!
Clicking the console button will automatically copy an SSH command formatted like this:
ssh -J root@my-clab-worker admin@172.20.20.3This instantly routes your SSH session through the worker node into the virtual container, giving you immediate terminal access without any VPNs or static routes.
Pro-Tip: You can persist this routing structure in your
~/.ssh/config:Host 172.20.20.* ProxyJump root@my-clab-worker User adminWith this block, you can simply run
ssh 172.20.20.3and your SSH client will automatically jump through the remote worker!
Unimus is contacted from the server running Direttore, not from your browser. Common issues:
| Symptom | Cause | Fix |
|---|---|---|
ConnectError: All connection attempts failed |
DNS doesn't resolve from server, or wrong hostname | Use the bare IP address in UNIMUS_URL (e.g. https://10.x.x.x) |
| Same error with IPv6 host | Must use bracket notation | UNIMUS_URL=https://[2001:db8::10] |
| HTTP 401 | Wrong or expired API token | Re-generate in Unimus → Settings → Security → API access tokens |
| HTTP 403 | Token lacks API permission | Enable "API Access" scope for the token |
| Devices found but backup returns "not found in Unimus" | Unimus stores devices by hostname, not IP — and there is no DNS | Use the manual Unimus device link (see below) |
Backup fails with 404 on /api/v2/jobs/backupDevices |
Wrong endpoint for your Unimus version | Direttore auto-detects: tries PATCH /api/v2/jobs/backup first (2.8+), then POST variants |
| Backup warning: "no working job endpoint found" | None of the trigger endpoints returned 2xx | The fallback returns the latest existing Unimus backup — still archives it to Git |
Use the built-in debug endpoint to diagnose from the server:
curl -s http://127.0.0.1:8000/api/hardware/debug-unimus | python3 -m json.toolThis now probes all known job endpoint variants (GET and POST/PATCH) so you can see exactly which ones your Unimus installation exposes.
When Direttore cannot automatically match a NetBox device to a Unimus device (because Unimus stores devices by FQDN but there is no DNS), use the manual link API:
# Step 1 — list all devices known to Unimus (to find the right address string)
curl -s http://127.0.0.1:8000/api/hardware/unimus-devices | python3 -m json.tool
# Output: [{"id": 5, "address": "gw1.mgt.cu-es.net", "vendor": "MikroTik", ...}, ...]
# Step 2 — link the NetBox device ID to the Unimus address
# (Find the NetBox device ID from the Hardware page URL or from /api/hardware/devices)
curl -s -X PUT http://127.0.0.1:8000/api/hardware/devices/2/unimus-link \
-H "Content-Type: application/json" \
-d '{"unimus_address": "gw1.mgt.cu-es.net"}'
# Step 3 — verify
curl -s http://127.0.0.1:8000/api/hardware/devices/2/unimus-link | python3 -m json.tool
# To remove a link (e.g. after DNS is configured and auto-match works again)
curl -s -X DELETE http://127.0.0.1:8000/api/hardware/devices/2/unimus-linkLinks are stored in /opt/unimus_links.json (one directory above GIT_CONFIG_LOCAL_PATH) and persist across restarts. When a link exists, it takes priority over all automatic discovery strategies.
When a backup or provision is triggered, Direttore searches for the Unimus device using these strategies in order:
| Priority | Strategy | Details |
|---|---|---|
| 0 | Manual link store | Fastest — uses the pinned unimus_address if set via the link API |
| 1 | Exact address match | findByAddress(primary_ip) — works if Unimus stores the device by IP |
| 2 | Reverse DNS | Resolves primary IP to FQDN, then tries findByAddress(fqdn) |
| 3 | NetBox device name | Tries findByAddress(hostname) directly |
| 4 | Linear scan | Fetches all Unimus devices and matches by address substring / device name |
- Python 3.13+ (backend)
- uv — fast Python package manager (replaces pip/venv)
- Node.js 20.19+ (frontend — see install guide below)
- A Proxmox VE host, or use
PROXMOX_MOCK=truefor development without hardware - NetBox instance (optional — only needed for the inventory proxy routes and NetBox NIC picker)
- ContainerLab (optional — only needed for the ContainerLab management page)
Important
Node.js 20.19+ is required by Vite 7. The system apt packages on Debian/Ubuntu are often too old, and binaries compiled for newer microarchitectures can produce Illegal instruction crashes on older or virtualized CPUs. Use nvm to install Node — it compiles a native binary that matches your CPU.
git clone <repo-url>
cd direttorecp .env.example .env
# Edit .env with your Proxmox host, credentials, and NetBox token
# Set PROXMOX_MOCK=true for development without real hardwareThe backend is managed with uv. Install it if you don't have it:
# Install uv (macOS / Linux)
curl -LsSf https://astral.sh/uv/install.sh | shThen install and run:
# Install all dependencies from pyproject.toml + uv.lock
uv sync
# Start the API server (IPv6 loopback; adjust host as needed)
PROXMOX_MOCK=true uv run uvicorn api.main:app --host ::1 --reload --port 8000
# Or on IPv4 / all interfaces:
PROXMOX_MOCK=true uv run uvicorn api.main:app --host 0.0.0.0 --reload --port 8000API docs available at http://localhost:8000/docs (Swagger) and http://localhost:8000/redoc (ReDoc). Health check: http://localhost:8000/healthz
If you do not have Node.js and npm installed, the recommended and most reliable way to install them (and to avoid architecture or "Illegal instruction" errors) is via Node Version Manager (nvm):
# 1. Install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# 2. Activate NVM (or restart your terminal)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# 3. Install Node.js 20 (LTS)
nvm install 20
nvm use 20Note on "Illegal instruction" errors: If you receive an
Illegal instructionerror when runningnpmornode, your CPU might lack the latest instruction sets (like SSE4.1/AVX) required by Node 20+. If you installed using a package manager (e.g.apt install nsolidorsnap), remove those packages (sudo apt remove nsolid nodejs npm). Usenvmas shown above. Ifnvm install 20still yields "Illegal instruction", fallback to Node 18 by runningnvm install 18 && nvm use 18.
Then, to install dependencies and run the development server:
cd frontend
npm install
npm run devFrontend available at http://[::1]:5173 (or http://localhost:5173).
Note: Vite is configured to bind to
::1(IPv6 loopback). If your system doesn't have IPv6 loopback, changehost: '::1'tohost: '0.0.0.0'infrontend/vite.config.js.
The Vite dev server also proxies the following paths directly to the FastAPI backend (no nginx required during development):
| Vite proxy path | Backend |
|---|---|
/api/* |
http://localhost:8000 |
/docs |
http://localhost:8000 |
/redoc |
http://localhost:8000 |
/openapi.json |
http://localhost:8000 |
Controls which address families Traefik binds on the host. IPv6 / dual-stack is the default. See docs/daemon-ipv6.md for Docker daemon setup.
| Variable | Default | Description |
|---|---|---|
TRAEFIK_HOST_IP |
:: |
Host IP for Docker port bindings. :: = dual-stack (IPv4+IPv6); 0.0.0.0 = IPv4 only |
TRAEFIK_BIND_ADDR |
[::] |
Address Traefik entrypoints bind inside the container. [::] = all; 0.0.0.0 = IPv4 only |
ENABLE_IPV6 |
true |
Adds an IPv6 IPAM subnet (fd00:0:1::/64) to the internal Docker proxy network |
| Mode | TRAEFIK_HOST_IP |
TRAEFIK_BIND_ADDR |
ENABLE_IPV6 |
|---|---|---|---|
| Dual-stack / IPv6-preferred (default) | :: |
[::] |
true |
| IPv6-only | :: |
[::] |
true + drop IPv4 at host firewall |
| IPv4-only | 0.0.0.0 |
0.0.0.0 |
false |
| Variable | Default | Description |
|---|---|---|
DOMAIN |
direttore.example.com |
Public hostname Traefik routes and requests a certificate for |
ACME_EMAIL |
admin@example.com |
Email sent to Let's Encrypt for expiry notifications |
| Variable | Default | Description |
|---|---|---|
PROXMOX_HOST |
192.168.1.100 |
Proxmox VE hostname or IP |
PROXMOX_USER |
root@pam |
Proxmox API user |
PROXMOX_PASSWORD |
— | Proxmox API password |
PROXMOX_VERIFY_SSL |
false |
Verify TLS certificate |
PROXMOX_MOCK |
false |
Use mock data (no real Proxmox needed) |
NETBOX_URL |
http://localhost:8000 |
NetBox base URL |
NETBOX_TOKEN |
— | NetBox API token |
DATABASE_URL |
sqlite+aiosqlite:///./direttore.db |
SQLAlchemy async DB URL |
API_CORS_ORIGINS |
http://localhost:5173,http://localhost:3000 |
Comma-separated allowed CORS origins |
| Variable | Default | Description |
|---|---|---|
UNIMUS_URL |
— | Unimus base URL — e.g. https://unimus.example.com or https://[2001:db8::10] for IPv6 hosts |
UNIMUS_TOKEN |
— | Unimus API Bearer token (Settings → Security → API access tokens) |
GIT_CONFIG_REPO |
— | HTTPS clone URL of the config archive repo (e.g. https://github.com/org/configs.git) |
GIT_CONFIG_BRANCH |
main |
Branch to commit device configs to |
GIT_CONFIG_AUTH_TOKEN |
— | Personal Access Token for HTTPS Git auth (needs repo read+write scope) |
GIT_CONFIG_LOCAL_PATH |
/opt/direttore/config-repo |
Local clone path on the server |
GIT_CONFIG_AUTHOR_NAME |
Direttore |
Git commit author name |
GIT_CONFIG_AUTHOR_EMAIL |
direttore@localhost |
Git commit author email |
Leave CLAB_MODE empty (or omit it) to hide the ContainerLab page entirely.
| Variable | Default | Description |
|---|---|---|
CLAB_MODE |
(empty — hidden) | Backend mode: local | ssh | rest |
CLAB_BINARY |
clab |
Path to the clab binary (local mode) |
CLAB_TOPO_DIR |
/opt/direttore/topologies |
Directory where topology .yml files are stored |
CLAB_SSH_HOST |
— | Hostname/IP of remote clab host (ssh mode) |
CLAB_SSH_PORT |
22 |
SSH port (ssh mode) |
CLAB_SSH_USER |
root |
SSH username (ssh mode) |
CLAB_SSH_KEY_PATH |
— | Path to SSH private key file (ssh mode) |
CLAB_SSH_PASSWORD |
— | SSH password fallback — prefer key auth (ssh mode) |
CLAB_API_URL |
— | Base URL of the clab-api-server (rest mode), e.g. https://clab-host:8080 |
CLAB_API_TOKEN |
— | Bearer token for clab-api-server auth (rest mode) |
CLAB_API_USERNAME |
— | HTTP Basic username for clab-api-server (rest mode) |
CLAB_API_PASSWORD |
— | HTTP Basic password for clab-api-server (rest mode) |
CLAB_API_VERIFY_SSL |
true |
Verify TLS for clab-api-server (rest mode) |
CLAB_TOPO_GIT_REPO |
— | HTTPS clone URL for topology Git backing (any mode) |
CLAB_TOPO_GIT_BRANCH |
main |
Branch for topology Git repo |
CLAB_TOPO_GIT_AUTH_TOKEN |
— | PAT for topology Git repo HTTPS push |
CLAB_TOPO_GIT_LOCAL_PATH |
/opt/direttore/clab-topologies |
Local clone path for topology Git repo |
| Method | Path | Description |
|---|---|---|
GET |
/api/proxmox/nodes |
List nodes with CPU/RAM/disk stats |
GET |
/api/proxmox/nodes/{node}/networks |
List bridge interfaces on a node |
GET |
/api/proxmox/nodes/{node}/storage |
List storage pools that support VM/CT disks |
GET |
/api/proxmox/nodes/{node}/vms |
List QEMU VMs |
POST |
/api/proxmox/nodes/{node}/vms |
Create a VM (supports multi-NIC, VLAN, storage selection) |
POST |
/api/proxmox/nodes/{node}/vms/{vmid}/{action} |
start / stop / reboot / shutdown / delete |
GET |
/api/proxmox/nodes/{node}/lxc |
List LXC containers |
POST |
/api/proxmox/nodes/{node}/lxc |
Create a container (supports multi-NIC, VLAN, storage selection) |
POST |
/api/proxmox/nodes/{node}/lxc/{vmid}/{action} |
start / stop / reboot / shutdown / delete |
GET |
/api/proxmox/nodes/{node}/templates |
List available ISOs and templates |
GET |
/api/proxmox/tasks/{node}/{upid} |
Poll task status by UPID |
{
"vmid": 1042,
"name": "my-vm",
"cores": 2,
"memory": 2048,
"disk": "32G",
"storage": "local-lvm",
"iso": "local:iso/ubuntu-22.04.4-live-server-amd64.iso",
"username": "labuser",
"password": "changeme",
"ssh_key": "ssh-ed25519 AAAA...",
"nics": [
{ "bridge": "vmbr0", "model": "virtio", "vlan": null },
{ "bridge": "vmbr1", "model": "e1000", "vlan": 100, "ip": "10.0.0.5/24", "gw": "10.0.0.1", "ip6": "2001:db8::5/64", "dns": "1.1.1.1 8.8.8.8" }
]
}{
"vmid": 3001,
"hostname": "my-container",
"cores": 1,
"memory": 512,
"storage": "local-lvm",
"disk_size": 8,
"template": "local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.gz",
"username": "labuser",
"password": "changeme",
"ssh_key": "ssh-ed25519 AAAA...",
"nics": [
{ "name": "eth0", "bridge": "vmbr0", "ip": "dhcp", "ip6": "auto", "vlan": null },
{ "name": "eth1", "bridge": "vmbr1", "ip": "10.10.100.5/24", "gw": "10.10.100.1", "ip6": "2001:db8::100:5/64", "gw6": "2001:db8::100:1", "dns": "1.1.1.1 2606:4700:4700::1111", "vlan": 200 }
],
"unprivileged": true,
"start_after_create": true
}| Method | Path | Description |
|---|---|---|
GET |
/api/reservations/ |
List reservations (filterable by ?start=&end=) |
POST |
/api/reservations/ |
Create reservation (conflict check included) |
GET |
/api/reservations/{id} |
Get a single reservation |
PATCH |
/api/reservations/{id} |
Update a reservation |
DELETE |
/api/reservations/{id} |
Cancel a reservation |
GET |
/api/reservations/export/ical |
iCAL feed for calendar apps |
| Method | Path | Description |
|---|---|---|
GET |
/api/inventory/netbox-status |
Quick reachability check |
GET |
/api/inventory/devices |
Proxy NetBox device list |
GET |
/api/inventory/ip-addresses |
IP addresses with dual-stack and DNS info |
GET |
/api/inventory/prefixes |
IP prefixes with gateway and DNS hints |
GET |
/api/inventory/vlans |
VLANs list |
| Method | Path | Description |
|---|---|---|
GET |
/healthz |
Returns {"status":"ok","mock_mode":<bool>} |
| Method | Path | Description |
|---|---|---|
GET |
/api/hardware/status |
Reachability check for Unimus and Git config repo |
GET |
/api/hardware/debug-unimus |
Raw multi-endpoint probe for Unimus — returns full HTTP status/body for debugging |
GET |
/api/hardware/devices |
List physical devices from NetBox (filterable: ?site=&role=&status=&limit=) |
GET |
/api/hardware/devices/{id} |
Full device detail: NetBox data + interface table, dual-stack mgmt IPs |
POST |
/api/hardware/devices/{id}/backup |
Trigger Unimus backup → poll until done → retrieve config → commit to Git |
GET |
/api/hardware/devices/{id}/configs |
Git log for this device's config file (?limit=30) |
GET |
/api/hardware/devices/{id}/configs/{ref} |
Config file content at a specific git commit SHA |
POST |
/api/hardware/devices/{id}/provision |
Push golden config to device via Unimus Pro |
GET |
/api/hardware/devices/{id}/unimus-link |
Get the manually pinned Unimus address for this device |
PUT |
/api/hardware/devices/{id}/unimus-link |
Set a manual Unimus device link ({"unimus_address": "host.example.com"}) |
DELETE |
/api/hardware/devices/{id}/unimus-link |
Remove a manual Unimus device link |
GET |
/api/hardware/unimus-devices |
List all devices known to Unimus (id, address, vendor, type, lastJobStatus) |
{ "targets": ["unimus", "git"] }targets is optional — defaults to both. Use ["unimus"] to skip Git archiving or ["git"] to archive an existing Unimus backup without re-triggering.
Note
Unimus version compatibility: Direttore detects the correct backup trigger endpoint automatically:
- Unimus 2.8+:
PATCH /api/v2/jobs/backup?deviceIds=<id>(query param, no JSON body) - Older Unimus: tries several
POST /api/v2/jobs/*variants - Fallback: if no trigger endpoint works, archives the latest backup already stored in Unimus
The backup response includes "tried_endpoints" showing which endpoints were attempted.
{
"source": "git",
"git_ref": "a1b2c3d4",
"note": "Rolling back after bad change"
}| Field | Values | Description |
|---|---|---|
source |
"git" | "unimus" |
Source of truth for the golden config |
git_ref |
commit SHA or null |
null = HEAD (latest committed config); ignored when source="unimus" |
note |
string | Human-readable note attached to the Unimus push job |
Warning
Provisioning pushes a config to a live device via Unimus Pro. Always verify the selected config in the Config History tab before provisioning.
| Method | Path | Description |
|---|---|---|
GET |
/api/containerlab/status |
Backend reachability check; returns mode, version, and configured features |
GET |
/api/containerlab/labs |
List all running labs (clab inspect --all) |
GET |
/api/containerlab/labs/{name} |
Inspect a specific lab — returns per-node detail |
POST |
/api/containerlab/labs |
Deploy a topology ({"topo_file": "example.yml", "reconfigure": true}) |
POST |
/api/containerlab/labs/validate |
Validate a topology file without deploying (clab deploy --check) |
POST |
/api/containerlab/labs/deploy-stream |
Deploy and stream output back as SSE (text/event-stream) |
DELETE |
/api/containerlab/labs/{name} |
Destroy a running lab |
POST |
/api/containerlab/labs/{name}/nodes/{node}/action |
Run start, stop, or restart on a single node ({"action": "restart"}) |
WebSocket |
/api/containerlab/labs/{name}/nodes/{node}/console |
In-browser xterm.js terminal session for a node container |
GET |
/api/containerlab/topologies |
List .yml topology files in CLAB_TOPO_DIR |
GET |
/api/containerlab/topologies/{filename} |
Return raw YAML + optional Git history for a topology file |
POST |
/api/containerlab/topologies |
Upload a new topology file (multipart/form-data) |
GET |
/api/containerlab/topologies/{filename}/history |
Git commit history for a topology file (requires CLAB_TOPO_GIT_REPO) |
GET |
/api/containerlab/workspace/{subpath} |
List workspace directory contents |
POST |
/api/containerlab/workspace/folder |
Create a new folder in the workspace |
POST |
/api/containerlab/workspace/file |
Create or overwrite a workspace file |
DELETE |
/api/containerlab/workspace/file |
Delete a workspace file |
POST |
/api/containerlab/workspace/rename |
Rename a file or folder in the workspace |
POST |
/api/containerlab/workspace/duplicate |
Duplicate a file to a new name in the same directory |
Note
All /api/containerlab/* endpoints return HTTP 503 if CLAB_MODE is not set. The sidebar nav item is also hidden client-side until the status endpoint returns 200.
Docker deployments use Traefik instead — see the Docker Compose section. This section covers bare-metal installs where Direttore runs directly under
systemdand nginx provides TLS termination.
Config files live in docs/nginx/:
| File | Purpose |
|---|---|
direttore.conf |
Main server block — HTTP→HTTPS redirect, TLS, upstream proxy |
websocket_map.conf |
map block for WebSocket / Vite HMR — goes in the http {} context |
| Path | Upstream |
|---|---|
/api/* |
FastAPI backend — [::1]:8000 |
/docs, /redoc, /openapi.json |
FastAPI Swagger / ReDoc |
/ (everything else) |
React (Vite dev [::1]:5173, or built dist/ served by nginx) |
Option A — Let's Encrypt via Certbot (recommended for public-facing hosts)
sudo apt install certbot python3-certbot-nginx # Debian/Ubuntu
sudo certbot --nginx -d direttore.example.comCertbot will edit nginx config automatically and schedule auto-renewal. Skip to Step 3 once done.
Option B — Let's Encrypt via standalone (no nginx running yet)
sudo certbot certonly --standalone -d direttore.example.com
# Certificates written to /etc/letsencrypt/live/direttore.example.com/Option C — Bring your own certificate
Place your cert and key at any path and update ssl_certificate / ssl_certificate_key in direttore.conf accordingly.
Auto-renewal (certbot installs a systemd timer automatically; verify with):
sudo systemctl status certbot.timer
sudo certbot renew --dry-run# 1. Install the WebSocket map snippet (http context — required for Vite HMR)
sudo cp docs/nginx/websocket_map.conf /etc/nginx/conf.d/
# 2. Install the site config
sudo cp docs/nginx/direttore.conf /etc/nginx/sites-available/direttore
sudo ln -s /etc/nginx/sites-available/direttore /etc/nginx/sites-enabled/
# 3. Edit the two server_name lines and the ssl_certificate paths
sudo nano /etc/nginx/sites-available/direttore
# 4. Validate and reload
sudo nginx -t && sudo systemctl reload nginxThe default direttore.conf uses [::1]:8000 (IPv6 loopback) for the FastAPI upstream and [::1]:5173 for Vite. This matches the default --host :: in both Dockerfile.api and vite.config.js.
If your systemd services still bind to 0.0.0.0 (IPv4 only), change the upstream server lines in direttore.conf back to 127.0.0.1:8000 / 127.0.0.1:5173, or update the service to use --host ::.
nginx terminates TLS, so the browser reaches the API as https://direttore.example.com. Add that origin to .env:
API_CORS_ORIGINS=https://direttore.example.com
Restart the API after changing .env:
sudo systemctl restart direttore-apiThe direttore.conf upstream block and both listen directives already support dual-stack:
listen 80; # IPv4
listen [::]:80; # IPv6
listen 443 ssl;
listen [::]:443 ssl;The OCSP resolver list includes both IPv4 and IPv6 addresses (1.1.1.1, 8.8.8.8, 2606:4700:4700::1111, 2001:4860:4860::8888) so stapling works on IPv6-only hosts.
For an IPv6-only host, ensure your DNS has an AAAA record for the domain and that the Let's Encrypt CA can reach port 80 over IPv6 during the HTTP-01 challenge.
direttore.conf includes a commented-out plain HTTP server block at the bottom — uncomment it and delete the HTTPS blocks for internal/staging use.
Important
Vite host check — Vite's dev server rejects requests whose Host header doesn't match localhost/127.0.0.1. When nginx proxies from a real hostname, Vite returns an "Invalid Host header" error. vite.config.js already sets allowedHosts: 'all' to prevent this. If you see a blank page, make sure Vite was restarted after any config change.
For bare-metal production deployments, Direttore includes systemd service files to keep the API and Vite server running in the background.
# 1. Copy the unit files into systemd
sudo cp systemd/direttore-api.service /etc/systemd/system/
sudo cp systemd/direttore-frontend.service /etc/systemd/system/
# 2. Reload daemon, enable, and start
sudo systemctl daemon-reload
sudo systemctl enable --now direttore-api
sudo systemctl enable --now direttore-frontend
# 3. Check status
sudo systemctl status direttore-apidocker-compose.yml uses Traefik as the sole externally-exposed service. Traefik automatically obtains and renews a Let's Encrypt certificate for your domain and redirects all HTTP traffic to HTTPS. The api and frontend services are not reachable from outside the host — only Traefik can reach them. Traffic to /api/* is routed directly to the FastAPI backend (WebSocket and SSE work natively); everything else goes to the nginx frontend container.
Prerequisites:
- Port 80 and 443 open on the host firewall
- DNS for
DOMAINresolving to this host before you start the stack (Let's Encrypt performs an HTTP-01 challenge on first boot) - For IPv6 / dual-stack: Docker daemon IPv6 enabled — see docs/daemon-ipv6.md
cp .env.example .env
# Required: set your public hostname and ACME email
# DOMAIN=direttore.example.com
# ACME_EMAIL=admin@example.com
#
# Stack mode (defaults to dual-stack / IPv6-preferred):
# TRAEFIK_HOST_IP=:: # or 0.0.0.0 for IPv4-only
# TRAEFIK_BIND_ADDR=[::] # or 0.0.0.0 for IPv4-only
# ENABLE_IPV6=true # set false for IPv4-only
docker compose up -dOnce up, the app is available at https://your.domain — HTTP requests are redirected to HTTPS automatically.
Certificate storage: Let's Encrypt state is persisted in the
letsencryptDocker volume so certificates survive container restarts and are reused across deploys.
The dev compose file brings up the complete local environment including a Proxmox container, NetBox, PostgreSQL, and Redis:
docker compose -f docker-compose.dev.yml up| Service | Port | Description |
|---|---|---|
api |
8000 | FastAPI backend (hot-reload via volume mount) |
frontend |
5173 | Vite dev server (hot-reload via volume mount) |
proxmox |
8006 | Proxmox VE 8 container (rtedpro/proxmox:8.4.1-arm64) |
netbox |
8080 | NetBox v3.7 (admin/superuserpassword) |
postgres |
— | PostgreSQL 15 for NetBox |
redis |
— | Redis 7 for NetBox |
After the dev stack is up, run the included setup script to configure the Proxmox container with templates, ISOs, and network bridges:
bash api/scripts/init_proxmox_node.shThis script:
- Sets the root password to
root - Updates the LXC appliance template list
- Downloads Ubuntu 22.04 and Debian 12 LXC templates
- Stubs out Rocky 9 and Ubuntu 22.04 ISO files
- Creates
vmbr0andvmbr1network bridges - Regenerates SSL certificates (fixes
pveproxystartup errors)
bash api/scripts/seed_test_resources.shdirettore/
├── api/ # FastAPI backend
│ ├── main.py # App entrypoint, CORS, lifespan
│ ├── config.py # Pydantic-settings from .env
│ ├── db.py # Async SQLAlchemy engine
│ ├── models.py # Reservation, ResourcePool ORM models
│ ├── schemas/ # Pydantic request/response schemas
│ │ ├── proxmox.py
│ │ ├── reservations.py
│ │ └── inventory.py
│ ├── services/ # Business logic / external integrations
│ │ ├── proxmox/
│ │ │ ├── client.py # proxmoxer wrapper + mock data
│ │ │ ├── vms.py # QEMU VM CRUD
│ │ │ ├── containers.py # LXC container CRUD
│ │ │ ├── templates.py # ISO/template listing
│ │ │ ├── network.py # Bridge interface listing
│ │ │ └── storage.py # Storage pool listing
│ │ ├── netbox/ # NetBox API client + IPAM allocation
│ │ ├── unimus/ # Unimus Pro REST API client
│ │ │ └── __init__.py # backup/poll/push, _unwrap normalizer
│ │ ├── git_config/ # Git config archive service
│ │ │ └── __init__.py # clone/pull/read/write/push
│ │ └── containerlab/ # ContainerLab service (local/SSH/REST backends)
│ │ └── __init__.py # deploy/destroy/inspect/topology CRUD
│ ├── routes/ # FastAPI routers
│ │ ├── proxmox.py # /api/proxmox/* routes
│ │ ├── reservations.py # /api/reservations/* routes
│ │ ├── inventory.py # /api/inventory/* routes
│ │ ├── hardware.py # /api/hardware/* routes
│ │ └── containerlab.py # /api/containerlab/* routes
│ └── scripts/ # Dev / ops helper scripts
│ ├── init_proxmox_node.sh # Bootstrap Proxmox Docker container
│ └── seed_test_resources.sh
├── frontend/ # React + Vite SPA
│ ├── src/
│ │ ├── api/ # Axios client + typed API functions
│ │ │ ├── client.js # Axios base instance
│ │ │ ├── hardware.js # Hardware management calls
│ │ │ ├── containerlab.js # ContainerLab API calls
│ │ │ └── ... # Other API modules
│ │ ├── components/
│ │ │ ├── Layout.jsx # Sidebar navigation
│ │ │ └── NetBoxNicPicker.jsx # NetBox IPAM modal
│ │ ├── features/
│ │ │ ├── provisioning/ # Wizard steps, hooks, utils
│ │ │ └── topology/ # Lab Topology canvas
│ │ └── pages/
│ │ ├── Dashboard.jsx # Node cards + resource bars
│ │ ├── Resources.jsx # VM/CT table with actions
│ │ ├── Provision.jsx # Provision wizard wrapper
│ │ ├── Hardware.jsx # Physical device management
│ │ ├── ContainerLab.jsx # ContainerLab topology management
│ │ ├── Lab.jsx # React Flow topology canvas
│ │ └── Reservations.jsx # FullCalendar + booking modal
│ ├── vite.config.js
│ └── Dockerfile.frontend
├── templates/ # Jinja2 network config templates
│ ├── junos/, arista/, panos/, nokia-sros/, mikrotik/
│ └── html/ # Legacy Flask template (superseded)
├── nornir-examples/ # Nornir task examples
├── inventory.py # Nornir + NetBox inventory plugin
├── deploy.py # Git-backed config deployment
├── scheduler.py # Legacy iCAL scheduler (superseded by API)
├── pyproject.toml # Python project metadata + dependencies (uv)
├── uv.lock # Locked dependency tree
├── Dockerfile.api # Backend Docker image
├── docker-compose.yml # Production compose — Traefik + Let's Encrypt
├── docker-compose.dev.yml # Full dev stack (Proxmox + NetBox + Postgres + Redis)
└── .env.example # Environment variable template
- Set
PROXMOX_MOCK=false(or remove the flag) in.env - Fill in
PROXMOX_HOST,PROXMOX_USER,PROXMOX_PASSWORD - If using a self-signed certificate, set
PROXMOX_VERIFY_SSL=false - Ensure the Proxmox user has
VM.Allocate,VM.PowerMgmt,Datastore.Allocateprivileges
# Proxmox — create a dedicated API user (recommended over root)
pveum role add DirettoreRole -privs "VM.Allocate VM.PowerMgmt VM.Console Datastore.AllocateSpace Pool.Allocate"
pveum user add direttore@pve --password <password>
pveum aclmod / -user direttore@pve -role DirettoreRoleThe snmp_to_netbox.sh bash script walks a live network device via SNMP and pushes it into the NetBox model (interfaces, VRFs, VLANs, IPv4, IPv6, Serial Number).
# Set your NetBox URL and token in .env or your environment
# SNMP_COMMUNITY defaults to "public" but can be overridden
# Single device (IPv4, literal IPv6, or DNS hostname)
./snmp_to_netbox.sh -s "New York" -r "Router" -t "ASR1000" gateway.local "Core Router 1"
./snmp_to_netbox.sh fd68:1e02:dc1a:ffff::1 "gw.buragl.io"
# Bulk import from CSV
./snmp_to_netbox.sh -f devices.csv| Platform | Issue | Fix applied |
|---|---|---|
| Junos | sysDescr contains forward slashes (e.g. Junos 23.2R1/amd64), breaking sed 's/...//' |
All sed delimiters changed from / to | |
| Junos | Doesn't respond to IF-MIB::ifDescr (MIB name form) |
Script retries with numeric OID 1.3.6.1.2.1.2.2.1.2 if MIB-name walk returns 0 interfaces |
/bin/sh (dash) |
grep -c output with trailing newline causes [: Illegal number: in arithmetic comparisons |
Counts are stripped with tr -d '[:space:]' and defaulted with ${var:-0} |
In addition to virtual infrastructure, Direttore manages physical networking hardware (Juniper, Cisco, Aruba, MikroTik, FS, IP Infusion, Palo Alto) using a closed-loop automation architecture:
- NetBox: The "Source of Truth" detailing intended state (VLANs, IPs, devices).
- Nornir: The orchestration engine that fetches NetBox data, renders Jinja2 templates via NAPALM/Netmiko, and pushes config.
- Unimus: The auditor that automatically syncs from NetBox, backs up the operational state, and detects configuration drift.
For the detailed, step-by-step implementation plan (including directory structures, Junos examples, and Netmiko platform mapping), please read the Network Automation Implementation Plan.
# Example: Deploy provisioned VLANs to all Active Juniper switches
uv run python nornir_automation/generate_and_push.py| Feature | Status | Effort |
|---|---|---|
| JWT Authentication (local users) | ✅ Done | — |
| Hardware Management — Unimus backup + Git archive | ✅ Done | — |
| Hardware Management — golden-config provisioning | ✅ Done | — |
| Unimus device-address auto-match (4-strategy: IP→rDNS→name→scan) | ✅ Done | — |
| Manual Unimus device link store (DNS-free pairing) | ✅ Done | — |
| Unimus 2.8 backup endpoint auto-detection | ✅ Done | — |
| ContainerLab integration — local, SSH, REST backends | ✅ Done | — |
| ContainerLab topology Git backing + history | ✅ Done | — |
| ContainerLab in-browser node console (xterm.js + WebSocket) | ✅ Done | — |
| Real-time VM console (xterm.js + WebSocket) | Planned | 15 hrs |
| Snapshot management UI | Planned | 5 hrs |
| Prometheus metrics endpoint | Planned | 8 hrs |
| Two-way iCAL sync (CalDAV) | Planned | 8 hrs |
| YANG config validation | Planned | 5 hrs |
| SSH-based direct backup (NAPALM/Netmiko, no Unimus) | Planned | 8 hrs |
| Config diff viewer in Hardware History tab | Planned | 3 hrs |
| ContainerLab — clab-api-server auth in UI | Planned | 2 hrs |
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-change - Commit your changes:
git commit -m 'Add my change' - Push and open a pull request
Note: Set
PROXMOX_MOCK=trueduring development so no Proxmox hardware is required.



