Skip to content

CLI commands

l2trace ships one binary, l2trace, with several subcommands. The Makefile wraps the most common ones behind targets so you don’t usually invoke it directly — but here’s the full surface.

All commands run inside the reconciler container by default:

Terminal window
docker compose run --rm --no-deps reconciler l2trace <subcommand> ...

Apply alembic migrations to head.

Terminal window
docker compose run --rm migrate alembic upgrade head
# or, equivalently:
make migrate

The Make target uses the migrate compose service which is configured for one-shot alembic runs. Direct invocation of l2trace migrate works too but goes through the longer-running reconciler image.

Start the reconciler and the compactor and the collector orchestrator with a shared in-memory LiveSet. Long-running. The default make up starts this as a daemon in the reconciler container.

Terminal window
l2trace reconcile

Co-running is not optional: the compactor must invalidate LiveSet entries when it closes valid_during ranges. Splitting them across two processes would silently corrupt the bitemporal log. See the compactor invariant for why.

The orchestrator picks up rows from the device_collector table on boot and on a ~30s reconfig pass — so anything added/disabled via l2trace device … takes effect without a process restart.

Register and manage per-device collector configurations. The orchestrator inside l2trace reconcile reads this table and (re)spawns gNMI/SNMP/SSH workers to match. Five subcommands:

