Skip to content

Declare MLAG / vPC peer groups

  • “sw-core-1 and sw-core-2 are vPC peers, but every trace shows duplicate entries through both — that’s not how the real path looks.”
  • “My audit dashboard has 200 ✓ peer-link rows for every real one-way-cable alert. I can’t see the signal.”
  • “We added a third pair to the fabric this morning and the traceroute query just got slower.”

l2trace’s L2 traceroute and adjacency audit are MLAG-aware once you declare the pair. Before declaration: the walker treats peer-link adjacencies as real hops, audits show peer-link rows as ✓ healthy, and the same MAC seen on both peers reads as a constant “MAC move.” After: the trace walks one logical path, the audit hides peer-links by default, and disagreement detection works correctly per-peer.

The mechanism is documented in How MLAG-collapsed traceroute works. This page is the operator workflow.

Terminal window
docker compose run --rm reconciler l2trace mlag create \
--hosts sw-core-1,sw-core-2

Output:

grouped sw-core-1, sw-core-2 into mlag_group_id=1

Effective immediately — the next l2trace trace and the next AUDIT refresh in the TUI use the new grouping. No reconciler restart needed.

Idempotent: re-running with the same pair is a no-op. Adding a third host to an existing pair promotes the third into the existing group:

Terminal window
l2trace mlag create --hosts sw-core-1,sw-core-3
# Output: grouped sw-core-1, sw-core-3 into mlag_group_id=1
# (sw-core-1's existing group ID is reused; sw-core-3 joins it)
Terminal window
docker compose run --rm reconciler l2trace mlag list
MLAG groups
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ group_id ┃ hosts ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 1 │ sw-core-1, sw-core-2 │
│ 2 │ sw-edge-a, sw-edge-b │
└──────────┴──────────────────────────┘

Group IDs are auto-assigned and opaque to operators — you don’t manage them by number, you manage them by hostnames.

Terminal window
docker compose run --rm reconciler l2trace mlag dissolve \
--hosts sw-core-1,sw-core-2

Resets mlag_group_id = NULL on the listed hosts. Use after physical MLAG teardown (replacing the peer-link with a routed uplink, splitting the pair into independent switches, etc).

Silent no-op on hostnames that aren’t in any group — useful when a device remove already cleaned up.

Mixing hosts that already belong to different non-NULL groups is a hard error:

Terminal window
$ l2trace mlag create --hosts sw-core-1,sw-edge-a
hosts already belong to conflicting MLAG groups: [1, 2]. Dissolve first.

This prevents accidentally merging two separate peer-pairs into a single quad-group. If you really do want a quad, dissolve one pair first, then re-create with all four:

Terminal window
l2trace mlag dissolve --hosts sw-edge-a,sw-edge-b
l2trace mlag create --hosts sw-core-1,sw-core-2,sw-edge-a,sw-edge-b

After declaring the MLAG pair (and seeding a fabric via make seed-realistic, which now includes an MLAG-paired core), the trace correctly walks one peer without detouring through the peer-link. Same shape in the TUI’s TRACE screen:

TRACE screen with MLAG-collapsed hop — middle device renders as "sw-core-1 (mlag-1)"

And from the CLI:

$ l2trace trace --src 00:03:93:44:20:82 --dst 00:50:56:78:9b:34 --vlan 10
L2 path 00:03:93:44:20:82 → 00:50:56:78:9b:34 vlan=10
as_of=2026-05-11T22:29:56.135884+00:00
┏━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━┳━━━━━━┓
┃ hop ┃ device ┃ in port ┃ out port ┃ role ┃ via ┃
┡━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━╇━━━━━━┩
│ 0 │ sw-access-1 │ Eth3 │ Eth8 │ trunk │ gnmi │
│ 1 │ sw-core-1 (mlag-1) │ Eth1 │ Eth2 │ trunk │ gnmi │
│ 2 │ sw-access-2 │ Eth8 │ Eth4 │ access │ gnmi │
└─────┴────────────────────┴─────────┴──────────┴────────┴──────┘
termination: reached

