Files
arista-evpn-vxlan-clab/scripts/README.md
Damien Arnodo fcd7ea2567 Layered default layout by hostname role + preserve live node positions
Default node position for spine/core/border-leaf/leaf/access is now a
Y-tier by role (parsed from hostname, same trust level as the site
parsing from #49) with X grouped by site within each tier, replacing
the flat site-grid heuristic.

Position specifically is now read from the live Grafana dashboard before
computing anything, so a node manually drag-and-dropped in the UI keeps
its position across regeneration -- only nodes with no live position yet
(new devices, or first-ever run) get the layered default. Devices removed
from the topology are simply absent from this run's output, so no
orphaned position/link reference is possible.

Refs #53
2026-07-10 10:40:42 +00:00

132 lines
9.0 KiB
Markdown

# scripts/generate_weathermap.py
Generates a [weathermap-ng](https://github.com/allamiro/grafana-network-weathermap-ng)
**panel only** from live IPFabric topology + gnmic/Prometheus metrics, merges
it into a manually-authored dashboard base, and writes the merged result to
`configs/grafana/weathermap-dashboard.json` (committed to Gitea as the build
artifact, same pattern as the retired Flow Panel YAML). Optionally provisions
it into Grafana via the HTTP API.
## File structure (see #52)
| File | What it is |
|---|---|
| `configs/grafana/dashboard-base.json` | **Manually authored**, committed source of truth for everything except the weathermap panel: BGP sessions table, ports/interfaces table, throughput-per-site graphs, `site`/`device` template variables, panel layout. Edit this directly for anything in those panels. Contains a reserved slot panel titled `__WEATHERMAP_SLOT__` (id `1`, `gridPos` fixed) that the script splices the generated weathermap panel into. |
| `configs/grafana/weathermap-dashboard.json` | **Generated build artifact** — base + freshly-generated weathermap panel, merged. This is what actually gets provisioned to Grafana. Don't hand-edit; re-run the script. |
The script itself only ever knows about the weathermap panel (`targets` +
`options.weathermap`) — it has no knowledge of the other three panels or the
template variables. That split is deliberate: topology-driven content
(nodes/links, generated from live IPFabric+Prometheus data) is separated from
hand-tuned dashboard composition (table layout, transformations, thresholds),
which doesn't need to regenerate on every topology change.
See issue #48 for the panel schema research this is built against, and its
comment thread for the live-validation history (auth quirks, plugin id, a
PromQL escaping bug, a panel-crash fix — worth reading before touching the
anchor/regex-escaping logic).
## Usage
```bash
export IPFABRIC_URL=https://<ipfabric-instance>
export IPFABRIC_TOKEN=<token>
python3 scripts/generate_weathermap.py # writes the merged JSON only
export GRAFANA_URL=https://<external-grafana-instance>
export GRAFANA_TOKEN=<token>
export GRAFANA_DATASOURCE_UID=<prometheus-datasource-uid-in-grafana>
python3 scripts/generate_weathermap.py --provision # also provisions via the Grafana API
```
Re-run after any topology change (`evpn-lab.clab.yml`) to regenerate the
weathermap panel and re-merge/re-provision. The script always regenerates the
weathermap panel fresh from live IPFabric data (no diffing/incremental
state), so a plain re-run already picks up any topology change — there's no
separate "detect changes" step. The only manual/operational step is
*triggering* that re-run after a change; there's no watcher or CI hook doing
it automatically yet.
If you need to change the BGP/ports/throughput panels, template variables, or
layout, edit `configs/grafana/dashboard-base.json` directly and re-run the
script (or run it with `--provision` to push the edit live) — no topology
data is needed for that path, the weathermap panel just gets regenerated
alongside it.
## Environment variables
| Variable | Required | Default | Notes |
|---|---|---|---|
| `--base` (CLI flag, not env) | no | `configs/grafana/dashboard-base.json` | Manual dashboard base to merge the weathermap panel into. |
| `IPFABRIC_URL` | yes | — | e.g. `https://ipfabric.example.com` |
| `IPFABRIC_TOKEN` | yes | — | Inventory/Snapshots read access. Sent as `X-API-Token`, not `Authorization: Bearer` — IPFabric's REST API does not use bearer auth. |
| `IPFABRIC_SNAPSHOT` | no | `$last` | |
| `PROMETHEUS_URL` | no | `http://172.16.0.71:9090` | The **in-topology** Prometheus (see #45), used at generation time to validate the IPFabric→gnmic interface alias and to discover live VTEP/VLAN pairs. This does not have to be the same instance Grafana queries at render time — see below. |
| `GRAFANA_URL` | no (see #53) | — | Required for `--provision`. Also used, if set, to fetch the currently-provisioned dashboard and preserve any manually drag-and-drop-repositioned node positions — see step 3 below. Without it, every node gets the layered default position, even ones a human previously repositioned in the UI. |
| `GRAFANA_TOKEN` | no (see #53) | — | Grafana Service Account token, sent as `Authorization: Bearer`. Same conditions as `GRAFANA_URL` above. |
| `GRAFANA_DATASOURCE_UID` | with `--provision` | — | UID of the Prometheus datasource in Grafana that will actually back the panel. **This must be a datasource that can see the gnmic metrics** — an unrelated/external Prometheus instance with no gnmic data will make the panel render with no values (this happened once, see #48). |
| `GRAFANA_DASHBOARD_UID` | no | `evpn-vxlan-fabric-weathermap` | |
| `GRAFANA_WEATHERMAP_PLUGIN_ID` | no | `tamirsuliman-weathermap-panel` | The installed weathermap-ng plugin id. Override if a different fork is installed — plugin ids don't always match the upstream repo name (the schema research in #48 was done against the `allamiro` fork's source; what's actually installed here is a different fork with a stricter/different runtime schema — see the `ANCHOR` handling in the script). |
## What the script does
1. Fetches device inventory + connectivity-matrix from IPFabric.
2. Filters the connectivity-matrix to physical Ethernet-to-Ethernet links
only: drops Management-plane neighbor entries and `.100`/`.200`
subinterface rows (802.1Q tags used for the gold VRF stitching on Core —
they ride the same physical port as their parent interface and gnmic only
exports physical interface counters), and dedupes the two directions
IPFabric reports for each physical link into one.
3. Computes node positions (see #53):
- **Default layered layout**, for nodes with no known position yet: role
is parsed from the hostname naming convention (`*-spine*`→spine,
`core*`→core, `*-border-leaf*`→border-leaf, `*-leaf*`→leaf,
`*-access*`→access — same trust level as the `site` parsing used for
the Prometheus relabel, not IPFabric-derived), giving Y-tiers top to
bottom `spine → core → border-leaf → leaf → access`. X position is
grouped/columned by site within each tier, so e.g. dc leafs and campus
leafs land in the same row but different column bands. A hostname
matching no role pattern is logged (not silently placed in the wrong
tier) and put in a fallback tier below `access`.
- **Live-position preservation**: position is edited by drag-and-drop
directly in the Grafana UI, not in the git-committed base file — a
deliberate exception to the "file is the only source of truth" rule
from #52, because that's simply not where this field is edited. Before
computing the default layout above, the script fetches the currently
provisioned dashboard's weathermap panel (if `GRAFANA_URL`/`GRAFANA_TOKEN`
are set) and reuses each existing node's live position as-is. Only
genuinely new nodes (not in that live map — first-ever run, or a fresh
device added to the topology) get the layered default. A device removed
from the topology simply has no entry in this run's node/link output at
all, so no orphaned position or dangling link reference is possible.
4. Builds the panel's `targets`: one PromQL query per metric family (BGP
status, interface tx, interface rx, VXLAN MAC/VNI), each with an explicit
`legendFormat` so the resolved display name is predictable.
5. Builds `nodes[]` and `links[]` referencing those resolved legend strings,
including the plugin's `anchors` tally (per-node count of link
attachments per side) and numeric anchor enum on each link side —
omitting these crashes the panel on load in the installed plugin fork,
despite the (fork-specific) schema research saying it's safe to skip.
6. Cross-checks every IPFabric interface name, aliased to gnmic's naming
(`Et``Ethernet`, `Po``Port-Channel`, `Lo``Loopback`, `Vl``Vlan`,
`Ma``Management`), against the live exporter. Any link whose aliased
name has no matching series is logged, not silently dropped — the actual
fix for a real mismatch belongs in gnmic interface aliasing (#43), not in
this script.
7. Loads `configs/grafana/dashboard-base.json`, substitutes the real
Prometheus datasource UID into its `__DATASOURCE_UID__` placeholders,
finds the panel titled `__WEATHERMAP_SLOT__` and splices in the generated
weathermap panel content (keeping the slot's `gridPos`/`id`, so the manual
layout is never repositioned).
8. Writes the merged dashboard JSON to `configs/grafana/weathermap-dashboard.json`,
and provisions it via `POST /api/dashboards/db` if `--provision` is passed.
## Known gaps
- **VXLAN MAC-per-VNI target**: the `vlan` join key used to correlate
VLAN→VNI mapping with FDB entries (query verbatim from #44) doesn't
actually match on live data for any VTEP node — likely an Arista
internal-VLAN-vs-front-panel-VLAN translation the OpenConfig paths don't
reconcile. Only affects the decorative per-VTEP tooltip metric, not
node/link status or traffic coloring. Tracked in #44, not fixed here.