diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..b83ce21
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,78 @@
+# CLAUDE.md
+
+## Project context
+
+Arista **EVPN/VXLAN** network lab built on Containerlab + cEOS, simulating three
+interconnected zones:
+
+- **DC** (Data Center): 2 spines, 8 leafs (4 MLAG/VTEP pairs), 2 MLAG border leafs, 4 access switches, 4 hosts
+- **Core**: 2 L3 transit routers (iBGP AS 65500, OSPF underlay)
+- **Campus**: 2 spines, 4 leafs (2 MLAG/VTEP pairs), 2 MLAG border leafs, 2 access switches, 2 hosts
+
+A `gold` VRF is stretched end-to-end between DC and Campus through the Core (EVPN
+Type-5, stitched over eBGP IPv4 on the Core).
+
+Source of truth repo: `https://gitea.arnodo.fr/Damien/arista-evpn-vxlan-clab`
+
+## Naming convention
+
+- Hostname format: `-` where `area` ∈ `{dc, campus, core}`
+- Examples: `dc-leaf1`, `campus-border-leaf2`, `core1`
+- This convention is used as-is for IPFabric site separation
+ (regex: `^(dc|campus|core)-?.*$`)
+
+## Repository structure
+
+- `README.md` — overview, IP addressing, VNI mapping, control plane, quickstart
+- `TROUBLESHOOTING.md` — diagnostic procedures
+- `END_TO_END_TESTING.md` — end-to-end test scenarios
+- `evpn-lab.clab.yml` — Containerlab topology definition
+- `configs/` — Arista EOS configs per node (`.cfg`)
+- `hosts/` — network configuration for simulated Linux hosts
+- `assets/` — diagrams (drawio/svg)
+
+## Tech stack
+
+- **Containerlab** for topology orchestration
+- **cEOS** (containerized Arista EOS) as the network OS
+- **IPFabric** for network assurance and site separation
+- **Gitea** (self-hosted, `gitea.arnodo.fr`) as the source of truth
+
+## GitOps workflow — mandatory rule
+
+**Every action on this project must be tracked by a Gitea issue.**
+
+- Before starting any work (config change, topology addition, documentation, etc.),
+ confirm an **issue exists** and reference it in commits (`Refs #` or `Closes #`).
+- If no issue covers the identified need: **create the issue first**, describe the
+ problem/requirement, and only then act on it.
+- No TODO should ever be left "floating" in code, commits, or discussions: it's
+ either already an issue, or it must become one immediately.
+- This `CLAUDE.md` file stays **general**: it must never contain one-off tasks.
+ Action details live exclusively in issues.
+
+## Style guidelines
+
+- Be concise and direct — no filler, no unnecessary preamble
+- Issues: short title, straight to the point description (context, expected outcome, no fluff)
+- Commit messages: short, factual, imperative mood
+- Documentation (README, TROUBLESHOOTING, etc.): technical and dense — tables over
+ prose, no marketing tone, no redundant explanations
+- Avoid restating what's already documented elsewhere — link/reference instead of duplicating
+
+## Contribution conventions
+
+- Atomic commits, clear message, reference to the associated issue
+- Any Arista config change must stay consistent with the IP addressing / VNI / RD
+ tables documented in `README.md`
+- Any topology change (`evpn-lab.clab.yml`) must be reflected in `assets/` (diagram)
+ and in the `README.md` tables
+- New validation procedures go in `END_TO_END_TESTING.md`; known issues go in
+ `TROUBLESHOOTING.md`
+
+## Default behavior for Claude
+
+1. Before any action, identify the corresponding Gitea issue (or create one first)
+2. Stay within the scope of the issue being worked on — don't drift to other topics
+3. Strictly follow the `-` naming convention
+4. Never introduce hardcoded TODO/FIXME in repo files — always go through an issue
diff --git a/README.md b/README.md
index 2e0239b..c9ae1ad 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,11 @@ An extended Arista BGP EVPN-VXLAN multi-fabric lab using ContainerLab and cEOS.
## 🎯 Overview
-| Zone | Devices |
-| ------ | --------------------------------------------------------------------------------------- |
-| DC | 2 spines, 8 leafs (4 MLAG VTEPs), 2 border leafs (MLAG), 4 access switches, 4 hosts |
-| Core | 2 core routers (iBGP AS 65500, OSPF underlay with BLs, eBGP to DC & Campus BLs) |
-| Campus | 2 spines, 4 leafs (2 MLAG VTEPs), 2 border leafs (MLAG), 2 access switches, 2 hosts |
+| Zone | Devices |
+| ------ | ----------------------------------------------------------------------------------- |
+| DC | 2 spines, 8 leafs (4 MLAG VTEPs), 2 border leafs (MLAG), 4 access switches, 4 hosts |
+| Core | 2 core routers (iBGP AS 65500, OSPF underlay with BLs, eBGP to DC & Campus BLs) |
+| Campus | 2 spines, 4 leafs (2 MLAG VTEPs), 2 border leafs (MLAG), 2 access switches, 2 hosts |
Key design choices:
@@ -16,12 +16,12 @@ Key design choices:
- **OSPF area 0 + eBGP multi-hop** between each Border Leaf pair and both Core routers (over dot1q subinterfaces: `.100` = default VRF underlay, `.200` = VRF `gold`).
- **MLAG** everywhere there is dual-homing at the fabric layers (leaf pairs, border-leaf pairs, access → leafs, and DC host → access).
- **Host attachment pattern**:
- - **DC hosts** (servers) are **dual-homed via LACP** to an access switch — typical DC
- server redundancy.
- - **Campus hosts** (user endpoints: PC, phone, printer) are **single-attached** to a
- Campus access switch via one plain Ethernet link. Redundancy lives at the access-switch
- layer (the access switch itself is dual-homed via LACP to its leaf MLAG pair), not at
- the host.
+ - **DC hosts** (servers) are **dual-homed via LACP** to an access switch — typical DC
+ server redundancy.
+ - **Campus hosts** (user endpoints: PC, phone, printer) are **single-attached** to a
+ Campus access switch via one plain Ethernet link. Redundancy lives at the access-switch
+ layer (the access switch itself is dual-homed via LACP to its leaf MLAG pair), not at
+ the host.
- **VRF `gold`** is stretched end-to-end: DC leafs (VLAN 34 / 78) ↔ DC-BL ↔ Core ↔ Campus-BL ↔ Campus leafs (VLAN 60 / 70), all sharing L3 VNI `100001`.
- **VLAN 50** remains defined as a campus-local L2 VXLAN stretched between the two Campus VTEPs (infrastructure-only, not wired to any host in the current topology).
- **Convention**: L2 VNI = `110000 + vlan_id`, L3 VNI = `100001` for VRF `gold`, RT `1:100001` in both fabrics.
@@ -64,50 +64,50 @@ docker exec -it clab-arista-evpn-fabric-dc-border-leaf1 Cli
### Node Inventory
-| Zone | Role | Nodes | AS |
-| ------ | ----------------------- | ------------------------------------------------------ | ------ |
-| DC | Spine | `dc-spine1`, `dc-spine2` | 65000 |
-| DC | Leaf VTEP1 (MLAG) | `dc-leaf1`, `dc-leaf2` | 65001 |
-| DC | Leaf VTEP2 (MLAG) | `dc-leaf3`, `dc-leaf4` | 65002 |
-| DC | Leaf VTEP3 (MLAG) | `dc-leaf5`, `dc-leaf6` | 65003 |
-| DC | Leaf VTEP4 (MLAG) | `dc-leaf7`, `dc-leaf8` | 65004 |
-| DC | Border Leaf (MLAG) | `dc-border-leaf1`, `dc-border-leaf2` | 65005 |
-| DC | Access (L2-only) | `dc-access1`-`dc-access4` | — |
-| DC | Host | `dc-server1`-`dc-server4` | — |
-| Core | Core router | `core1`, `core2` | 65500 |
-| Campus | Spine | `campus-spine1`, `campus-spine2` | 66000 |
-| Campus | Leaf VTEP1 (MLAG) | `campus-leaf1`, `campus-leaf2` | 66001 |
-| Campus | Leaf VTEP2 (MLAG) | `campus-leaf3`, `campus-leaf4` | 66002 |
-| Campus | Border Leaf (MLAG) | `campus-border-leaf1`, `campus-border-leaf2` | 66005 |
-| Campus | Access (L2-only) | `campus-access1`, `campus-access2` | — |
-| Campus | Host | `campus-host1`, `campus-host2` | — |
+| Zone | Role | Nodes | AS |
+| ------ | ------------------ | -------------------------------------------- | ----- |
+| DC | Spine | `dc-spine1`, `dc-spine2` | 65000 |
+| DC | Leaf VTEP1 (MLAG) | `dc-leaf1`, `dc-leaf2` | 65001 |
+| DC | Leaf VTEP2 (MLAG) | `dc-leaf3`, `dc-leaf4` | 65002 |
+| DC | Leaf VTEP3 (MLAG) | `dc-leaf5`, `dc-leaf6` | 65003 |
+| DC | Leaf VTEP4 (MLAG) | `dc-leaf7`, `dc-leaf8` | 65004 |
+| DC | Border Leaf (MLAG) | `dc-border-leaf1`, `dc-border-leaf2` | 65005 |
+| DC | Access (L2-only) | `dc-access1`-`dc-access4` | — |
+| DC | Host | `dc-server1`-`dc-server4` | — |
+| Core | Core router | `core1`, `core2` | 65500 |
+| Campus | Spine | `campus-spine1`, `campus-spine2` | 66000 |
+| Campus | Leaf VTEP1 (MLAG) | `campus-leaf1`, `campus-leaf2` | 66001 |
+| Campus | Leaf VTEP2 (MLAG) | `campus-leaf3`, `campus-leaf4` | 66002 |
+| Campus | Border Leaf (MLAG) | `campus-border-leaf1`, `campus-border-leaf2` | 66005 |
+| Campus | Access (L2-only) | `campus-access1`, `campus-access2` | — |
+| Campus | Host | `campus-host1`, `campus-host2` | — |
### AS Numbering
-| AS | Role |
-| ----- | ---------------------------------- |
-| 65000 | DC Spine |
-| 65001 | DC VTEP1 (dc-leaf1/2) |
-| 65002 | DC VTEP2 (dc-leaf3/4) |
-| 65003 | DC VTEP3 (dc-leaf5/6) |
-| 65004 | DC VTEP4 (dc-leaf7/8) |
-| 65005 | DC Border Leaf pair |
-| 65500 | Core (iBGP between core1 & core2) |
-| 66000 | Campus Spine |
-| 66001 | Campus VTEP1 (campus-leaf1/2) |
-| 66002 | Campus VTEP2 (campus-leaf3/4) |
-| 66005 | Campus Border Leaf pair |
+| AS | Role |
+| ----- | --------------------------------- |
+| 65000 | DC Spine |
+| 65001 | DC VTEP1 (dc-leaf1/2) |
+| 65002 | DC VTEP2 (dc-leaf3/4) |
+| 65003 | DC VTEP3 (dc-leaf5/6) |
+| 65004 | DC VTEP4 (dc-leaf7/8) |
+| 65005 | DC Border Leaf pair |
+| 65500 | Core (iBGP between core1 & core2) |
+| 66000 | Campus Spine |
+| 66001 | Campus VTEP1 (campus-leaf1/2) |
+| 66002 | Campus VTEP2 (campus-leaf3/4) |
+| 66005 | Campus Border Leaf pair |
### Access Switches
-| Access Switch | Uplink Pair | VLANs | Host | Host attachment |
-| --------------- | ------------------------ | -------- | -------------- | ------------------------- |
-| dc-access1 | dc-leaf1/2 (VTEP1) | 40 | dc-server1 | LACP Po1 (dual-homed) |
-| dc-access2 | dc-leaf3/4 (VTEP2) | 34 | dc-server2 | LACP Po1 (dual-homed) |
-| dc-access3 | dc-leaf5/6 (VTEP3) | 40 | dc-server3 | LACP Po1 (dual-homed) |
-| dc-access4 | dc-leaf7/8 (VTEP4) | 78 | dc-server4 | LACP Po1 (dual-homed) |
-| campus-access1 | campus-leaf1/2 (VTEP1) | 60 | campus-host1 | access port (single link) |
-| campus-access2 | campus-leaf3/4 (VTEP2) | 70 | campus-host2 | access port (single link) |
+| Access Switch | Uplink Pair | VLANs | Host | Host attachment |
+| -------------- | ---------------------- | ----- | ------------ | ------------------------- |
+| dc-access1 | dc-leaf1/2 (VTEP1) | 40 | dc-server1 | LACP Po1 (dual-homed) |
+| dc-access2 | dc-leaf3/4 (VTEP2) | 34 | dc-server2 | LACP Po1 (dual-homed) |
+| dc-access3 | dc-leaf5/6 (VTEP3) | 40 | dc-server3 | LACP Po1 (dual-homed) |
+| dc-access4 | dc-leaf7/8 (VTEP4) | 78 | dc-server4 | LACP Po1 (dual-homed) |
+| campus-access1 | campus-leaf1/2 (VTEP1) | 60 | campus-host1 | access port (single link) |
+| campus-access2 | campus-leaf3/4 (VTEP2) | 70 | campus-host2 | access port (single link) |
All access switches are L2-only, LACP-bonded to their leaf MLAG pair via `Port-Channel10`. MSTP + edge-port BPDU guard.
@@ -122,71 +122,73 @@ Host-facing ports:
### Management (`172.16.0.0/24`)
-| Node | IP | Node | IP |
-| ------------------------- | --------------- | ------------------------- | --------------- |
-| dc-spine1 | 172.16.0.1 | campus-spine1 | 172.16.0.20 |
-| dc-spine2 | 172.16.0.2 | campus-spine2 | 172.16.0.21 |
-| dc-border-leaf1 | 172.16.0.3 | campus-border-leaf1 | 172.16.0.22 |
-| dc-border-leaf2 | 172.16.0.4 | campus-border-leaf2 | 172.16.0.23 |
-| core1 | 172.16.0.10 | campus-leaf1-4 | 172.16.0.51-54 |
-| core2 | 172.16.0.11 | campus-access1 | 172.16.0.61 |
-| dc-leaf1 | 172.16.0.25 | campus-access2 | 172.16.0.62 |
-| dc-leaf2 | 172.16.0.50 | dc-server1-4 | 172.16.0.101-104|
-| dc-leaf3-8 | 172.16.0.27-32 | campus-host1 | 172.16.0.105 |
-| dc-access1-4 | 172.16.0.41-44 | campus-host2 | 172.16.0.106 |
+| Node | IP | Node | IP |
+| --------------- | -------------- | ------------------- | ---------------- |
+| dc-spine1 | 172.16.0.1 | campus-spine1 | 172.16.0.20 |
+| dc-spine2 | 172.16.0.2 | campus-spine2 | 172.16.0.21 |
+| dc-border-leaf1 | 172.16.0.3 | campus-border-leaf1 | 172.16.0.22 |
+| dc-border-leaf2 | 172.16.0.4 | campus-border-leaf2 | 172.16.0.23 |
+| core1 | 172.16.0.10 | campus-leaf1-4 | 172.16.0.51-54 |
+| core2 | 172.16.0.11 | campus-access1 | 172.16.0.61 |
+| dc-leaf1 | 172.16.0.25 | campus-access2 | 172.16.0.62 |
+| dc-leaf2 | 172.16.0.50 | dc-server1-4 | 172.16.0.101-104 |
+| dc-leaf3-8 | 172.16.0.27-32 | campus-host1 | 172.16.0.105 |
+| dc-access1-4 | 172.16.0.41-44 | campus-host2 | 172.16.0.106 |
+| | | gnmic | 172.16.0.70 |
+| | | prometheus | 172.16.0.71 |
Gateway: `172.16.0.254`.
### Router-ID Loopback0 (`Lo0`)
-| Zone | Range | Nodes |
-| ------ | ------------------- | --------------------------------------------------------------------- |
-| DC | `10.0.250.0/24` | dc-spine1 .1, dc-spine2 .2, dc-leaf1-8 .11-.18, BL-dc1 .21, BL-dc2 .22 |
-| Core | `10.0.200.0/24` | core1 `10.0.200.1`, core2 `10.0.200.2` |
-| Campus | `10.1.250.0/24` | campus-spine1 .1, campus-spine2 .2, campus-leaf1-4 .11-.14, BL-campus1 .21, BL-campus2 .22 |
+| Zone | Range | Nodes |
+| ------ | --------------- | ------------------------------------------------------------------------------------------ |
+| DC | `10.0.250.0/24` | dc-spine1 .1, dc-spine2 .2, dc-leaf1-8 .11-.18, BL-dc1 .21, BL-dc2 .22 |
+| Core | `10.0.200.0/24` | core1 `10.0.200.1`, core2 `10.0.200.2` |
+| Campus | `10.1.250.0/24` | campus-spine1 .1, campus-spine2 .2, campus-leaf1-4 .11-.14, BL-campus1 .21, BL-campus2 .22 |
### VTEP Loopback1 (`Lo1`) — shared per MLAG pair
-| Fabric | VTEP | Address | Leafs |
-| ------ | ------ | --------------- | ---------------------- |
-| DC | VTEP1 | `10.0.255.11` | dc-leaf1, dc-leaf2 |
-| DC | VTEP2 | `10.0.255.12` | dc-leaf3, dc-leaf4 |
-| DC | VTEP3 | `10.0.255.13` | dc-leaf5, dc-leaf6 |
-| DC | VTEP4 | `10.0.255.14` | dc-leaf7, dc-leaf8 |
-| DC | BL | `10.0.255.15` | dc-border-leaf1/2 |
-| Campus | VTEP1 | `10.1.255.11` | campus-leaf1/2 |
-| Campus | VTEP2 | `10.1.255.12` | campus-leaf3/4 |
-| Campus | BL | `10.1.255.21` | campus-border-leaf1/2 |
+| Fabric | VTEP | Address | Leafs |
+| ------ | ----- | ------------- | --------------------- |
+| DC | VTEP1 | `10.0.255.11` | dc-leaf1, dc-leaf2 |
+| DC | VTEP2 | `10.0.255.12` | dc-leaf3, dc-leaf4 |
+| DC | VTEP3 | `10.0.255.13` | dc-leaf5, dc-leaf6 |
+| DC | VTEP4 | `10.0.255.14` | dc-leaf7, dc-leaf8 |
+| DC | BL | `10.0.255.15` | dc-border-leaf1/2 |
+| Campus | VTEP1 | `10.1.255.11` | campus-leaf1/2 |
+| Campus | VTEP2 | `10.1.255.12` | campus-leaf3/4 |
+| Campus | BL | `10.1.255.21` | campus-border-leaf1/2 |
### Underlay P2P (`/31`)
-| Segment | Subnets |
-| -------------------------------- | --------------------------------------- |
-| DC dc-spine1 ↔ leaf/BL | `10.0.1.0/31` … `10.0.1.18/31` |
-| DC dc-spine2 ↔ leaf/BL | `10.0.2.0/31` … `10.0.2.18/31` |
-| DC MLAG iBGP SVIs (per pair) | `10.0.3.0/31`, `.2/31`, `.4/31`, `.6/31`, `.8/31` (BL) |
-| DC MLAG peer-link SVIs | `10.0.199.240/31` … `10.0.199.246/31` |
-| DC-BL ↔ Core (default, `.100`) | `10.0.4.0/31` .. `10.0.4.6/31` |
-| DC-BL ↔ Core (VRF gold, `.200`) | `10.0.14.0/31` .. `10.0.14.6/31` |
-| Campus-BL ↔ Core (default) | `10.0.5.0/31` .. `10.0.5.6/31` |
-| Campus-BL ↔ Core (VRF gold) | `10.0.15.0/31` .. `10.0.15.6/31` |
-| Core1 ↔ Core2 (default) | `10.0.200.128/31` |
-| Core1 ↔ Core2 (VRF gold) | `10.0.200.130/31` |
-| Campus dc-spine1 ↔ leaf/BL | `10.1.1.0/31` … `10.1.1.10/31` |
-| Campus dc-spine2 ↔ leaf/BL | `10.1.2.0/31` … `10.1.2.10/31` |
-| Campus MLAG iBGP SVIs | `10.1.3.0/31`, `.2/31`, `.4/31` |
-| Campus MLAG peer-link SVIs | `10.1.199.250/31` … `10.1.199.254/31` |
+| Segment | Subnets |
+| ------------------------------- | ------------------------------------------------------ |
+| DC dc-spine1 ↔ leaf/BL | `10.0.1.0/31` … `10.0.1.18/31` |
+| DC dc-spine2 ↔ leaf/BL | `10.0.2.0/31` … `10.0.2.18/31` |
+| DC MLAG iBGP SVIs (per pair) | `10.0.3.0/31`, `.2/31`, `.4/31`, `.6/31`, `.8/31` (BL) |
+| DC MLAG peer-link SVIs | `10.0.199.240/31` … `10.0.199.246/31` |
+| DC-BL ↔ Core (default, `.100`) | `10.0.4.0/31` .. `10.0.4.6/31` |
+| DC-BL ↔ Core (VRF gold, `.200`) | `10.0.14.0/31` .. `10.0.14.6/31` |
+| Campus-BL ↔ Core (default) | `10.0.5.0/31` .. `10.0.5.6/31` |
+| Campus-BL ↔ Core (VRF gold) | `10.0.15.0/31` .. `10.0.15.6/31` |
+| Core1 ↔ Core2 (default) | `10.0.200.128/31` |
+| Core1 ↔ Core2 (VRF gold) | `10.0.200.130/31` |
+| Campus dc-spine1 ↔ leaf/BL | `10.1.1.0/31` … `10.1.1.10/31` |
+| Campus dc-spine2 ↔ leaf/BL | `10.1.2.0/31` … `10.1.2.10/31` |
+| Campus MLAG iBGP SVIs | `10.1.3.0/31`, `.2/31`, `.4/31` |
+| Campus MLAG peer-link SVIs | `10.1.199.250/31` … `10.1.199.254/31` |
### Host Addressing
-| Host | VLAN | VRF | IP / Mask | Gateway | Purpose |
-| ------------- | ---- | -------- | ----------------- | ------------ | ------------------------------ |
-| dc-server1 | 40 | default | 10.40.40.101/24 | — | DC L2 stretched (VTEP1↔VTEP3) |
-| dc-server2 | 34 | gold | 10.34.34.102/24 | 10.34.34.1 | DC L3 VRF gold |
-| dc-server3 | 40 | default | 10.40.40.103/24 | — | DC L2 stretched |
-| dc-server4 | 78 | gold | 10.78.78.104/24 | 10.78.78.1 | DC L3 VRF gold |
-| campus-host1 | 60 | gold | 10.60.60.101/24 | 10.60.60.1 | Campus L3 VRF gold |
-| campus-host2 | 70 | gold | 10.60.70.102/24 | 10.60.70.1 | Campus L3 VRF gold |
+| Host | VLAN | VRF | IP / Mask | Gateway | Purpose |
+| ------------ | ---- | ------- | --------------- | ---------- | ----------------------------- |
+| dc-server1 | 40 | default | 10.40.40.101/24 | — | DC L2 stretched (VTEP1↔VTEP3) |
+| dc-server2 | 34 | gold | 10.34.34.102/24 | 10.34.34.1 | DC L3 VRF gold |
+| dc-server3 | 40 | default | 10.40.40.103/24 | — | DC L2 stretched |
+| dc-server4 | 78 | gold | 10.78.78.104/24 | 10.78.78.1 | DC L3 VRF gold |
+| campus-host1 | 60 | gold | 10.60.60.101/24 | 10.60.60.1 | Campus L3 VRF gold |
+| campus-host2 | 70 | gold | 10.60.70.102/24 | 10.60.70.1 | Campus L3 VRF gold |
> DC hosts are dual-homed in LACP over `bond0` with tagged VLAN sub-interfaces.
> Campus hosts are single-attached with one untagged `eth1` in a single access VLAN.
@@ -195,20 +197,20 @@ Gateway: `172.16.0.254`.
### L2 VNI Mapping
-| VLAN | Description | VNI | Scope | RT |
-| ---- | ------------------------------ | ------ | ------------------------------------------------------ | ---------- |
-| 40 | DC L2 VXLAN (stretched) | 110040 | DC VTEP1 (dc-leaf1/2) + VTEP3 (dc-leaf5/6) | 40:110040 |
-| 50 | Campus L2 VXLAN (stretched) | 110050 | Campus VTEP1 (campus-leaf1/2) + VTEP2 (campus-leaf3/4) | 50:110050 |
-| 34 | DC VRF gold subnet (local) | 110034 | DC VTEP2 only (anycast GW 10.34.34.1) | 34:110034 |
-| 78 | DC VRF gold subnet (local) | 110078 | DC VTEP4 only (anycast GW 10.78.78.1) | 78:110078 |
-| 60 | Campus VRF gold subnet (local) | 110060 | Campus VTEP1 only (anycast GW 10.60.60.1) | 60:110060 |
-| 70 | Campus VRF gold subnet (local) | 110070 | Campus VTEP2 only (anycast GW 10.60.70.1) | 70:110070 |
+| VLAN | Description | VNI | Scope | RT |
+| ---- | ------------------------------ | ------ | ------------------------------------------------------ | --------- |
+| 40 | DC L2 VXLAN (stretched) | 110040 | DC VTEP1 (dc-leaf1/2) + VTEP3 (dc-leaf5/6) | 40:110040 |
+| 50 | Campus L2 VXLAN (stretched) | 110050 | Campus VTEP1 (campus-leaf1/2) + VTEP2 (campus-leaf3/4) | 50:110050 |
+| 34 | DC VRF gold subnet (local) | 110034 | DC VTEP2 only (anycast GW 10.34.34.1) | 34:110034 |
+| 78 | DC VRF gold subnet (local) | 110078 | DC VTEP4 only (anycast GW 10.78.78.1) | 78:110078 |
+| 60 | Campus VRF gold subnet (local) | 110060 | Campus VTEP1 only (anycast GW 10.60.60.1) | 60:110060 |
+| 70 | Campus VRF gold subnet (local) | 110070 | Campus VTEP2 only (anycast GW 10.60.70.1) | 70:110070 |
### L3 VNI Mapping (end-to-end)
-| VRF | L3 VNI | RT | Scope |
-| ---- | ------- | ---------- | ----------------------------------------------------- |
-| gold | 100001 | 1:100001 | DC VTEP2/VTEP4/DC-BL + Campus VTEP1/VTEP2/Campus-BL |
+| VRF | L3 VNI | RT | Scope |
+| ---- | ------ | -------- | --------------------------------------------------- |
+| gold | 100001 | 1:100001 | DC VTEP2/VTEP4/DC-BL + Campus VTEP1/VTEP2/Campus-BL |
VRF `gold` is announced over EVPN Type-5 (IP prefix) inside each fabric, and **stitched by the Core** via eBGP IPv4 unicast in VRF gold (over the `.200` dot1q subinterfaces). L3 VNI `100001` is re-used end-to-end for symmetry; RT `1:100001` is consistent across both fabrics.
@@ -219,19 +221,19 @@ VRF `gold` is announced over EVPN Type-5 (IP prefix) inside each fabric, and **s
## 🔀 Control Plane Summary
-| Segment | Protocol | Notes |
-| ----------------------------------- | ------------------------------------ | ------------------------------------- |
-| DC spine ↔ leaf/BL underlay | eBGP IPv4 (AS 65000 ↔ 650xx) | `maximum-paths 4 ecmp 64` |
-| DC spine ↔ leaf/BL overlay | eBGP EVPN via Loopback0, multi-hop 3 | Spines reflect via `ebgp peer-group` |
-| DC MLAG pair iBGP | iBGP over VLAN 4091 SVI | `next-hop-self` |
-| DC-BL ↔ Core (default) | OSPF area 0 + eBGP AS 65005 ↔ 65500 | on `.100` dot1q subinterface |
-| DC-BL ↔ Core (VRF gold) | eBGP AS 65005 ↔ 65500 | on `.200` dot1q subinterface |
-| Core1 ↔ Core2 (default) | OSPF area 0 + iBGP AS 65500 | via Loopback0 |
-| Core1 ↔ Core2 (VRF gold) | iBGP AS 65500 | VRF-aware over `.200` subinterface |
-| Campus-BL ↔ Core (default / gold) | OSPF + eBGP AS 66005 ↔ 65500 | same pattern as DC-BL |
-| Campus spine ↔ leaf/BL underlay | eBGP IPv4 (AS 66000 ↔ 660xx) | |
-| Campus spine ↔ leaf/BL overlay | eBGP EVPN via Loopback0, multi-hop 3 | |
-| Campus MLAG pair iBGP | iBGP over VLAN 4091 SVI | |
+| Segment | Protocol | Notes |
+| --------------------------------- | ------------------------------------ | ------------------------------------ |
+| DC spine ↔ leaf/BL underlay | eBGP IPv4 (AS 65000 ↔ 650xx) | `maximum-paths 4 ecmp 64` |
+| DC spine ↔ leaf/BL overlay | eBGP EVPN via Loopback0, multi-hop 3 | Spines reflect via `ebgp peer-group` |
+| DC MLAG pair iBGP | iBGP over VLAN 4091 SVI | `next-hop-self` |
+| DC-BL ↔ Core (default) | OSPF area 0 + eBGP AS 65005 ↔ 65500 | on `.100` dot1q subinterface |
+| DC-BL ↔ Core (VRF gold) | eBGP AS 65005 ↔ 65500 | on `.200` dot1q subinterface |
+| Core1 ↔ Core2 (default) | OSPF area 0 + iBGP AS 65500 | via Loopback0 |
+| Core1 ↔ Core2 (VRF gold) | iBGP AS 65500 | VRF-aware over `.200` subinterface |
+| Campus-BL ↔ Core (default / gold) | OSPF + eBGP AS 66005 ↔ 65500 | same pattern as DC-BL |
+| Campus spine ↔ leaf/BL underlay | eBGP IPv4 (AS 66000 ↔ 660xx) | |
+| Campus spine ↔ leaf/BL overlay | eBGP EVPN via Loopback0, multi-hop 3 | |
+| Campus MLAG pair iBGP | iBGP over VLAN 4091 SVI | |
## 🧪 Testing & Validation
@@ -323,6 +325,74 @@ ssh admin@clab-arista-evpn-fabric-dc-border-leaf1 "show bgp evpn route-type ip-p
ssh admin@clab-arista-evpn-fabric-campus-border-leaf1 "show bgp evpn route-type ip-prefix ipv4"
```
+## 📡 Telemetry
+
+A [`gnmic`](https://gnmic.openconfig.net/) container node subscribes to gNMI (port `6030`,
+`eos-native` provider) on every Arista `cEOS` node in the fabric, and exposes the
+collected metrics on a Prometheus exporter endpoint.
+
+Subscriptions (see issue #44 for the path-selection rationale):
+
+| Subscription | Nodes | Paths |
+| -------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------- |
+| `eos-interfaces` | all | `interfaces/interface/state/{oper-status,counters}` |
+| `eos-bgp` | all | `.../bgp/neighbors/neighbor/state/session-state`, `.../afi-safis/afi-safi/state/prefixes` (IPv4/IPv6 unicast + L2VPN EVPN) |
+| `eos-system` | all | `/system/cpus/cpu/state`, `/system/memory/state` |
+| `eos-vxlan` | VTEP nodes only (leafs + border-leafs, both fabrics) | VLAN-to-VNI mapping + MAC table entries — joined in Prometheus to get MAC count per VNI |
+
+- Config: `configs/gnmic/gnmic-config.yml`
+- TLS: `insecure: true` — cEOS gNMI runs in plaintext here (no SSL profile configured on
+ `management api gnmi`), not TLS; `skip-verify` would fail the handshake
+- `oper-status` and BGP `session-state` are string enums; gnmic's Prometheus output drops
+ non-numeric leaves, so the `state-to-int` event-processor maps them to `1`/`0`
+- Prometheus exporter: `http://clab-arista-evpn-fabric-gnmic:9273/metrics`
+
+A `prometheus` container node (`prom/prometheus`, `172.16.0.71:9090`) scrapes the
+gnmic exporter every `5s` and is deployed inside the topology (`clab deploy`/`clab
+destroy`, self-contained — see issue #45). Metric names are kept as-is; raw
+OpenConfig-flattened labels are relabeled to clean, joinable names:
+
+| Raw label | Clean label |
+| -------------------------------- | ------------------ |
+| `source` | `device` |
+| `interface_name` | `interface` |
+| `neighbor_neighbor_address` | `neighbor_address` |
+| `vlan_to_vni_vlan` / `entry_vlan` | `vlan` |
+| `entry_mac_address` | `mac_address` |
+| `afi_safi_afi_safi_name` | `afi_safi` |
+
+`vni` is **not** available as a native label on any gnmic-exported metric — it
+only appears as the sample *value* of
+`interfaces_interface_arista_vxlan_vlan_to_vnis_vlan_to_vni_state_vni`, keyed by
+`(device, interface, vlan)`. Per-VNI MAC counts require a PromQL join on `vlan`
+rather than a native `vni` label.
+
+`site` (`dc`/`campus`/`core`) is also not gnmic-exported. It's added as a
+Prometheus `metric_relabel_configs` rule deriving it from `device` via the
+`-` naming convention regex (see #49), rather than repeating
+`label_replace()` in every dashboard query.
+
+- Config: `configs/prometheus/prometheus.yml`
+- This is separate from, and does not replace, the existing external Prometheus
+ instance — no cutover yet, both run in parallel pending validation
+
+```bash
+# Validate gnmic is subscribed and streaming from all targets
+docker logs clab-arista-evpn-fabric-gnmic
+
+# Scrape the Prometheus exporter directly
+docker exec clab-arista-evpn-fabric-gnmic wget -qO- http://localhost:9273/metrics | head
+
+# Query the in-topology Prometheus instance
+curl -s 'http://172.16.0.71:9090/api/v1/query?query=system_memory_state_used' | jq
+```
+
+### Weathermap panel generation (dashboard-as-code)
+
+`scripts/generate_weathermap.py` generates a weathermap-ng Grafana panel from
+live IPFabric topology + gnmic/Prometheus metrics. See
+[`scripts/README.md`](scripts/README.md) for usage and details.
+
## 📁 Repository Structure
```
@@ -343,7 +413,16 @@ arista-evpn-vxlan-clab/
│ ├── campus-spine1.cfg, campus-spine2.cfg
│ ├── campus-leaf1.cfg … campus-leaf4.cfg
│ ├── campus-border-leaf1.cfg, campus-border-leaf2.cfg
-│ └── campus-access1.cfg, campus-access2.cfg
+│ ├── campus-access1.cfg, campus-access2.cfg
+│ ├── gnmic/
+│ │ └── gnmic-config.yml
+│ ├── prometheus/
+│ │ └── prometheus.yml
+│ └── grafana/
+│ └── weathermap-dashboard.json
+├── scripts/
+│ ├── README.md
+│ └── generate_weathermap.py
└── hosts/
├── README.md
├── dc-server1_interfaces … dc-server4_interfaces
diff --git a/assets/grafana-weathermap-dashboard.png b/assets/grafana-weathermap-dashboard.png
new file mode 100644
index 0000000..b73a5b1
Binary files /dev/null and b/assets/grafana-weathermap-dashboard.png differ
diff --git a/assets/weathermap-pipeline.svg b/assets/weathermap-pipeline.svg
new file mode 100644
index 0000000..b47cdc2
--- /dev/null
+++ b/assets/weathermap-pipeline.svg
@@ -0,0 +1,72 @@
+
diff --git a/configs/gnmic/gnmic-config.yml b/configs/gnmic/gnmic-config.yml
new file mode 100644
index 0000000..1fe6e5e
--- /dev/null
+++ b/configs/gnmic/gnmic-config.yml
@@ -0,0 +1,135 @@
+username: admin
+password: admin
+port: 6030
+timeout: 10s
+
+common_eos_subscriptions: &common_eos_subs
+ insecure: true
+ subscriptions:
+ - eos-interfaces
+ - eos-bgp
+ - eos-system
+
+vtep_eos_subscriptions: &vtep_eos_subs
+ insecure: true
+ subscriptions:
+ - eos-interfaces
+ - eos-bgp
+ - eos-system
+ - eos-vxlan
+
+targets:
+ # DC fabric
+ dc-spine1: *common_eos_subs
+ dc-spine2: *common_eos_subs
+ dc-leaf1: *vtep_eos_subs
+ dc-leaf2: *vtep_eos_subs
+ dc-leaf3: *vtep_eos_subs
+ dc-leaf4: *vtep_eos_subs
+ dc-leaf5: *vtep_eos_subs
+ dc-leaf6: *vtep_eos_subs
+ dc-leaf7: *vtep_eos_subs
+ dc-leaf8: *vtep_eos_subs
+ dc-border-leaf1: *vtep_eos_subs
+ dc-border-leaf2: *vtep_eos_subs
+ dc-access1: *common_eos_subs
+ dc-access2: *common_eos_subs
+ dc-access3: *common_eos_subs
+ dc-access4: *common_eos_subs
+
+ # Core
+ core1: *common_eos_subs
+ core2: *common_eos_subs
+
+ # Campus fabric
+ campus-spine1: *common_eos_subs
+ campus-spine2: *common_eos_subs
+ campus-border-leaf1: *vtep_eos_subs
+ campus-border-leaf2: *vtep_eos_subs
+ campus-leaf1: *vtep_eos_subs
+ campus-leaf2: *vtep_eos_subs
+ campus-leaf3: *vtep_eos_subs
+ campus-leaf4: *vtep_eos_subs
+ campus-access1: *common_eos_subs
+ campus-access2: *common_eos_subs
+
+subscriptions:
+ eos-interfaces:
+ mode: stream
+ stream-mode: sample
+ sample-interval: 10s
+ paths:
+ - /interfaces/interface/state/oper-status
+ - /interfaces/interface/state/counters
+ eos-bgp:
+ mode: stream
+ stream-mode: sample
+ sample-interval: 10s
+ paths:
+ - /network-instances/network-instance/protocols/protocol/bgp/neighbors/neighbor/state/session-state
+ - /network-instances/network-instance/protocols/protocol/bgp/neighbors/neighbor/afi-safis/afi-safi/state/prefixes
+ eos-system:
+ mode: stream
+ stream-mode: sample
+ sample-interval: 10s
+ paths:
+ - /system/cpus/cpu/state
+ - /system/memory/state
+ eos-vxlan:
+ mode: stream
+ stream-mode: sample
+ sample-interval: 10s
+ paths:
+ # VLAN-to-VNI mapping (join key for MAC-count-per-VNI in Prometheus)
+ - /interfaces/interface[name=Vxlan1]/arista-exp-eos-vxlan:arista-vxlan/vlan-to-vnis/vlan-to-vni
+ # MAC table entries, tagged by VLAN — joined with the mapping above to get per-VNI counts
+ - /network-instances/network-instance/fdb/mac-table/entries/entry
+
+outputs:
+ prom-output:
+ type: prometheus
+ listen: :9273
+ event-processors:
+ - state-to-int
+
+# oper-status and BGP session-state are string enums; the Prometheus output
+# silently drops non-numeric values, so they're mapped to 1 (up/established) / 0 (anything else)
+processors:
+ state-to-int:
+ event-strings:
+ value-names:
+ - ".*oper-status$"
+ - ".*session-state$"
+ transforms:
+ - replace:
+ apply-on: "value"
+ old: "UP"
+ new: "1"
+ - replace:
+ apply-on: "value"
+ old: "ESTABLISHED"
+ new: "1"
+ - replace:
+ apply-on: "value"
+ old: "DOWN"
+ new: "0"
+ - replace:
+ apply-on: "value"
+ old: "IDLE"
+ new: "0"
+ - replace:
+ apply-on: "value"
+ old: "CONNECT"
+ new: "0"
+ - replace:
+ apply-on: "value"
+ old: "ACTIVE"
+ new: "0"
+ - replace:
+ apply-on: "value"
+ old: "OPENSENT"
+ new: "0"
+ - replace:
+ apply-on: "value"
+ old: "OPENCONFIRM"
+ new: "0"
diff --git a/configs/grafana/dashboard-base.json b/configs/grafana/dashboard-base.json
new file mode 100644
index 0000000..356875c
--- /dev/null
+++ b/configs/grafana/dashboard-base.json
@@ -0,0 +1,197 @@
+{
+ "uid": "evpn-vxlan-fabric-weathermap",
+ "title": "EVPN/VXLAN Fabric Weathermap",
+ "tags": ["evpn", "vxlan", "weathermap", "auto-generated"],
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "version": 0,
+ "templating": {
+ "list": [
+ {
+ "name": "site",
+ "type": "query",
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "query": { "query": "label_values(interfaces_interface_state_oper_status, site)", "refId": "site" },
+ "refresh": 2,
+ "sort": 1,
+ "multi": true,
+ "includeAll": true,
+ "current": { "text": "All", "value": "$__all" }
+ },
+ {
+ "name": "device",
+ "type": "query",
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "query": {
+ "query": "label_values(interfaces_interface_state_oper_status{site=~\"$site\"}, device)",
+ "refId": "device"
+ },
+ "refresh": 2,
+ "sort": 1,
+ "multi": true,
+ "includeAll": true,
+ "current": { "text": "All", "value": "$__all" }
+ }
+ ]
+ },
+ "panels": [
+ {
+ "id": 1,
+ "title": "__WEATHERMAP_SLOT__",
+ "gridPos": { "h": 24, "w": 24, "x": 0, "y": 0 }
+ },
+ {
+ "id": 2,
+ "type": "table",
+ "title": "BGP Sessions",
+ "gridPos": { "h": 10, "w": 12, "x": 0, "y": 24 },
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "targets": [
+ {
+ "refId": "A",
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "instant": true,
+ "format": "table",
+ "expr": "network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state{device=~\"$device\"}"
+ }
+ ],
+ "transformations": [
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true,
+ "__name__": true,
+ "instance": true,
+ "job": true,
+ "protocol_identifier": true,
+ "protocol_name": true,
+ "subscription_name": true
+ },
+ "renameByName": {
+ "device": "Device",
+ "neighbor_address": "Neighbor Address",
+ "network_instance_name": "VRF",
+ "Value": "State"
+ }
+ }
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [
+ { "type": "value", "options": { "0": { "text": "Down", "color": "red" }, "1": { "text": "Up", "color": "green" } } }
+ ],
+ "custom": { "cellOptions": { "type": "color-background" } }
+ },
+ "overrides": [
+ {
+ "matcher": { "id": "byName", "options": "State" },
+ "properties": [
+ { "id": "custom.cellOptions", "value": { "type": "color-background" } },
+ {
+ "id": "mappings",
+ "value": [
+ { "type": "value", "options": { "0": { "text": "Down", "color": "red" }, "1": { "text": "Up", "color": "green" } } }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "id": 3,
+ "type": "table",
+ "title": "Ports / Interfaces",
+ "gridPos": { "h": 10, "w": 12, "x": 12, "y": 24 },
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "targets": [
+ {
+ "refId": "A",
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "instant": true,
+ "format": "table",
+ "expr": "interfaces_interface_state_oper_status{device=~\"$device\"}"
+ },
+ {
+ "refId": "B",
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "instant": true,
+ "format": "table",
+ "expr": "rate(interfaces_interface_state_counters_in_octets{device=~\"$device\"}[5m]) * 8"
+ },
+ {
+ "refId": "C",
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "instant": true,
+ "format": "table",
+ "expr": "rate(interfaces_interface_state_counters_out_octets{device=~\"$device\"}[5m]) * 8"
+ }
+ ],
+ "transformations": [
+ { "id": "merge", "options": {} },
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true,
+ "__name__": true,
+ "instance": true,
+ "job": true,
+ "subscription_name": true
+ },
+ "renameByName": {
+ "device": "Device",
+ "interface": "Interface",
+ "Value #A": "Status",
+ "Value #B": "In (bps)",
+ "Value #C": "Out (bps)"
+ }
+ }
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {},
+ "overrides": [
+ {
+ "matcher": { "id": "byName", "options": "Status" },
+ "properties": [
+ { "id": "custom.cellOptions", "value": { "type": "color-background" } },
+ {
+ "id": "mappings",
+ "value": [
+ { "type": "value", "options": { "0": { "text": "Down", "color": "red" }, "1": { "text": "Up", "color": "green" } } }
+ ]
+ }
+ ]
+ },
+ { "matcher": { "id": "byName", "options": "In (bps)" }, "properties": [{ "id": "unit", "value": "bps" }] },
+ { "matcher": { "id": "byName", "options": "Out (bps)" }, "properties": [{ "id": "unit", "value": "bps" }] }
+ ]
+ }
+ },
+ {
+ "id": 4,
+ "type": "timeseries",
+ "title": "Throughput per Site",
+ "gridPos": { "h": 10, "w": 24, "x": 0, "y": 34 },
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "fieldConfig": { "defaults": { "unit": "bps" }, "overrides": [] },
+ "targets": [
+ {
+ "refId": "A",
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "expr": "sum by (site) (rate(interfaces_interface_state_counters_in_octets{site=~\"$site\", interface!~\"Management.*\"}[5m]) * 8)",
+ "legendFormat": "{{site}} in"
+ },
+ {
+ "refId": "B",
+ "datasource": { "type": "prometheus", "uid": "__DATASOURCE_UID__" },
+ "expr": "-sum by (site) (rate(interfaces_interface_state_counters_out_octets{site=~\"$site\", interface!~\"Management.*\"}[5m]) * 8)",
+ "legendFormat": "{{site}} out"
+ }
+ ]
+ }
+ ]
+}
diff --git a/configs/grafana/weathermap-dashboard.json b/configs/grafana/weathermap-dashboard.json
new file mode 100644
index 0000000..513c83c
--- /dev/null
+++ b/configs/grafana/weathermap-dashboard.json
@@ -0,0 +1,4233 @@
+{
+ "dashboard": {
+ "uid": "evpn-vxlan-fabric-weathermap",
+ "title": "EVPN/VXLAN Fabric Weathermap",
+ "tags": [
+ "evpn",
+ "vxlan",
+ "weathermap",
+ "auto-generated"
+ ],
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "version": 0,
+ "templating": {
+ "list": [
+ {
+ "name": "site",
+ "type": "query",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "query": {
+ "query": "label_values(interfaces_interface_state_oper_status, site)",
+ "refId": "site"
+ },
+ "refresh": 2,
+ "sort": 1,
+ "multi": true,
+ "includeAll": true,
+ "current": {
+ "text": "All",
+ "value": "$__all"
+ }
+ },
+ {
+ "name": "device",
+ "type": "query",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "query": {
+ "query": "label_values(interfaces_interface_state_oper_status{site=~\"$site\"}, device)",
+ "refId": "device"
+ },
+ "refresh": 2,
+ "sort": 1,
+ "multi": true,
+ "includeAll": true,
+ "current": {
+ "text": "All",
+ "value": "$__all"
+ }
+ }
+ ]
+ },
+ "panels": [
+ {
+ "id": 1,
+ "title": "Fabric Weathermap",
+ "gridPos": {
+ "h": 24,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "type": "tamirsuliman-weathermap-panel",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "min by (device) (interfaces_interface_state_oper_status{device=~\"dc-spine1|dc-spine2|campus-border-leaf1|campus-spine2|campus-border-leaf2|campus-spine1|dc-leaf1|dc-leaf3|dc-leaf4|dc-leaf5|dc-leaf7|dc-border-leaf1|dc-leaf6|dc-access1|dc-access2|dc-access4|dc-access3|dc-leaf8|dc-border-leaf2|dc-leaf2|campus-access2|campus-leaf1|campus-leaf3|campus-access1|campus-leaf4|campus-leaf2|core1|core2\"}) * (min by (device) (network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state{network_instance_name=\"default\", device=~\"dc-spine1|dc-spine2|campus-border-leaf1|campus-spine2|campus-border-leaf2|campus-spine1|dc-leaf1|dc-leaf3|dc-leaf4|dc-leaf5|dc-leaf7|dc-border-leaf1|dc-leaf6|dc-access1|dc-access2|dc-access4|dc-access3|dc-leaf8|dc-border-leaf2|dc-leaf2|campus-access2|campus-leaf1|campus-leaf3|campus-access1|campus-leaf4|campus-leaf2|core1|core2\"}) or (min by (device) (interfaces_interface_state_oper_status{device=~\"dc-spine1|dc-spine2|campus-border-leaf1|campus-spine2|campus-border-leaf2|campus-spine1|dc-leaf1|dc-leaf3|dc-leaf4|dc-leaf5|dc-leaf7|dc-border-leaf1|dc-leaf6|dc-access1|dc-access2|dc-access4|dc-access3|dc-leaf8|dc-border-leaf2|dc-leaf2|campus-access2|campus-leaf1|campus-leaf3|campus-access1|campus-leaf4|campus-leaf2|core1|core2\"}) * 0 + 1))",
+ "legendFormat": "STATUS {{device}}",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ }
+ },
+ {
+ "refId": "B",
+ "expr": "rate(interfaces_interface_state_counters_out_octets{device=~\"dc-spine1|dc-spine2|campus-border-leaf1|campus-spine2|campus-border-leaf2|campus-spine1|dc-leaf1|dc-leaf3|dc-leaf4|dc-leaf5|dc-leaf7|dc-border-leaf1|dc-leaf6|dc-access1|dc-access2|dc-access4|dc-access3|dc-leaf8|dc-border-leaf2|dc-leaf2|campus-access2|campus-leaf1|campus-leaf3|campus-access1|campus-leaf4|campus-leaf2|core1|core2\", interface=~\"Ethernet1|Ethernet10|Ethernet11|Ethernet12|Ethernet13|Ethernet14|Ethernet2|Ethernet3|Ethernet4|Ethernet5|Ethernet6|Ethernet7|Ethernet8|Ethernet9\"}[5m]) * 8",
+ "legendFormat": "{{device}} {{interface}} tx",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ }
+ },
+ {
+ "refId": "D",
+ "expr": "count by (device, vlan) (network_instances_network_instance_fdb_mac_table_entries_entry_vlan{device=~\"campus-leaf1|campus-leaf2|campus-leaf3|campus-leaf4|dc-leaf1|dc-leaf2|dc-leaf5|dc-leaf6\"})\n* on(device, vlan) group_left(vlan_to_vni_state_vni)\nmax by (device, vlan) (interfaces_interface_arista_vxlan_vlan_to_vnis_vlan_to_vni_state_vni{device=~\"campus-leaf1|campus-leaf2|campus-leaf3|campus-leaf4|dc-leaf1|dc-leaf2|dc-leaf5|dc-leaf6\"})",
+ "legendFormat": "VNI {{device}} {{vlan}}",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ }
+ }
+ ],
+ "options": {
+ "weathermap": {
+ "version": 14,
+ "id": "evpn-vxlan-fabric-weathermap",
+ "nodes": [
+ {
+ "id": "dc-spine1",
+ "label": "dc-spine1",
+ "position": [
+ 350,
+ 80
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-spine1",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 10,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-spine2",
+ "label": "dc-spine2",
+ "position": [
+ 750,
+ 80
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-spine2",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 10,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "campus-border-leaf1",
+ "label": "campus-border-leaf1",
+ "position": [
+ 1450,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS campus-border-leaf1",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 5,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "campus-spine2",
+ "label": "campus-spine2",
+ "position": [
+ 1850,
+ 80
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS campus-spine2",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 6,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "campus-border-leaf2",
+ "label": "campus-border-leaf2",
+ "position": [
+ 1555,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS campus-border-leaf2",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 1,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 4,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "campus-spine1",
+ "label": "campus-spine1",
+ "position": [
+ 1600,
+ 80
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS campus-spine1",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 6,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-leaf1",
+ "label": "dc-leaf1",
+ "position": [
+ 80,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-leaf1",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 1,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 3,
+ "numFilledLinks": 0
+ }
+ },
+ "tooltipMetrics": [
+ {
+ "label": "VNI dc-leaf1 40",
+ "query": "VNI dc-leaf1 40",
+ "units": "MACs"
+ }
+ ]
+ },
+ {
+ "id": "dc-leaf3",
+ "label": "dc-leaf3",
+ "position": [
+ 290,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-leaf3",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 1,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 3,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-leaf4",
+ "label": "dc-leaf4",
+ "position": [
+ 395,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-leaf4",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-leaf5",
+ "label": "dc-leaf5",
+ "position": [
+ 500,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-leaf5",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 1,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 3,
+ "numFilledLinks": 0
+ }
+ },
+ "tooltipMetrics": [
+ {
+ "label": "VNI dc-leaf5 40",
+ "query": "VNI dc-leaf5 40",
+ "units": "MACs"
+ }
+ ]
+ },
+ {
+ "id": "dc-leaf7",
+ "label": "dc-leaf7",
+ "position": [
+ 710,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-leaf7",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 1,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 3,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-border-leaf1",
+ "label": "dc-border-leaf1",
+ "position": [
+ 920,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-border-leaf1",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 3,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-leaf6",
+ "label": "dc-leaf6",
+ "position": [
+ 605,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-leaf6",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ },
+ "tooltipMetrics": [
+ {
+ "label": "VNI dc-leaf6 40",
+ "query": "VNI dc-leaf6 40",
+ "units": "MACs"
+ }
+ ]
+ },
+ {
+ "id": "dc-access1",
+ "label": "dc-access1",
+ "position": [
+ 132,
+ 440
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-access1",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-access2",
+ "label": "dc-access2",
+ "position": [
+ 342,
+ 440
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-access2",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-access4",
+ "label": "dc-access4",
+ "position": [
+ 762,
+ 440
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-access4",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-access3",
+ "label": "dc-access3",
+ "position": [
+ 552,
+ 440
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-access3",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-leaf8",
+ "label": "dc-leaf8",
+ "position": [
+ 815,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-leaf8",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-border-leaf2",
+ "label": "dc-border-leaf2",
+ "position": [
+ 1025,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-border-leaf2",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 3,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "dc-leaf2",
+ "label": "dc-leaf2",
+ "position": [
+ 185,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS dc-leaf2",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ },
+ "tooltipMetrics": [
+ {
+ "label": "VNI dc-leaf2 40",
+ "query": "VNI dc-leaf2 40",
+ "units": "MACs"
+ }
+ ]
+ },
+ {
+ "id": "campus-access2",
+ "label": "campus-access2",
+ "position": [
+ 1922,
+ 440
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS campus-access2",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "campus-leaf1",
+ "label": "campus-leaf1",
+ "position": [
+ 1660,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS campus-leaf1",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 1,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 3,
+ "numFilledLinks": 0
+ }
+ },
+ "tooltipMetrics": [
+ {
+ "label": "VNI campus-leaf1 50",
+ "query": "VNI campus-leaf1 50",
+ "units": "MACs"
+ }
+ ]
+ },
+ {
+ "id": "campus-leaf3",
+ "label": "campus-leaf3",
+ "position": [
+ 1870,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS campus-leaf3",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 1,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 3,
+ "numFilledLinks": 0
+ }
+ },
+ "tooltipMetrics": [
+ {
+ "label": "VNI campus-leaf3 50",
+ "query": "VNI campus-leaf3 50",
+ "units": "MACs"
+ }
+ ]
+ },
+ {
+ "id": "campus-access1",
+ "label": "campus-access1",
+ "position": [
+ 1712,
+ 440
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS campus-access1",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "campus-leaf4",
+ "label": "campus-leaf4",
+ "position": [
+ 1975,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS campus-leaf4",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ },
+ "tooltipMetrics": [
+ {
+ "label": "VNI campus-leaf4 50",
+ "query": "VNI campus-leaf4 50",
+ "units": "MACs"
+ }
+ ]
+ },
+ {
+ "id": "campus-leaf2",
+ "label": "campus-leaf2",
+ "position": [
+ 1765,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS campus-leaf2",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ },
+ "tooltipMetrics": [
+ {
+ "label": "VNI campus-leaf2 50",
+ "query": "VNI campus-leaf2 50",
+ "units": "MACs"
+ }
+ ]
+ },
+ {
+ "id": "core1",
+ "label": "core1",
+ "position": [
+ 1150,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS core1",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 3,
+ "numFilledLinks": 0
+ }
+ }
+ },
+ {
+ "id": "core2",
+ "label": "core2",
+ "position": [
+ 1280,
+ 260
+ ],
+ "isConnection": false,
+ "useConstantSpacing": false,
+ "compactVerticalLinks": true,
+ "padding": {
+ "horizontal": 12,
+ "vertical": 6
+ },
+ "colors": {
+ "font": "#ffffff",
+ "background": "#22252b",
+ "border": "#5794F2",
+ "statusDown": "#F2495C"
+ },
+ "nodeIcon": null,
+ "statusQuery": "STATUS core2",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [
+ {
+ "value": 0,
+ "color": "#F2495C"
+ },
+ {
+ "value": 1,
+ "color": "#73BF69"
+ }
+ ],
+ "anchors": {
+ "0": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "1": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "2": {
+ "numLinks": 0,
+ "numFilledLinks": 0
+ },
+ "3": {
+ "numLinks": 3,
+ "numFilledLinks": 0
+ },
+ "4": {
+ "numLinks": 2,
+ "numFilledLinks": 0
+ }
+ }
+ }
+ ],
+ "links": [
+ {
+ "id": "dc-leaf6-Ethernet12--dc-spine2-Ethernet6",
+ "nodes": [
+ {
+ "id": "dc-leaf6"
+ },
+ {
+ "id": "dc-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf6 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine2 Ethernet6 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-border-leaf1-Ethernet10--campus-border-leaf2-Ethernet10",
+ "nodes": [
+ {
+ "id": "campus-border-leaf1"
+ },
+ {
+ "id": "campus-border-leaf2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-border-leaf1 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-border-leaf2 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-border-leaf1-Ethernet11--campus-spine1-Ethernet5",
+ "nodes": [
+ {
+ "id": "campus-border-leaf1"
+ },
+ {
+ "id": "campus-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-border-leaf1 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine1 Ethernet5 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-border-leaf1-Ethernet12--campus-spine2-Ethernet5",
+ "nodes": [
+ {
+ "id": "campus-border-leaf1"
+ },
+ {
+ "id": "campus-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-border-leaf1 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine2 Ethernet5 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-border-leaf2-Ethernet12--campus-spine2-Ethernet6",
+ "nodes": [
+ {
+ "id": "campus-border-leaf2"
+ },
+ {
+ "id": "campus-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-border-leaf2 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine2 Ethernet6 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-leaf3-Ethernet12--campus-spine2-Ethernet3",
+ "nodes": [
+ {
+ "id": "campus-leaf3"
+ },
+ {
+ "id": "campus-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf3 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine2 Ethernet3 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-leaf4-Ethernet12--campus-spine2-Ethernet4",
+ "nodes": [
+ {
+ "id": "campus-leaf4"
+ },
+ {
+ "id": "campus-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf4 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine2 Ethernet4 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-leaf2-Ethernet12--campus-spine2-Ethernet2",
+ "nodes": [
+ {
+ "id": "campus-leaf2"
+ },
+ {
+ "id": "campus-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf2 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine2 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-border-leaf2-Ethernet11--campus-spine1-Ethernet6",
+ "nodes": [
+ {
+ "id": "campus-border-leaf2"
+ },
+ {
+ "id": "campus-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-border-leaf2 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine1 Ethernet6 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf1-Ethernet11--dc-spine1-Ethernet1",
+ "nodes": [
+ {
+ "id": "dc-leaf1"
+ },
+ {
+ "id": "dc-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf1 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine1 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf1-Ethernet12--dc-spine2-Ethernet1",
+ "nodes": [
+ {
+ "id": "dc-leaf1"
+ },
+ {
+ "id": "dc-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf1 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine2 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf3-Ethernet11--dc-spine1-Ethernet3",
+ "nodes": [
+ {
+ "id": "dc-leaf3"
+ },
+ {
+ "id": "dc-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf3 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine1 Ethernet3 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf3-Ethernet12--dc-spine2-Ethernet3",
+ "nodes": [
+ {
+ "id": "dc-leaf3"
+ },
+ {
+ "id": "dc-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf3 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine2 Ethernet3 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf3-Ethernet10--dc-leaf4-Ethernet10",
+ "nodes": [
+ {
+ "id": "dc-leaf3"
+ },
+ {
+ "id": "dc-leaf4"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf3 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf4 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-access2-Ethernet1--dc-leaf3-Ethernet1",
+ "nodes": [
+ {
+ "id": "dc-access2"
+ },
+ {
+ "id": "dc-leaf3"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-access2 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf3 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf4-Ethernet11--dc-spine1-Ethernet4",
+ "nodes": [
+ {
+ "id": "dc-leaf4"
+ },
+ {
+ "id": "dc-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf4 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine1 Ethernet4 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf4-Ethernet12--dc-spine2-Ethernet4",
+ "nodes": [
+ {
+ "id": "dc-leaf4"
+ },
+ {
+ "id": "dc-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf4 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine2 Ethernet4 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf5-Ethernet11--dc-spine1-Ethernet5",
+ "nodes": [
+ {
+ "id": "dc-leaf5"
+ },
+ {
+ "id": "dc-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf5 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine1 Ethernet5 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf5-Ethernet12--dc-spine2-Ethernet5",
+ "nodes": [
+ {
+ "id": "dc-leaf5"
+ },
+ {
+ "id": "dc-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf5 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine2 Ethernet5 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf5-Ethernet10--dc-leaf6-Ethernet10",
+ "nodes": [
+ {
+ "id": "dc-leaf5"
+ },
+ {
+ "id": "dc-leaf6"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf5 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf6 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf7-Ethernet11--dc-spine1-Ethernet7",
+ "nodes": [
+ {
+ "id": "dc-leaf7"
+ },
+ {
+ "id": "dc-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf7 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine1 Ethernet7 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf7-Ethernet12--dc-spine2-Ethernet7",
+ "nodes": [
+ {
+ "id": "dc-leaf7"
+ },
+ {
+ "id": "dc-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf7 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine2 Ethernet7 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-access4-Ethernet1--dc-leaf7-Ethernet1",
+ "nodes": [
+ {
+ "id": "dc-access4"
+ },
+ {
+ "id": "dc-leaf7"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-access4 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf7 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf7-Ethernet10--dc-leaf8-Ethernet10",
+ "nodes": [
+ {
+ "id": "dc-leaf7"
+ },
+ {
+ "id": "dc-leaf8"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf7 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf8 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-border-leaf1-Ethernet11--dc-spine1-Ethernet9",
+ "nodes": [
+ {
+ "id": "dc-border-leaf1"
+ },
+ {
+ "id": "dc-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-border-leaf1 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine1 Ethernet9 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-border-leaf1-Ethernet12--dc-spine2-Ethernet9",
+ "nodes": [
+ {
+ "id": "dc-border-leaf1"
+ },
+ {
+ "id": "dc-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-border-leaf1 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine2 Ethernet9 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf6-Ethernet11--dc-spine1-Ethernet6",
+ "nodes": [
+ {
+ "id": "dc-leaf6"
+ },
+ {
+ "id": "dc-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf6 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine1 Ethernet6 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-access1-Ethernet1--dc-leaf1-Ethernet1",
+ "nodes": [
+ {
+ "id": "dc-access1"
+ },
+ {
+ "id": "dc-leaf1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-access1 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf1 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-access1-Ethernet2--dc-leaf2-Ethernet1",
+ "nodes": [
+ {
+ "id": "dc-access1"
+ },
+ {
+ "id": "dc-leaf2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-access1 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf2 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-access2-Ethernet2--dc-leaf4-Ethernet1",
+ "nodes": [
+ {
+ "id": "dc-access2"
+ },
+ {
+ "id": "dc-leaf4"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-access2 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf4 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-access3-Ethernet1--dc-leaf5-Ethernet1",
+ "nodes": [
+ {
+ "id": "dc-access3"
+ },
+ {
+ "id": "dc-leaf5"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-access3 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf5 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-access3-Ethernet2--dc-leaf6-Ethernet1",
+ "nodes": [
+ {
+ "id": "dc-access3"
+ },
+ {
+ "id": "dc-leaf6"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-access3 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf6 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf8-Ethernet11--dc-spine1-Ethernet8",
+ "nodes": [
+ {
+ "id": "dc-leaf8"
+ },
+ {
+ "id": "dc-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf8 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine1 Ethernet8 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf8-Ethernet12--dc-spine2-Ethernet8",
+ "nodes": [
+ {
+ "id": "dc-leaf8"
+ },
+ {
+ "id": "dc-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf8 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine2 Ethernet8 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-access4-Ethernet2--dc-leaf8-Ethernet1",
+ "nodes": [
+ {
+ "id": "dc-access4"
+ },
+ {
+ "id": "dc-leaf8"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-access4 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf8 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-border-leaf2-Ethernet11--dc-spine1-Ethernet10",
+ "nodes": [
+ {
+ "id": "dc-border-leaf2"
+ },
+ {
+ "id": "dc-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-border-leaf2 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine1 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-border-leaf2-Ethernet12--dc-spine2-Ethernet10",
+ "nodes": [
+ {
+ "id": "dc-border-leaf2"
+ },
+ {
+ "id": "dc-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-border-leaf2 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine2 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-border-leaf1-Ethernet10--dc-border-leaf2-Ethernet10",
+ "nodes": [
+ {
+ "id": "dc-border-leaf1"
+ },
+ {
+ "id": "dc-border-leaf2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-border-leaf1 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-border-leaf2 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf2-Ethernet11--dc-spine1-Ethernet2",
+ "nodes": [
+ {
+ "id": "dc-leaf2"
+ },
+ {
+ "id": "dc-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf2 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine1 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf2-Ethernet12--dc-spine2-Ethernet2",
+ "nodes": [
+ {
+ "id": "dc-leaf2"
+ },
+ {
+ "id": "dc-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf2 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-spine2 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "dc-leaf1-Ethernet10--dc-leaf2-Ethernet10",
+ "nodes": [
+ {
+ "id": "dc-leaf1"
+ },
+ {
+ "id": "dc-leaf2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf1 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-leaf2 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-leaf1-Ethernet12--campus-spine2-Ethernet1",
+ "nodes": [
+ {
+ "id": "campus-leaf1"
+ },
+ {
+ "id": "campus-spine2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf1 Ethernet12 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine2 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-leaf1-Ethernet11--campus-spine1-Ethernet1",
+ "nodes": [
+ {
+ "id": "campus-leaf1"
+ },
+ {
+ "id": "campus-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf1 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine1 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-access1-Ethernet1--campus-leaf1-Ethernet1",
+ "nodes": [
+ {
+ "id": "campus-access1"
+ },
+ {
+ "id": "campus-leaf1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-access1 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf1 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-leaf1-Ethernet10--campus-leaf2-Ethernet10",
+ "nodes": [
+ {
+ "id": "campus-leaf1"
+ },
+ {
+ "id": "campus-leaf2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf1 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf2 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-leaf3-Ethernet11--campus-spine1-Ethernet3",
+ "nodes": [
+ {
+ "id": "campus-leaf3"
+ },
+ {
+ "id": "campus-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf3 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine1 Ethernet3 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-access2-Ethernet1--campus-leaf3-Ethernet1",
+ "nodes": [
+ {
+ "id": "campus-access2"
+ },
+ {
+ "id": "campus-leaf3"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-access2 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf3 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-leaf3-Ethernet10--campus-leaf4-Ethernet10",
+ "nodes": [
+ {
+ "id": "campus-leaf3"
+ },
+ {
+ "id": "campus-leaf4"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf3 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf4 Ethernet10 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-access1-Ethernet2--campus-leaf2-Ethernet1",
+ "nodes": [
+ {
+ "id": "campus-access1"
+ },
+ {
+ "id": "campus-leaf2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-access1 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf2 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-leaf4-Ethernet11--campus-spine1-Ethernet4",
+ "nodes": [
+ {
+ "id": "campus-leaf4"
+ },
+ {
+ "id": "campus-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf4 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine1 Ethernet4 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-access2-Ethernet2--campus-leaf4-Ethernet1",
+ "nodes": [
+ {
+ "id": "campus-access2"
+ },
+ {
+ "id": "campus-leaf4"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-access2 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf4 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-leaf2-Ethernet11--campus-spine1-Ethernet2",
+ "nodes": [
+ {
+ "id": "campus-leaf2"
+ },
+ {
+ "id": "campus-spine1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-leaf2 Ethernet11 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "campus-spine1 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-border-leaf1-Ethernet13--core1-Ethernet3",
+ "nodes": [
+ {
+ "id": "campus-border-leaf1"
+ },
+ {
+ "id": "core1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-border-leaf1 Ethernet13 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "core1 Ethernet3 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-border-leaf2-Ethernet13--core1-Ethernet4",
+ "nodes": [
+ {
+ "id": "campus-border-leaf2"
+ },
+ {
+ "id": "core1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-border-leaf2 Ethernet13 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "core1 Ethernet4 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "core1-Ethernet1--dc-border-leaf1-Ethernet13",
+ "nodes": [
+ {
+ "id": "core1"
+ },
+ {
+ "id": "dc-border-leaf1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "core1 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-border-leaf1 Ethernet13 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "core1-Ethernet2--dc-border-leaf2-Ethernet13",
+ "nodes": [
+ {
+ "id": "core1"
+ },
+ {
+ "id": "dc-border-leaf2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "core1 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-border-leaf2 Ethernet13 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "core1-Ethernet5--core2-Ethernet5",
+ "nodes": [
+ {
+ "id": "core1"
+ },
+ {
+ "id": "core2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "core1 Ethernet5 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "core2 Ethernet5 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-border-leaf1-Ethernet14--core2-Ethernet3",
+ "nodes": [
+ {
+ "id": "campus-border-leaf1"
+ },
+ {
+ "id": "core2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-border-leaf1 Ethernet14 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "core2 Ethernet3 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "campus-border-leaf2-Ethernet14--core2-Ethernet4",
+ "nodes": [
+ {
+ "id": "campus-border-leaf2"
+ },
+ {
+ "id": "core2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "campus-border-leaf2 Ethernet14 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "core2 Ethernet4 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "core2-Ethernet1--dc-border-leaf1-Ethernet14",
+ "nodes": [
+ {
+ "id": "core2"
+ },
+ {
+ "id": "dc-border-leaf1"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "core2 Ethernet1 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-border-leaf1 Ethernet14 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ },
+ {
+ "id": "core2-Ethernet2--dc-border-leaf2-Ethernet14",
+ "nodes": [
+ {
+ "id": "core2"
+ },
+ {
+ "id": "dc-border-leaf2"
+ }
+ ],
+ "sides": {
+ "A": {
+ "bandwidth": 1000000000,
+ "query": "core2 Ethernet2 tx",
+ "labelOffset": 55,
+ "anchor": 4,
+ "dashboardLink": ""
+ },
+ "Z": {
+ "bandwidth": 1000000000,
+ "query": "dc-border-leaf2 Ethernet14 tx",
+ "labelOffset": 55,
+ "anchor": 3,
+ "dashboardLink": ""
+ }
+ },
+ "units": "bps",
+ "arrows": {
+ "width": 8,
+ "height": 10,
+ "offset": 2
+ },
+ "stroke": 5,
+ "showThroughputPercentage": false
+ }
+ ],
+ "scale": [
+ {
+ "percent": 0,
+ "color": "#5794F2"
+ },
+ {
+ "percent": 70,
+ "color": "#FA6400"
+ },
+ {
+ "percent": 90,
+ "color": "#C4162A"
+ }
+ ],
+ "settings": {
+ "panel": {
+ "backgroundColor": "#212124",
+ "panelSize": {
+ "width": 1600,
+ "height": 1000
+ },
+ "zoomScale": 0,
+ "offset": {
+ "x": 0,
+ "y": 0
+ },
+ "showTimestamp": true,
+ "grid": {
+ "enabled": false,
+ "size": 10,
+ "guidesEnabled": false
+ }
+ },
+ "link": {
+ "spacing": {
+ "horizontal": 10,
+ "vertical": 5
+ },
+ "stroke": {
+ "color": "#CCCCDC"
+ },
+ "label": {
+ "background": "#FFFFFF",
+ "border": "#000000",
+ "font": "#000000"
+ },
+ "showAllWithPercentage": false,
+ "defaultUnits": "bps"
+ },
+ "tooltip": {
+ "fontSize": 10,
+ "textColor": "#CCCCDC",
+ "backgroundColor": "#1A1B1F",
+ "inboundColor": "#73BF69",
+ "outboundColor": "#5794F2",
+ "scaleToBandwidth": false
+ },
+ "fontSizing": {
+ "node": 12,
+ "link": 10
+ },
+ "scale": {
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "size": {
+ "width": 150,
+ "height": 100
+ },
+ "title": "Utilization",
+ "fontSizing": {
+ "title": 10,
+ "threshold": 9
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": 2,
+ "type": "table",
+ "title": "BGP Sessions",
+ "gridPos": {
+ "h": 10,
+ "w": 12,
+ "x": 0,
+ "y": 24
+ },
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "instant": true,
+ "format": "table",
+ "expr": "network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state{device=~\"$device\"}"
+ }
+ ],
+ "transformations": [
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true,
+ "__name__": true,
+ "instance": true,
+ "job": true,
+ "protocol_identifier": true,
+ "protocol_name": true,
+ "subscription_name": true
+ },
+ "renameByName": {
+ "device": "Device",
+ "neighbor_address": "Neighbor Address",
+ "network_instance_name": "VRF",
+ "Value": "State"
+ }
+ }
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [
+ {
+ "type": "value",
+ "options": {
+ "0": {
+ "text": "Down",
+ "color": "red"
+ },
+ "1": {
+ "text": "Up",
+ "color": "green"
+ }
+ }
+ }
+ ],
+ "custom": {
+ "cellOptions": {
+ "type": "color-background"
+ }
+ }
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "State"
+ },
+ "properties": [
+ {
+ "id": "custom.cellOptions",
+ "value": {
+ "type": "color-background"
+ }
+ },
+ {
+ "id": "mappings",
+ "value": [
+ {
+ "type": "value",
+ "options": {
+ "0": {
+ "text": "Down",
+ "color": "red"
+ },
+ "1": {
+ "text": "Up",
+ "color": "green"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "id": 3,
+ "type": "table",
+ "title": "Ports / Interfaces",
+ "gridPos": {
+ "h": 10,
+ "w": 12,
+ "x": 12,
+ "y": 24
+ },
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "instant": true,
+ "format": "table",
+ "expr": "interfaces_interface_state_oper_status{device=~\"$device\"}"
+ },
+ {
+ "refId": "B",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "instant": true,
+ "format": "table",
+ "expr": "rate(interfaces_interface_state_counters_in_octets{device=~\"$device\"}[5m]) * 8"
+ },
+ {
+ "refId": "C",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "instant": true,
+ "format": "table",
+ "expr": "rate(interfaces_interface_state_counters_out_octets{device=~\"$device\"}[5m]) * 8"
+ }
+ ],
+ "transformations": [
+ {
+ "id": "merge",
+ "options": {}
+ },
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true,
+ "__name__": true,
+ "instance": true,
+ "job": true,
+ "subscription_name": true
+ },
+ "renameByName": {
+ "device": "Device",
+ "interface": "Interface",
+ "Value #A": "Status",
+ "Value #B": "In (bps)",
+ "Value #C": "Out (bps)"
+ }
+ }
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {},
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Status"
+ },
+ "properties": [
+ {
+ "id": "custom.cellOptions",
+ "value": {
+ "type": "color-background"
+ }
+ },
+ {
+ "id": "mappings",
+ "value": [
+ {
+ "type": "value",
+ "options": {
+ "0": {
+ "text": "Down",
+ "color": "red"
+ },
+ "1": {
+ "text": "Up",
+ "color": "green"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "In (bps)"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "bps"
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Out (bps)"
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "bps"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "id": 4,
+ "type": "timeseries",
+ "title": "Throughput per Site",
+ "gridPos": {
+ "h": 10,
+ "w": 24,
+ "x": 0,
+ "y": 34
+ },
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "unit": "bps"
+ },
+ "overrides": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "expr": "sum by (site) (rate(interfaces_interface_state_counters_in_octets{site=~\"$site\", interface!~\"Management.*\"}[5m]) * 8)",
+ "legendFormat": "{{site}} in"
+ },
+ {
+ "refId": "B",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "cfrlusac89se8a"
+ },
+ "expr": "-sum by (site) (rate(interfaces_interface_state_counters_out_octets{site=~\"$site\", interface!~\"Management.*\"}[5m]) * 8)",
+ "legendFormat": "{{site}} out"
+ }
+ ]
+ }
+ ]
+ },
+ "overwrite": true
+}
diff --git a/configs/prometheus/prometheus.yml b/configs/prometheus/prometheus.yml
new file mode 100644
index 0000000..837ef16
--- /dev/null
+++ b/configs/prometheus/prometheus.yml
@@ -0,0 +1,92 @@
+global:
+ scrape_interval: 5s
+ evaluation_interval: 5s
+
+scrape_configs:
+ - job_name: gnmic
+ static_configs:
+ - targets: ["172.16.0.70:9273"]
+ metric_relabel_configs:
+ # device: raw label is "source" (gnmic subscription target)
+ - source_labels: [source]
+ target_label: device
+ regex: (.+)
+ replacement: $1
+ action: replace
+ - action: labeldrop
+ regex: source
+
+ # site: not exported by gnmic -- derived from the hostname naming
+ # convention (see CLAUDE.md: dc-*/campus-*/core* regex used for
+ # IPFabric site separation) here instead of repeating label_replace()
+ # in every dashboard query, see issue #49
+ - source_labels: [device]
+ target_label: site
+ regex: (campus|core|dc)-?.*
+ replacement: $1
+ action: replace
+
+ # interface: raw label is already "interface_name" (OpenConfig key on the
+ # "interface" path element) — normalize to "interface" for consistency
+ - source_labels: [interface_name]
+ target_label: interface
+ regex: (.+)
+ replacement: $1
+ action: replace
+ - action: labeldrop
+ regex: interface_name
+
+ # BGP neighbor address: raw label is double-prefixed "neighbor_neighbor_address"
+ # (OpenConfig path flattening — key name matches the path element name),
+ # see issue #44
+ - source_labels: [neighbor_neighbor_address]
+ target_label: neighbor_address
+ regex: (.+)
+ replacement: $1
+ action: replace
+ - action: labeldrop
+ regex: neighbor_neighbor_address
+
+ # VLAN id: normalize the two raw spellings ("vlan_to_vni_vlan" on the
+ # VLAN-to-VNI mapping, "entry_vlan" on FDB entries) to a single "vlan" label
+ - source_labels: [vlan_to_vni_vlan]
+ target_label: vlan
+ regex: (.+)
+ replacement: $1
+ action: replace
+ - action: labeldrop
+ regex: vlan_to_vni_vlan
+ - source_labels: [entry_vlan]
+ target_label: vlan
+ regex: (.+)
+ replacement: $1
+ action: replace
+ - action: labeldrop
+ regex: entry_vlan
+
+ # NOTE: "vni" is NOT available as a label from any gnmic-exported metric.
+ # The VNI only appears as the sample *value* of
+ # interfaces_interface_arista_vxlan_vlan_to_vnis_vlan_to_vni_state_vni,
+ # keyed by (device, interface, vlan). Relabeling can only rename existing
+ # labels, not promote a value to a label. Per-VNI MAC counts therefore
+ # require a PromQL join (vlan_to_vni_state_vni joined with the FDB
+ # entry count, grouped by vlan) rather than a native "vni" label — see
+ # issue #45 for the documented gap.
+
+ # MAC address on FDB entries: drop the "entry_" prefix for consistency
+ - source_labels: [entry_mac_address]
+ target_label: mac_address
+ regex: (.+)
+ replacement: $1
+ action: replace
+ - action: labeldrop
+ regex: entry_mac_address
+
+ # AFI/SAFI on BGP prefix counters: drop the flattened prefix
+ - source_labels: [afi_safi_afi_safi_name]
+ target_label: afi_safi
+ regex: (.+)
+ replacement: $1
+ action: replace
+ - action: labeldrop
+ regex: afi_safi_afi_safi_name
diff --git a/envrc.sample b/envrc.sample
new file mode 100644
index 0000000..52e9916
--- /dev/null
+++ b/envrc.sample
@@ -0,0 +1,11 @@
+export GRAFANA_URL=""
+export GRAFANA_TOKEN=""
+export GRAFANA_DATASOURCE_UID=""
+export IPFABRIC_URL=""
+export IPFABRIC_TOKEN=""
+
+# Optional -- see scripts/README.md for defaults and when to override
+# export IPFABRIC_SNAPSHOT="$last"
+# export PROMETHEUS_URL="http://172.16.0.71:9090"
+# export GRAFANA_DASHBOARD_UID="evpn-vxlan-fabric-weathermap"
+# export GRAFANA_WEATHERMAP_PLUGIN_ID="tamirsuliman-weathermap-panel"
diff --git a/evpn-lab.clab.yml b/evpn-lab.clab.yml
index 07c656d..510acde 100644
--- a/evpn-lab.clab.yml
+++ b/evpn-lab.clab.yml
@@ -290,6 +290,25 @@ topology:
exec:
- ifup eth1
+ # =====================================================
+ # TELEMETRY
+ # =====================================================
+
+ gnmic:
+ kind: linux
+ mgmt-ipv4: 172.16.0.70
+ image: ghcr.io/openconfig/gnmic:0.46.0
+ binds:
+ - configs/gnmic/gnmic-config.yml:/gnmic-config.yml:ro
+ cmd: --config /gnmic-config.yml --log subscribe
+
+ prometheus:
+ kind: linux
+ mgmt-ipv4: 172.16.0.71
+ image: prom/prometheus:v2.55.1
+ binds:
+ - configs/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
+
links:
# =====================================================
# DATACENTER FABRIC LINKS
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..1509ea3
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,167 @@
+# scripts/generate_weathermap.py
+
+Generates the **weathermap panel only** from live IPFabric + Prometheus
+data, merges it into a hand-authored dashboard base, and provisions the
+result to Grafana.
+
+
+
+## Files
+
+| File | What it is | Edit it? |
+| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
+| `configs/grafana/dashboard-base.json` | Hand-authored: BGP table, ports table, throughput graphs, `site`/`device` variables, layout. Has one reserved slot panel (`title: "__WEATHERMAP_SLOT__"`) where the weathermap gets spliced in. | Yes, directly |
+| `configs/grafana/weathermap-dashboard.json` | Generated build artifact — base + fresh weathermap panel, merged. What actually gets provisioned. | No — regenerate instead |
+
+Why split like this: topology data (nodes/links) changes with the lab and
+should regenerate every time; the rest of the dashboard (table layout,
+thresholds, transformations) is hand-tuned and shouldn't be touched by a
+topology refresh. See #52.
+
+## Quickstart
+
+```bash
+cp envrc.sample .envrc # fill in the values (see "Credentials" below)
+source .envrc
+
+python3 scripts/generate_weathermap.py # dry run: writes the JSON only
+python3 scripts/generate_weathermap.py --provision # also pushes it to Grafana
+```
+
+Re-run the same `--provision` command any time the lab topology changes —
+the script always rebuilds the weathermap panel from live data, so there's
+nothing to "detect", just re-run it. It's not automated yet (no watcher/CI
+hook), so re-running is a manual step for now.
+
+## Credentials
+
+| Value | Env var | Get it from the UI | Get it via API |
+| ----------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
+| IPFabric token | `IPFABRIC_TOKEN` | IPFabric → user menu → Settings → API Tokens → Add token | not possible, UI-only |
+| Grafana service account token | `GRAFANA_TOKEN` | Grafana → Administration → Service accounts → Add → give it **Editor** role → Add token | possible but multi-step (create account, then create token under it) — UI is simpler |
+| Prometheus datasource UID | `GRAFANA_DATASOURCE_UID` | Grafana → Connections → Data sources → open the one with gnmic data → UID is the last part of the URL | `curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" "$GRAFANA_URL/api/datasources" \| jq '.[] \| {name,uid}'` |
+| Weathermap plugin ID | `GRAFANA_WEATHERMAP_PLUGIN_ID` (optional, only if not the default fork) | Grafana → Administration → Plugins → search "weathermap" → id is in the URL | — |
+
+⚠️ The datasource must be the one that can actually see gnmic metrics —
+picking an unrelated Prometheus instance renders the panel with no data
+(see #48).
+
+## Verifying the result
+
+**In the browser** (the only way to actually see it render — colors,
+tables, and the ports-table merge transformation all happen client-side):
+
+1. Open `$GRAFANA_URL/d/evpn-vxlan-fabric-weathermap`.
+2. Weathermap: nodes grouped spine-top → access-bottom, all green.
+3. `site`/`device` dropdowns filter the two tables below.
+4. BGP Sessions / Ports tables: populated, green=Up / red=Down.
+5. Throughput per Site: signed graph, non-zero values.
+
+**Without a browser**, use Grafana's **Explore** view (paste a PromQL
+expression from the panel JSON and run it) — same result as the UI check
+above minus the rendering, no `curl` needed. Or hit the API directly:
+
+```bash
+curl -s -X POST -H "Authorization: Bearer $GRAFANA_TOKEN" -H "Content-Type: application/json" \
+ "$GRAFANA_URL/api/ds/query" -d '{
+ "queries": [{"refId":"A","datasource":{"type":"prometheus","uid":"'"$GRAFANA_DATASOURCE_UID"'"},
+ "expr":"min by (device) (interfaces_interface_state_oper_status)","instant":true}],
+ "from":"now-5m","to":"now"}' | python3 -m json.tool
+```
+
+## Troubleshooting
+
+| Symptom | Cause | Check |
+| ---------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
+| `Refusing to provision: set GRAFANA_DATASOURCE_UID...` | Env var unset | `echo $GRAFANA_DATASOURCE_UID` |
+| Panel renders with no data | Wrong datasource — can't see gnmic metrics | Explore → run `up` against it, confirm gnmic series come back |
+| `Base dashboard has no panel titled '__WEATHERMAP_SLOT__'` | Someone edited `dashboard-base.json` and renamed/removed that panel | Check panel `id: 1` still has that exact title |
+| Manually-moved node keeps resetting position | `GRAFANA_URL`/`GRAFANA_TOKEN` weren't set for that run | Look for `Fetching live node positions from ...` in the run output |
+| `HTTP 401/403` fetching live positions | Expired/bad Grafana token | Re-issue the service account token |
+
+## Environment variables
+
+| Variable | Required | Default |
+| ------------------------------ | ------------------ | ------------------------------------- |
+| `IPFABRIC_URL` | yes | — |
+| `IPFABRIC_TOKEN` | yes | — |
+| `IPFABRIC_SNAPSHOT` | no | `$last` |
+| `PROMETHEUS_URL` | no | `http://172.16.0.71:9090` |
+| `GRAFANA_URL` | no* | — |
+| `GRAFANA_TOKEN` | no* | — |
+| `GRAFANA_DATASOURCE_UID` | with `--provision` | — |
+| `GRAFANA_DASHBOARD_UID` | no | `evpn-vxlan-fabric-weathermap` |
+| `GRAFANA_WEATHERMAP_PLUGIN_ID` | no | `tamirsuliman-weathermap-panel` |
+| `--base` (CLI flag) | no | `configs/grafana/dashboard-base.json` |
+
+\* `GRAFANA_URL`/`GRAFANA_TOKEN` are required for `--provision`, and
+optional otherwise — if set on a plain (non-`--provision`) run, they're
+used to read back live node positions so manual repositioning survives
+the next regeneration (see #53). Skip them and every node just gets the
+default layout.
+
+## How it works
+
+1. Fetch device inventory + connectivity-matrix from IPFabric.
+2. Keep only physical Ethernet↔Ethernet links (drop management-plane and
+ `.100`/`.200` subinterface rows), dedupe the two directions IPFabric
+ reports per link.
+3. Position each node:
+ - **New node** → layered default: Y-tier by role parsed from the
+ hostname (`spine → core → border-leaf → leaf → access`), X grouped
+ by site within the tier. A hostname that matches no role is logged
+ and dropped into a fallback tier, never silently misplaced (#53).
+ - **Known node** → whatever position is currently live in Grafana,
+ unchanged. This is why `GRAFANA_URL`/`GRAFANA_TOKEN` matter even on
+ a dry run (#53).
+4. Build the panel's PromQL `targets`: node status, per-side link tx (each
+ side its own egress counter, so both directions are represented — #57),
+ VXLAN MAC/VNI tooltip — each with an explicit `legendFormat`.
+5. Build `nodes[]`/`links[]`, including the plugin's `anchors` tally
+ (link count per side) — required by the installed plugin fork even
+ though the schema doc says it's optional (#48).
+6. Cross-check every interface name against the live gnmic exporter
+ (`Et`→`Ethernet`, `Po`→`Port-Channel`, etc.); mismatches are logged,
+ not dropped — the link stays, just without data until fixed upstream.
+7. Load `dashboard-base.json`, substitute the real datasource UID, splice
+ the generated panel into `__WEATHERMAP_SLOT__` (keeping that slot's
+ `gridPos`/`id` — layout stays manual).
+8. Write `weathermap-dashboard.json`; provision it if `--provision`.
+
+## Known gaps
+
+- **VXLAN MAC-per-VNI tooltip**: the `vlan` join key doesn't match on live
+ data for any VTEP node, likely an Arista internal-vs-front-panel VLAN
+ translation gap. Only affects that one decorative tooltip metric, not
+ node/link status or traffic coloring. Tracked in #44.
+
+# scripts/generate_traffic.sh
+
+Generates real DC↔Campus traffic over VRF `gold` using `iperf3` (bundled
+in the `network-multitool` image every host container runs), with a live
+bandwidth dashboard. Without this, host containers sit idle and IPFabric
+ARP/MAC tables, Grafana throughput graphs, and the weathermap panel stay
+empty until someone manually generates traffic (see #55).
+
+## Quickstart
+
+```bash
+./scripts/generate_traffic.sh
+```
+
+## How it works
+
+- Starts `iperf3 -s` on the DC gold-VRF servers: `dc-server2`
+ (10.34.34.102), `dc-server4` (10.78.78.104).
+- Runs `iperf3 -c -R` from the paired campus gold-VRF hosts, reversing the
+ stream so the DC server pushes to the campus consumer: `dc-server2` →
+ `campus-host1`, `dc-server4` → `campus-host2` — exercising the full
+ DC→Core→Campus stitched EVPN Type-5 path end to end.
+- Redraws a terminal dashboard every second for the run duration: server
+ list, and live Mbits/sec per client session parsed from `iperf3 -i 1`
+ output.
+- On exit (duration end or Ctrl-C), kills the client processes and stops
+ the `iperf3 -s` processes on the DC servers — no leftover state.
+
+`dc-server1`/`dc-server3` (VLAN 40, VRF default, no gateway) are out of
+scope — this script only exercises the routed gold VRF path.
diff --git a/scripts/generate_traffic.sh b/scripts/generate_traffic.sh
new file mode 100755
index 0000000..6fe8079
--- /dev/null
+++ b/scripts/generate_traffic.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+# Generates DC<->Campus traffic over VRF gold using iperf3 (bundled in the
+# network-multitool image every host container runs), with a live
+# server/client bandwidth dashboard. Refs #55.
+set -euo pipefail
+
+DURATION="${1:?Usage: $0 }"
+if ! [[ "$DURATION" =~ ^[0-9]+$ ]] || [[ "$DURATION" -lt 1 ]]; then
+ echo "Duration must be a positive integer (seconds)" >&2
+ exit 1
+fi
+
+LAB_PREFIX="clab-arista-evpn-fabric"
+PORT=5301
+
+# server_name:server_ip:client_name — gold VRF pairs, stitched EVPN
+# Type-5 path DC -> Core -> Campus (see README Host Addressing table)
+PAIRS=(
+ "dc-server2:10.34.34.102:campus-host1"
+ "dc-server4:10.78.78.104:campus-host2"
+)
+
+WORKDIR="$(mktemp -d)"
+CLIENT_PIDS=()
+
+cleanup() {
+ for pid in "${CLIENT_PIDS[@]:-}"; do
+ kill "$pid" >/dev/null 2>&1 || true
+ done
+ for pair in "${PAIRS[@]}"; do
+ IFS=':' read -r server _ _ <<<"$pair"
+ docker exec "${LAB_PREFIX}-${server}" pkill -f "iperf3 -s -p ${PORT}" >/dev/null 2>&1 || true
+ done
+ rm -rf "$WORKDIR"
+}
+trap cleanup EXIT INT TERM
+
+echo "Starting iperf3 servers..."
+for pair in "${PAIRS[@]}"; do
+ IFS=':' read -r server server_ip _ <<<"$pair"
+ docker exec -d "${LAB_PREFIX}-${server}" iperf3 -s -p "$PORT"
+done
+sleep 1
+
+echo "Starting iperf3 clients for ${DURATION}s..."
+for pair in "${PAIRS[@]}"; do
+ IFS=':' read -r server server_ip client <<<"$pair"
+ logfile="${WORKDIR}/${client}.log"
+ # -R: DC hosts the service, campus is the consumer -- data should flow
+ # server -> client (download), not client -> server, to match how a
+ # real DC-hosted service/campus-consumer pair behaves.
+ docker exec "${LAB_PREFIX}-${client}" iperf3 -c "$server_ip" -p "$PORT" -R \
+ -t "$DURATION" -i 1 --forceflush -f m >"$logfile" 2>&1 &
+ CLIENT_PIDS+=("$!")
+done
+
+for ((elapsed = 0; elapsed <= DURATION; elapsed++)); do
+ clear
+ echo "EVPN/VXLAN lab traffic generator — ${elapsed}/${DURATION}s"
+ echo
+ echo "Servers (iperf3 -s):"
+ for pair in "${PAIRS[@]}"; do
+ IFS=':' read -r server server_ip _ <<<"$pair"
+ echo " ${server} (${server_ip}:${PORT})"
+ done
+ echo
+ echo "Clients (live bandwidth):"
+ for pair in "${PAIRS[@]}"; do
+ IFS=':' read -r server server_ip client <<<"$pair"
+ logfile="${WORKDIR}/${client}.log"
+ last_line="$(grep -E 'Mbits/sec' "$logfile" 2>/dev/null | tail -1 || true)"
+ bw="$(sed -E 's/.*[[:space:]]([0-9.]+ Mbits\/sec).*/\1/' <<<"$last_line")"
+ [[ -z "$last_line" ]] && bw="waiting..."
+ printf " %-13s -> %-13s : %s\n" "$server" "$client" "$bw"
+ done
+ sleep 1
+done
+
+wait "${CLIENT_PIDS[@]}" 2>/dev/null || true
+
+echo
+echo "Done. Summary:"
+for pair in "${PAIRS[@]}"; do
+ IFS=':' read -r server _ client <<<"$pair"
+ logfile="${WORKDIR}/${client}.log"
+ summary="$(grep -E 'receiver' "$logfile" 2>/dev/null || true)"
+ echo " ${server} -> ${client}: ${summary:-no data}"
+done
diff --git a/scripts/generate_weathermap.py b/scripts/generate_weathermap.py
new file mode 100644
index 0000000..8c9cc73
--- /dev/null
+++ b/scripts/generate_weathermap.py
@@ -0,0 +1,664 @@
+#!/usr/bin/env python3
+"""Generate a weathermap-ng PANEL (only) from IPFabric topology + gnmic/Prometheus
+metrics, merge it into a manually-authored dashboard base, and optionally
+provision the result into Grafana.
+
+Scope (see #52): this script knows only about the weathermap panel --
+targets (node status / link tx per side / VXLAN tooltip queries) and
+options.weathermap (nodes/links/scale/settings). It has no knowledge of the
+BGP sessions table, ports/interfaces table, or throughput panels -- those
+live in the manually-authored `configs/grafana/dashboard-base.json` and are
+not touched by this script.
+
+Data sources (see gitea issues #44, #47, #48):
+ - IPFabric REST API: device inventory + connectivity-matrix (topology)
+ - Prometheus (gnmic exporter, configs/prometheus/prometheus.yml): live label
+ values, used both to validate the IPFabric->gnmic interface-name mapping
+ and to resolve which VTEPs/VLANs actually have VXLAN data.
+
+Merge: the weathermap panel is generated fresh from live data on every run
+(no diffing), then spliced into the base dashboard's reserved slot (the panel
+titled "__WEATHERMAP_SLOT__"), keeping that slot's gridPos so the manually
+authored layout is never repositioned by the generator. See #52.
+
+Usage:
+ python3 scripts/generate_weathermap.py [--base PATH] [--output PATH] [--provision]
+
+Environment:
+ IPFABRIC_URL e.g. https://ipfabric.example.com
+ IPFABRIC_TOKEN API token (Inventory/Snapshots read access)
+ IPFABRIC_SNAPSHOT snapshot id, default "$last"
+ PROMETHEUS_URL default http://172.16.0.71:9090 (in-topology instance)
+ GRAFANA_URL required with --provision
+ GRAFANA_TOKEN required with --provision
+ GRAFANA_DATASOURCE_UID Prometheus datasource UID in Grafana, required with --provision
+ GRAFANA_DASHBOARD_UID default "evpn-vxlan-fabric-weathermap"
+ GRAFANA_WEATHERMAP_PLUGIN_ID default "tamirsuliman-weathermap-panel" -- override
+ if a different weathermap-ng fork/plugin id is installed.
+"""
+import argparse
+import json
+import os
+import re
+import sys
+import urllib.request
+import urllib.error
+import urllib.parse
+
+WEATHERMAP_SCHEMA_VERSION = 14
+
+# IPFabric abbreviated interface prefixes -> gnmic/OpenConfig full names.
+# Longest-prefix-first so e.g. "Ma" doesn't shadow a hypothetical multi-letter clash.
+INTERFACE_PREFIX_ALIASES = {
+ "Et": "Ethernet",
+ "Po": "Port-Channel",
+ "Lo": "Loopback",
+ "Vl": "Vlan",
+ "Ma": "Management",
+}
+
+SITE_ORDER = ["dc", "core", "campus"]
+GRID_COLUMNS = 6
+GRID_SPACING_X = 180
+GRID_SPACING_Y = 150
+
+# Layered default layout (see #53): device role is parsed from the hostname
+# naming convention, same trust level as the `site` parsing already in place
+# for the Prometheus relabel (#49) -- not IPFabric-derived, not configurable.
+# Checked in this order so e.g. "campus-border-leaf1" matches border-leaf
+# before the more general leaf pattern.
+ROLE_PATTERNS = [
+ ("spine", re.compile(r"-spine")),
+ ("core", re.compile(r"^core")),
+ ("border-leaf", re.compile(r"-border-leaf")),
+ ("leaf", re.compile(r"-leaf")),
+ ("access", re.compile(r"-access")),
+]
+ROLE_TIER_ORDER = ["spine", "core", "border-leaf", "leaf", "access"]
+TIER_SPACING_Y = 300
+# Per-site X band start, wide enough that the largest role/site group (8 dc
+# leafs) doesn't spill into the next site's band at GRID_COLUMNS=6.
+SITE_X_OFFSET = {"dc": 100, "core": 1300, "campus": 1600}
+UNMATCHED_ROLE_TIER_Y = len(ROLE_TIER_ORDER) * TIER_SPACING_Y + 300
+
+# tamirsuliman-weathermap-panel's numeric anchor enum (Center=0, Top=1,
+# Bottom=2, Left=3, Right=4) -- reverse-engineered from module.js, since
+# link.sides.*.anchor and node.anchors{} both index by this numeric value,
+# not by the anchor name string.
+ANCHOR = {"Center": 0, "Top": 1, "Bottom": 2, "Left": 3, "Right": 4}
+
+NODE_COLORS = {"font": "#ffffff", "background": "#22252b", "border": "#5794F2", "statusDown": "#F2495C"}
+STATUS_VALUE_MAPPINGS = [{"value": 0, "color": "#F2495C"}, {"value": 1, "color": "#73BF69"}]
+DEFAULT_LINK_BANDWIDTH_BPS = 10_000_000_000 # 10G fallback when IPFabric speed is missing
+
+
+def env(name, default=None, required=False):
+ val = os.environ.get(name, default)
+ if required and not val:
+ sys.exit(f"Missing required environment variable: {name}")
+ return val
+
+
+# --------------------------------------------------------------------------
+# IPFabric
+# --------------------------------------------------------------------------
+
+def ipfabric_request(base_url, token, path, body):
+ req = urllib.request.Request(
+ f"{base_url.rstrip('/')}/api{path}",
+ data=json.dumps(body).encode(),
+ headers={"X-API-Token": token, "Content-Type": "application/json"},
+ method="POST",
+ )
+ with urllib.request.urlopen(req, timeout=30) as resp:
+ return json.load(resp)["data"]
+
+
+def fetch_devices(base_url, token, snapshot):
+ return ipfabric_request(
+ base_url, token, "/tables/inventory/devices",
+ {"columns": ["hostname", "siteName", "vendor", "model", "sn"], "snapshot": snapshot,
+ "pagination": {"limit": 1000, "start": 0}},
+ )
+
+
+def fetch_connectivity_matrix(base_url, token, snapshot):
+ return ipfabric_request(
+ base_url, token, "/tables/interfaces/connectivity-matrix",
+ {"columns": ["localHost", "localInt", "remoteHost", "remoteInt", "protocol"], "snapshot": snapshot,
+ "pagination": {"limit": 5000, "start": 0}},
+ )
+
+
+def fetch_interface_speeds(base_url, token, snapshot):
+ rows = ipfabric_request(
+ base_url, token, "/tables/inventory/interfaces",
+ {"columns": ["hostname", "intName", "speed"], "snapshot": snapshot,
+ "pagination": {"limit": 5000, "start": 0}},
+ )
+ speeds = {}
+ for row in rows:
+ speed = row.get("speed")
+ if speed:
+ speeds[(row["hostname"], row["intName"])] = int(speed)
+ return speeds
+
+
+# --------------------------------------------------------------------------
+# Prometheus (validation + VXLAN discovery)
+# --------------------------------------------------------------------------
+
+def prom_query(prometheus_url, promql):
+ url = f"{prometheus_url.rstrip('/')}/api/v1/query?query=" + urllib.parse.quote(promql)
+ with urllib.request.urlopen(url, timeout=15) as resp:
+ payload = json.load(resp)
+ if payload["status"] != "success":
+ sys.exit(f"Prometheus query failed: {promql}")
+ return payload["data"]["result"]
+
+
+def known_interface_pairs(prometheus_url):
+ """(device, interface) pairs that actually exist in the gnmic exporter,
+ used to validate the IPFabric->gnmic interface alias before trusting it."""
+ results = prom_query(prometheus_url, "interfaces_interface_state_oper_status")
+ return {(m["metric"]["device"], m["metric"]["interface"]) for m in results}
+
+
+def known_vtep_vlan_pairs(prometheus_url):
+ """(device, vlan) pairs with a live VLAN-to-VNI mapping -- these are the
+ VTEP nodes eligible for a VXLAN MAC-count tooltip metric."""
+ results = prom_query(prometheus_url, "interfaces_interface_arista_vxlan_vlan_to_vnis_vlan_to_vni_state_vni")
+ return {(m["metric"]["device"], m["metric"]["vlan"]) for m in results}
+
+
+# --------------------------------------------------------------------------
+# Interface aliasing (IPFabric abbreviated name -> gnmic full name)
+# --------------------------------------------------------------------------
+
+def alias_interface(ipf_name):
+ match = re.match(r"^([A-Za-z]+)(\d.*)$", ipf_name)
+ if not match:
+ return ipf_name
+ prefix, rest = match.groups()
+ full = INTERFACE_PREFIX_ALIASES.get(prefix)
+ if full is None:
+ return ipf_name
+ return f"{full}{rest}"
+
+
+def promql_escape(s):
+ """re.escape() escapes '-' as '\\-', which Python's own regex engine
+ accepts but PromQL's RE2-based engine rejects ("unknown escape sequence
+ U+002D '-'") -- and every hostname in this lab contains hyphens. '-' is
+ not a regex metacharacter outside a character class, so it's safe to
+ leave unescaped."""
+ return re.escape(s).replace("\\-", "-")
+
+
+# --------------------------------------------------------------------------
+# Topology processing
+# --------------------------------------------------------------------------
+
+def parse_role(hostname):
+ """Device role from the hostname naming convention -- see ROLE_PATTERNS.
+ Returns None if nothing matches (logged by the caller, not silently
+ defaulted into the wrong tier)."""
+ for role, pattern in ROLE_PATTERNS:
+ if pattern.search(hostname):
+ return role
+ return None
+
+
+def build_layout(devices, mismatches):
+ """Default layered layout for nodes with no live (Grafana-drag-and-drop)
+ position yet -- see #53. Y-tier by role (spine top, access bottom), X
+ grouped/columned by site within each tier so same-role devices from
+ different sites don't overlap."""
+ groups = {} # (role, site) -> [hostname, ...]
+ unmatched = []
+ for dev in devices:
+ host, site = dev["hostname"], dev["siteName"]
+ role = parse_role(host)
+ if role is None:
+ unmatched.append(host)
+ continue
+ groups.setdefault((role, site), []).append(host)
+
+ positions = {}
+ for role_idx, role in enumerate(ROLE_TIER_ORDER):
+ tier_y = role_idx * TIER_SPACING_Y
+ for site in SITE_ORDER:
+ x_offset = SITE_X_OFFSET.get(site, max(SITE_X_OFFSET.values()) + 300)
+ for idx, host in enumerate(sorted(groups.get((role, site), []))):
+ col, row = idx % GRID_COLUMNS, idx // GRID_COLUMNS
+ positions[host] = [x_offset + col * GRID_SPACING_X, tier_y + row * GRID_SPACING_Y]
+
+ if unmatched:
+ mismatches.append(
+ f"{len(unmatched)} hostname(s) matched no role pattern (spine/core/border-leaf/leaf/access), "
+ f"placed in a fallback tier instead of guessing: {', '.join(sorted(unmatched))}"
+ )
+ for idx, host in enumerate(sorted(unmatched)):
+ col, row = idx % GRID_COLUMNS, idx // GRID_COLUMNS
+ positions[host] = [100 + col * GRID_SPACING_X, UNMATCHED_ROLE_TIER_Y + row * GRID_SPACING_Y]
+
+ return positions
+
+
+def dedupe_links(connectivity_matrix):
+ """connectivity-matrix reports each physical link twice (once from each
+ side), plus management-plane (Management0) neighbor entries, plus one row
+ per 802.1Q subinterface on trunked ports (e.g. Et13.100/Et13.200, used for
+ the gold VRF stitching on Core -- see README). Subinterfaces ride the same
+ physical port as their parent interface, so they'd otherwise show up as
+ bogus duplicate parallel links with permanently-unresolvable queries
+ (gnmic only subscribes to physical interface counters). Keep only
+ physical Ethernet-to-Ethernet links, one entry per unordered
+ (host,int)-(host,int) pair."""
+ seen = set()
+ links = []
+ for row in connectivity_matrix:
+ local_int, remote_int = row["localInt"], row["remoteInt"]
+ if "." in local_int or "." in remote_int:
+ continue
+ if not (local_int.startswith("Et") and remote_int.startswith("Et")):
+ continue
+ side_a = (row["localHost"], local_int)
+ side_z = (row["remoteHost"], remote_int)
+ key = tuple(sorted([side_a, side_z]))
+ if key in seen:
+ continue
+ seen.add(key)
+ links.append({"a_host": key[0][0], "a_int": key[0][1], "z_host": key[1][0], "z_int": key[1][1]})
+ return links
+
+
+# --------------------------------------------------------------------------
+# Weathermap assembly
+# --------------------------------------------------------------------------
+
+def build_weathermap(devices, links, interface_speeds, positions, prometheus_url, mismatches):
+ hostnames = [d["hostname"] for d in devices]
+ host_regex = "|".join(promql_escape(h) for h in hostnames)
+
+ known_pairs = known_interface_pairs(prometheus_url)
+ vtep_vlan_pairs = known_vtep_vlan_pairs(prometheus_url)
+ vtep_hosts = sorted({dev for dev, _ in vtep_vlan_pairs})
+
+ def resolve_and_check(host, ipf_int, side_label):
+ gnmic_int = alias_interface(ipf_int)
+ if (host, gnmic_int) not in known_pairs:
+ mismatches.append(
+ f"{host} {ipf_int} -> {gnmic_int} ({side_label}): no matching series in "
+ f"interfaces_interface_state_oper_status -- link will reference a query that "
+ f"resolves to no data until the gnmic/IPFabric interface names are aligned "
+ f"(see #47/#48 interface aliasing fallback)"
+ )
+ return gnmic_int
+
+ # -- nodes --
+ # anchors{} tallies how many link-sides attach to each anchor position on
+ # this node; every link below uses Right for its A side and Left for its
+ # Z side, so that's what gets counted per node here.
+ anchor_counts = {host: {a: 0 for a in ANCHOR.values()} for host in (d["hostname"] for d in devices)}
+ for link in links:
+ anchor_counts[link["a_host"]][ANCHOR["Right"]] += 1
+ anchor_counts[link["z_host"]][ANCHOR["Left"]] += 1
+
+ nodes = []
+ for dev in devices:
+ host = dev["hostname"]
+ node = {
+ "id": host,
+ "label": host,
+ "position": positions[host],
+ "isConnection": False,
+ "useConstantSpacing": False,
+ # True (not the plugin default) so rendered node HEIGHT is
+ # constant, decoupled from per-node link count -- see #54. Ground
+ # truth confirmed against the installed plugin's real module.js
+ # (not the #48 schema doc, which doesn't cover this): height is
+ # `fontSize + 2*padding.vertical` when compactVerticalLinks is
+ # true, unconditionally; when false, it's the larger of that or
+ # a term proportional to max(anchors[Left].numLinks,
+ # anchors[Right].numLinks) -- exactly why campus-leaf1 (3 links)
+ # and campus-leaf2 (4 links) rendered at different heights.
+ # Width is unaffected either way: it's always recomputed from
+ # the label text at render time (no override field exists), and
+ # useConstantSpacing only pulls in Top/Bottom anchor link count,
+ # which this generator never uses (links always attach
+ # Right/Left -- see anchor_counts above).
+ "compactVerticalLinks": True,
+ "padding": {"horizontal": 12, "vertical": 6},
+ "colors": dict(NODE_COLORS),
+ "nodeIcon": None,
+ "statusQuery": f"STATUS {host}",
+ "nodeStatusColorTarget": "border",
+ "statusValueMappings": [dict(m) for m in STATUS_VALUE_MAPPINGS],
+ "anchors": {a: {"numLinks": anchor_counts[host][a], "numFilledLinks": 0} for a in ANCHOR.values()},
+ }
+ if host in vtep_hosts:
+ node["tooltipMetrics"] = [
+ {"label": f"VNI {host} {vlan}", "query": f"VNI {host} {vlan}", "units": "MACs"}
+ for _, vlan in sorted(v for v in vtep_vlan_pairs if v[0] == host)
+ ]
+ nodes.append(node)
+
+ # -- links --
+ link_defs = []
+ interface_regex_parts = set()
+ for link in links:
+ a_gnmic_int = resolve_and_check(link["a_host"], link["a_int"], "side A")
+ z_gnmic_int = resolve_and_check(link["z_host"], link["z_int"], "side Z")
+ interface_regex_parts.add(promql_escape(a_gnmic_int))
+ interface_regex_parts.add(promql_escape(z_gnmic_int))
+
+ a_bw = interface_speeds.get((link["a_host"], link["a_int"]), DEFAULT_LINK_BANDWIDTH_BPS)
+ z_bw = interface_speeds.get((link["z_host"], link["z_int"]), DEFAULT_LINK_BANDWIDTH_BPS)
+ if (link["a_host"], link["a_int"]) not in interface_speeds:
+ mismatches.append(f"{link['a_host']} {link['a_int']}: no IPFabric speed, defaulting bandwidth to {DEFAULT_LINK_BANDWIDTH_BPS}")
+ if (link["z_host"], link["z_int"]) not in interface_speeds:
+ mismatches.append(f"{link['z_host']} {link['z_int']}: no IPFabric speed, defaulting bandwidth to {DEFAULT_LINK_BANDWIDTH_BPS}")
+
+ link_defs.append({
+ "id": f"{link['a_host']}-{a_gnmic_int}--{link['z_host']}-{z_gnmic_int}",
+ "nodes": [{"id": link["a_host"]}, {"id": link["z_host"]}],
+ # Each side's query is that side's own tx (egress) counter, not the
+ # far end's rx -- see #57. a_host tx and z_host rx both describe the
+ # *same* A->Z flow measured from opposite ends (the a_host->z_host
+ # direction, counted twice), leaving the Z->A direction never
+ # queried by either side. Using each node's own tx gives two
+ # independent, opposite-direction measurements instead.
+ "sides": {
+ "A": {
+ "bandwidth": a_bw,
+ "query": f"{link['a_host']} {a_gnmic_int} tx",
+ "labelOffset": 55, "anchor": ANCHOR["Right"], "dashboardLink": "",
+ },
+ "Z": {
+ "bandwidth": z_bw,
+ "query": f"{link['z_host']} {z_gnmic_int} tx",
+ "labelOffset": 55, "anchor": ANCHOR["Left"], "dashboardLink": "",
+ },
+ },
+ "units": "bps",
+ "arrows": {"width": 8, "height": 10, "offset": 2},
+ "stroke": 5,
+ "showThroughputPercentage": False,
+ })
+
+ interface_regex = "|".join(sorted(interface_regex_parts))
+
+ # -- targets (one query per metric family, per task spec) --
+ #
+ # Node status (refId A): a device is "up" only if at least one interface
+ # is up AND every BGP session is up, if it has any -- see #49. The old
+ # "BGP {{device}}" query sourced raw per-neighbor session-state series,
+ # so a device with multiple neighbors collided on one legend string and
+ # only one arbitrarily won. `min by (device)` picks the *worst* session
+ # (0 beats 1), and devices with zero BGP sessions (access switches) are
+ # not penalized: the `or` fallback substitutes a constant 1 for any
+ # device present in device-up but absent from the BGP series entirely.
+ device_up = f'min by (device) (interfaces_interface_state_oper_status{{device=~"{host_regex}"}})'
+ bgp_worst = (
+ f'min by (device) (network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state'
+ f'{{network_instance_name="default", device=~"{host_regex}"}})'
+ )
+ bgp_ok_or_not_applicable = f'({bgp_worst} or ({device_up} * 0 + 1))'
+ targets = [
+ {
+ "refId": "A",
+ "expr": f'{device_up} * {bgp_ok_or_not_applicable}',
+ "legendFormat": "STATUS {{device}}",
+ },
+ {
+ "refId": "B",
+ "expr": f'rate(interfaces_interface_state_counters_out_octets{{device=~"{host_regex}", interface=~"{interface_regex}"}}[5m]) * 8',
+ "legendFormat": "{{device}} {{interface}} tx",
+ },
+ ]
+ if vtep_hosts:
+ vtep_regex = "|".join(promql_escape(h) for h in vtep_hosts)
+ # Verbatim join from #44 "VXLAN (MAC count per VNI)", scoped to VTEP nodes.
+ # RHS is wrapped in max by (device, vlan) so the join key is always
+ # unique -- a Prometheus restart or relabel change otherwise leaves
+ # the pre-restart (frozen) and post-restart series briefly coexisting
+ # within the 5m staleness window, both matching the same (device,
+ # vlan) group, which trips PromQL's "many-to-many matching not
+ # allowed" error. See #50.
+ targets.append({
+ "refId": "D",
+ "expr": (
+ f'count by (device, vlan) (network_instances_network_instance_fdb_mac_table_entries_entry_vlan{{device=~"{vtep_regex}"}})\n'
+ f'* on(device, vlan) group_left(vlan_to_vni_state_vni)\n'
+ f'max by (device, vlan) (interfaces_interface_arista_vxlan_vlan_to_vnis_vlan_to_vni_state_vni{{device=~"{vtep_regex}"}})'
+ ),
+ "legendFormat": "VNI {{device}} {{vlan}}",
+ })
+
+ weathermap = {
+ "version": WEATHERMAP_SCHEMA_VERSION,
+ "id": "evpn-vxlan-fabric-weathermap",
+ "nodes": nodes,
+ "links": link_defs,
+ "scale": [
+ {"percent": 0, "color": "#5794F2"},
+ {"percent": 70, "color": "#FA6400"},
+ {"percent": 90, "color": "#C4162A"},
+ ],
+ "settings": {
+ "panel": {"backgroundColor": "#212124", "panelSize": {"width": 1600, "height": 1000},
+ "zoomScale": 0, "offset": {"x": 0, "y": 0}, "showTimestamp": True,
+ "grid": {"enabled": False, "size": 10, "guidesEnabled": False}},
+ "link": {"spacing": {"horizontal": 10, "vertical": 5}, "stroke": {"color": "#CCCCDC"},
+ "label": {"background": "#FFFFFF", "border": "#000000", "font": "#000000"},
+ "showAllWithPercentage": False, "defaultUnits": "bps"},
+ "tooltip": {"fontSize": 10, "textColor": "#CCCCDC", "backgroundColor": "#1A1B1F",
+ "inboundColor": "#73BF69", "outboundColor": "#5794F2", "scaleToBandwidth": False},
+ "fontSizing": {"node": 12, "link": 10},
+ "scale": {"position": {"x": 0, "y": 0}, "size": {"width": 150, "height": 100},
+ "title": "Utilization", "fontSizing": {"title": 10, "threshold": 9}},
+ },
+ }
+ return weathermap, targets
+
+
+# --------------------------------------------------------------------------
+# Weathermap panel (only) -- see #52, scope narrowed from full-dashboard
+# --------------------------------------------------------------------------
+
+WEATHERMAP_SLOT_TITLE = "__WEATHERMAP_SLOT__"
+WEATHERMAP_PANEL_TITLE = "Fabric Weathermap" # what the slot is renamed to on merge
+
+
+def build_weathermap_panel(weathermap, targets, datasource_uid, plugin_id):
+ """The weathermap panel's own content: type/datasource/targets/options.
+ Deliberately has no gridPos/id/title of its own -- those come from
+ whatever slot it's merged into (see merge_weathermap_into_base)."""
+ return {
+ "type": plugin_id,
+ "datasource": {"type": "prometheus", "uid": datasource_uid},
+ "targets": [dict(t, datasource={"type": "prometheus", "uid": datasource_uid}) for t in targets],
+ "options": {"weathermap": weathermap},
+ }
+
+
+# --------------------------------------------------------------------------
+# Manual base dashboard + merge (see #52)
+# --------------------------------------------------------------------------
+
+def load_base_dashboard(path):
+ with open(path) as f:
+ return json.load(f)
+
+
+def substitute_placeholders(obj, replacements):
+ """Recursively replace exact-match string placeholders (e.g. the
+ datasource UID token) anywhere in the manually-authored base JSON."""
+ if isinstance(obj, dict):
+ return {k: substitute_placeholders(v, replacements) for k, v in obj.items()}
+ if isinstance(obj, list):
+ return [substitute_placeholders(v, replacements) for v in obj]
+ if isinstance(obj, str) and obj in replacements:
+ return replacements[obj]
+ return obj
+
+
+def merge_weathermap_into_base(base_dashboard, weathermap_panel, dashboard_uid):
+ """Splice the freshly generated weathermap panel into the base
+ dashboard's reserved slot (matched by title), keeping the slot's
+ gridPos/id -- layout stays manual, the generator never repositions it."""
+ dashboard = dict(base_dashboard)
+ dashboard["uid"] = dashboard_uid
+ panels = []
+ found = False
+ for panel in dashboard.get("panels", []):
+ if panel.get("title") == WEATHERMAP_SLOT_TITLE:
+ found = True
+ merged = dict(panel)
+ merged.update(weathermap_panel)
+ merged["title"] = WEATHERMAP_PANEL_TITLE
+ panels.append(merged)
+ else:
+ panels.append(panel)
+ if not found:
+ sys.exit(f"Base dashboard has no panel titled {WEATHERMAP_SLOT_TITLE!r} -- nowhere to merge the weathermap panel")
+ dashboard["panels"] = panels
+ return dashboard
+
+
+def fetch_live_node_positions(grafana_url, api_token, dashboard_uid):
+ """Node positions as currently provisioned in Grafana, keyed by node id
+ -- see #53. Position is edited live via drag-and-drop in the Grafana UI,
+ not in the git-committed base file, so it's the one weathermap field that
+ must be sourced from live Grafana state rather than regenerated or read
+ from the manual base -- a manual repositioning must survive every rerun.
+
+ Returns {} if the dashboard doesn't exist yet (first-ever run) or has no
+ weathermap panel yet -- everything falls back to the default layered
+ layout in that case. Any other HTTP error is treated as a real
+ misconfiguration (e.g. a bad token) and raised, rather than silently
+ treated as "no dashboard" -- that would risk quietly discarding every
+ manual position on a run that should have failed loudly instead."""
+ req = urllib.request.Request(
+ f"{grafana_url.rstrip('/')}/api/dashboards/uid/{dashboard_uid}",
+ headers={"Authorization": f"Bearer {api_token}"},
+ method="GET",
+ )
+ try:
+ with urllib.request.urlopen(req, timeout=30) as resp:
+ payload = json.load(resp)
+ except urllib.error.HTTPError as e:
+ if e.code == 404:
+ return {}
+ sys.exit(f"Fetching live dashboard for position preservation failed: HTTP {e.code} {e.read().decode()}")
+
+ for panel in payload.get("dashboard", {}).get("panels", []):
+ if panel.get("title") == WEATHERMAP_PANEL_TITLE:
+ nodes = panel.get("options", {}).get("weathermap", {}).get("nodes", [])
+ return {n["id"]: n["position"] for n in nodes if "position" in n}
+ return {}
+
+
+def provision_to_grafana(grafana_url, api_token, dashboard_payload):
+ req = urllib.request.Request(
+ f"{grafana_url.rstrip('/')}/api/dashboards/db",
+ data=json.dumps(dashboard_payload).encode(),
+ headers={"Authorization": f"Bearer {api_token}", "Content-Type": "application/json"},
+ method="POST",
+ )
+ try:
+ with urllib.request.urlopen(req, timeout=30) as resp:
+ return json.load(resp)
+ except urllib.error.HTTPError as e:
+ sys.exit(f"Grafana provisioning failed: HTTP {e.code} {e.read().decode()}")
+
+
+# --------------------------------------------------------------------------
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument("--base", default="configs/grafana/dashboard-base.json",
+ help="manually-authored dashboard base JSON (everything but the weathermap panel)")
+ parser.add_argument("--output", default="configs/grafana/weathermap-dashboard.json",
+ help="where to write the merged dashboard-as-code JSON (build artifact)")
+ parser.add_argument("--provision", action="store_true",
+ help="also POST the merged dashboard to the Grafana API (requires GRAFANA_* env vars)")
+ args = parser.parse_args()
+
+ ipfabric_url = env("IPFABRIC_URL", required=True)
+ ipfabric_token = env("IPFABRIC_TOKEN", required=True)
+ snapshot = env("IPFABRIC_SNAPSHOT", "$last")
+ prometheus_url = env("PROMETHEUS_URL", "http://172.16.0.71:9090")
+ dashboard_uid = env("GRAFANA_DASHBOARD_UID", "evpn-vxlan-fabric-weathermap")
+ datasource_uid = env("GRAFANA_DATASOURCE_UID", "PROMETHEUS_DATASOURCE_UID_PLACEHOLDER")
+ plugin_id = env("GRAFANA_WEATHERMAP_PLUGIN_ID", "tamirsuliman-weathermap-panel")
+ grafana_url = env("GRAFANA_URL")
+ grafana_token = env("GRAFANA_TOKEN")
+
+ print(f"Fetching devices from IPFabric ({ipfabric_url}, snapshot={snapshot})...")
+ devices = fetch_devices(ipfabric_url, ipfabric_token, snapshot)
+ print(f" {len(devices)} devices")
+
+ print("Fetching connectivity-matrix...")
+ matrix = fetch_connectivity_matrix(ipfabric_url, ipfabric_token, snapshot)
+ links = dedupe_links(matrix)
+ print(f" {len(links)} fabric links after Management-plane filter + dedup ({len(matrix)} raw rows)")
+
+ print("Fetching interface speeds...")
+ interface_speeds = fetch_interface_speeds(ipfabric_url, ipfabric_token, snapshot)
+
+ mismatches = []
+ default_positions = build_layout(devices, mismatches)
+
+ # Position specifically is edited live in the Grafana UI (drag-and-drop),
+ # not in the git-committed base file -- see #53. Reuse whatever's live
+ # for nodes that already exist there; only brand-new nodes get the
+ # layered default. Iterating over default_positions (this run's device
+ # set) rather than the live map means a removed device's stale live
+ # position is simply never looked up again, no orphaned entry survives.
+ if grafana_url and grafana_token:
+ print(f"Fetching live node positions from {grafana_url} (preserve manual repositioning)...")
+ live_positions = fetch_live_node_positions(grafana_url, grafana_token, dashboard_uid)
+ print(f" {len(live_positions)} node(s) with an existing live position")
+ else:
+ print("GRAFANA_URL/GRAFANA_TOKEN not set -- skipping live position fetch, using layered default for all nodes", file=sys.stderr)
+ live_positions = {}
+ positions = {host: live_positions.get(host, default) for host, default in default_positions.items()}
+
+ print(f"Cross-checking interface names against live exporter ({prometheus_url})...")
+ weathermap, targets = build_weathermap(devices, links, interface_speeds, positions, prometheus_url, mismatches)
+
+ if mismatches:
+ print(f"\n{len(mismatches)} mismatch(es) found (link/position kept, not dropped):", file=sys.stderr)
+ for m in mismatches:
+ print(f" - {m}", file=sys.stderr)
+ print(file=sys.stderr)
+
+ weathermap_panel = build_weathermap_panel(weathermap, targets, datasource_uid, plugin_id)
+
+ print(f"Loading manual dashboard base ({args.base})...")
+ base_dashboard = load_base_dashboard(args.base)
+ base_dashboard = substitute_placeholders(base_dashboard, {"__DATASOURCE_UID__": datasource_uid})
+ dashboard = merge_weathermap_into_base(base_dashboard, weathermap_panel, dashboard_uid)
+ dashboard_payload = {"dashboard": dashboard, "overwrite": True}
+
+ os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True)
+ with open(args.output, "w") as f:
+ json.dump(dashboard_payload, f, indent=2)
+ f.write("\n")
+ print(f"Wrote {args.output} ({len(weathermap['nodes'])} nodes, {len(weathermap['links'])} links, "
+ f"{len(dashboard['panels'])} panels total)")
+
+ if args.provision:
+ if not (grafana_url and grafana_token):
+ sys.exit("Refusing to provision: GRAFANA_URL and GRAFANA_TOKEN must both be set")
+ if datasource_uid == "PROMETHEUS_DATASOURCE_UID_PLACEHOLDER":
+ sys.exit("Refusing to provision: set GRAFANA_DATASOURCE_UID to the real Prometheus datasource UID first")
+ print(f"Provisioning dashboard '{dashboard_uid}' to {grafana_url}...")
+ result = provision_to_grafana(grafana_url, grafana_token, dashboard_payload)
+ print(f" {result.get('status')}: {result.get('url')}")
+
+
+if __name__ == "__main__":
+ main()