The (mlag-1) annotation tells the operator that sw-core-1 is one half of a paired logical node — under failover the same flow would be carried by sw-core-2. Without MLAG declaration, the trace would still have walked through sw-core-1 in this seeded scenario (the peer-link adjacency between sw-core-1 and sw-core-2 doesn’t participate in the CAM-following walk), but the renderer wouldn’t show the suffix and the audit (below) would be polluted with peer-link rows.

Without --include-peer-links (default), peer-link adjacencies are filtered:

$ l2trace audit-adjacencies --show-healthy
adjacency audit
┏━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━┳━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┓
┃ status ┃ local ┃ port ┃ → ┃ peer ┃ peer port ┃ source ┃
┡━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━╇━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━┩
│ ✓ │ sw-access-1 │ Eth8 │ → │ sw-core-1 │ Eth1 │ gnmi │
│ ✓ │ sw-access-2 │ Eth8 │ → │ sw-core-1 │ Eth2 │ gnmi │
│ ✓ │ sw-core-1 │ Eth1 │ → │ sw-access-1 │ Eth8 │ gnmi │
│ ✓ │ sw-core-1 │ Eth2 │ → │ sw-access-2 │ Eth8 │ gnmi │
└────────┴─────────────┴──────┴───┴─────────────┴───────────┴────────┘

Four rows: the two real uplinks audited in both directions.

With --include-peer-links, the peer-link adjacency rows (sw-core-1.Eth-pl ↔ sw-core-2.Eth-pl) come back into view:

$ l2trace audit-adjacencies --show-healthy --include-peer-links
adjacency audit
┏━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┓
┃ status ┃ local ┃ port ┃ → ┃ peer ┃ peer port ┃ source ┃
┡━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━┩
│ ✓ │ sw-access-1 │ Eth8 │ → │ sw-core-1 │ Eth1 │ gnmi │
│ ✓ │ sw-access-2 │ Eth8 │ → │ sw-core-1 │ Eth2 │ gnmi │
│ ✓ │ sw-core-1 │ Eth1 │ → │ sw-access-1 │ Eth8 │ gnmi │
│ ✓ │ sw-core-1 │ Eth2 │ → │ sw-access-2 │ Eth8 │ gnmi │
│ ✓ │ sw-core-1 │ Eth-pl │ → │ sw-core-2 │ Eth-pl │ gnmi │
│ ✓ │ sw-core-2 │ Eth-pl │ → │ sw-core-1 │ Eth-pl │ gnmi │
└────────┴─────────────┴────────┴───┴─────────────┴───────────┴────────┘

Both peer-link rows show as ✓ healthy — they’re each other’s reverse. That’s expected (peer-sync depends on it), which is why the default view hides them: they’d always be noise in the “what’s actually broken?” picture.

Real output from the seeded MLAG group:

$ l2trace mlag list
MLAG groups
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ group_id ┃ hosts ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ 1 │ sw-core-1, sw-core-2 │
└──────────┴──────────────────────┘

Unchanged. Each peer device still tracks its OWN gNMI/SNMP/SSH sources independently — l2trace doesn’t merge their telemetry. So a real disagreement on either peer (gNMI says one port, SNMP says another, on the same device) still flags via the OPS screen’s disagreement pane. MLAG declaration is about topology semantics, not about telemetry semantics.

l2trace mlag create is NOT bitemporal — there’s no “what was the MLAG declaration at 14:42 last Tuesday?” query. The grouping is a single nullable column on device, treated like vendor/model: it reflects the current operational reality.

If your MLAG configuration changes (a pair is torn down, a new pair is added), the change takes effect immediately for new traces. The bitemporal log of adjacency and mac_observation is unaffected — those keep their historical truth; only the trace’s interpretation of that history changes.