l2trace device add --hostname HOST --mgmt-ip IP --source SRC \
[--community COMMUNITY | --community-uri URI] [--snmp-port 161] \
[--username USER | --username-uri URI] \
[--password PW | --password-uri URI] \
[--enable-secret PW | --enable-secret-uri URI] [--ssh-port 22] \
[--poll-interval 60] [--vendor VENDOR]
FlagDefaultDescription
--hostname(required)Switch hostname — unique per install
--mgmt-ip(required)Management IP the collector connects to
--sourcesnmpOne of gnmi, snmp, ssh, netconf
--community / --community-uriSNMPv2c community as cleartext, or as a secrets URI (env://VAR, file:///path#/ptr). Required for --source=snmp
--snmp-port161UDP port for SNMP collection
--username / --username-uriSSH username (cleartext or URI). Required for --source=ssh
--password / --password-uriSSH password (cleartext or URI). One of --password* OR --ssh-key-file is required for --source=ssh
--ssh-key-fileAbsolute path to private key file (alternative to password). Typically a Docker secret mount like /run/secrets/ssh/sw-edge-7-key
--ssh-passphrase / --ssh-passphrase-uriPassphrase for an encrypted private key (cleartext or URI)
--enable-secret / --enable-secret-uriOptional privileged-mode password for IOS-XE
--ssh-port22TCP port for SSH collection
--poll-interval60.0Seconds between polls (FDB + LLDP + chassis-id)
--vendorFree-text vendor hint. Required for SSH — picks the napalm driver: cisco-ios-xe / cisco-ios / cisco-nxos

URI-form credential flags are preferred for production. The URI form stores a reference to the secret (env var name, JSON pointer into a mounted file, etc.) instead of the value itself — so pg_dump of the database doesn’t expose credentials. See Manage collector credentials for the full URI scheme catalog. Passing both forms for the same field (e.g. --password and --password-uri) is a user error.

Idempotent. Re-running with the same --hostname updates fields in place; the orchestrator picks up the change on its next reconfig pass. You can register the same device twice with different --source values to get redundant collection (e.g. gNMI primary + SNMP backup, or SNMP primary + SSH fallback).

For SSH specifically — see How to add an SSH-only switch for the operator walkthrough including a working IOS-XE example.

l2trace device list

Renders a Rich table showing every registered device, its enabled sources, last successful poll, and the most recent collector error. Quick health check before opening a screenshot session.

l2trace device disable --hostname HOST --source SRC
l2trace device enable --hostname HOST --source SRC

Toggles the enabled flag on the (device, source) collector row. Keeps history intact — re-enabling is one command. The orchestrator gracefully stops the worker on the next reconfig pass.

l2trace device remove --hostname HOST --yes

DELETEs the device + cascade-deletes its entire observation history. Destructive. The --yes flag is required to skip the confirmation prompt. Most operators want device disable instead — that stops collection but preserves the bitemporal log for forensic queries.

Run a one-shot L2 traceroute from the CLI.

l2trace trace --src AA:BB:CC:11:22:33 --dst AA:BB:CC:44:55:66 --vlan 10 \
[--as-of TIMESTAMP|now] [--audit-at TIMESTAMP] [--max-hops 32]
FlagDefaultDescription
--src(required)Source MAC, any common format
--dst(required)Destination MAC
--vlan(required)VLAN ID, 1..4094
--as-ofnowWire-time anchor: “what was the path at T?”
--audit-atcurrent beliefBelief-time anchor: “what did we believe at T?”
--max-hops32Loop guard — abort after N hops

The Make wrapper is:

Terminal window
make trace SRC=... DST=... VLAN=... [AS_OF=...]

(AUDIT_AT is not in the Make wrapper — pass it via the docker compose invocation directly.)

Audit every open LLDP adjacency for one-way / asymmetric observations. For each A → B row, checks whether B → A exists from the same source and from any source.

Terminal window
l2trace audit-adjacencies [--source SRC] [--show-healthy]
# or
make audit-adjacencies
FlagDefaultEffect
--sourceLimit audit to one telemetry source (gnmi, snmp, …)
--show-healthyoffInclude bidirectionally-confirmed rows in output

Output is a Rich table with one row per open adjacency:

SymbolMeaning
✗ no reverseNo peer observation back from any source — likely one-way cable, LLDP disabled on peer, or port down
△ source asymLink is bidirectional, but the same source only sees one direction (telemetry asymmetry, not a cable problem)
Both directions confirmed by the same source

A green “all open adjacencies are bidirectional” line means nothing to investigate. Empty table with --show-healthy off (the default) is also the all-clear signal. See Audit LLDP adjacencies for the full operator playbook.

Launch the Textual TUI operator console.

Terminal window
l2trace tui
# or
make tui

The TUI runs inside the tui compose service (which has TTY allocated via --service-ports). It needs the postgres and nats services running.

Four modes accessible from the HOME screen:

BindingModeWhat it does
Ctrl+TTRACEL2 traceroute form (src/dst MAC + VLAN)
Ctrl+HHISTORYMAC bitemporal timeline
Ctrl+OOPSLive FDB tree + disagreements + quarantine tail
Ctrl+UAUDITBidirectional LLDP audit (same query as audit-adjacencies)

F1 opens a per-screen help overlay. The ambient as-of timestamp picker at the top of every screen re-fires whichever query is on display, so “what did the fabric look like an hour ago?” is one input change away. See the TUI tour.

Fetch IEEE’s three OUI registries (MA-L / MA-M / MA-S) and UPSERT them into oui_vendor. Safe to run on a live system — UPSERT, no TRUNCATE.

Terminal window
l2trace oui-refresh
# or
make oui-refresh

Pulls ~5 MB across three HTTPS requests to standards-oui.ieee.org. ~52k rows land per refresh. Recommended cadence: weekly.

Run a one-off poll cycle for a single (device, source) outside the orchestrator — useful for debugging a new device config without waiting for the next reconfig pass.

Terminal window
l2trace collect --device-id ID --source gnmi

Most operators want l2trace reconcile (which is what make up starts) — that runs the orchestrator that owns this lifecycle. Use collect only when you need a single foreground cycle whose stderr you can read directly.

Most flags also have an env-var form. The reconciler reads them on start via Pydantic settings. See .env.example for the full list.

Common ones:

VariableDefaultEffect
DATABASE_URL(in compose)asyncpg URL — set by compose to the in-network postgres
NATS_URL(in compose)NATS URL — set by compose to the in-network nats
GNMI_AGING_THRESHOLD_SECONDS300Compactor closes gNMI rows after this much silence
SNMP_POLL_INTERVAL_SECONDS60Compactor uses 2 × this as the SNMP aging threshold
COMPACTOR_INTERVAL_SECONDS30How often the compactor pass runs
TUI_THEME(empty / default)One of hacker, amber, or empty for terminal default