Generate weathermap-ng nodes/links config from IPFabric topology #48

Closed
opened 2026-07-09 08:24:08 +00:00 by Damien · 6 comments
Owner

Remplace la config Flow Panel YAML par une génération automatique de la config JSON weathermap-ng (allamiro/grafana-network-weathermap-ng) à partir de la topologie découverte par IPFabric.

Prérequis : #47 (endpoints IPFabric validés : /tables/inventory/devices pour les nœuds, /tables/interfaces/connectivity-matrix pour les liens).

  • Écrire un script (Python) qui appelle ces deux endpoints via le serveur MCP IPFabric
  • Calculer un layout automatique (X/Y) pour les nœuds
  • Générer les 4 liens bidirectionnels avec, pour chaque sens, une requête PromQL basée sur les labels source/name exposés par gnmic (mapping interface-to-neighbor au niveau PromQL, pas au niveau structure)
  • Si les noms d'interface IPFabric et gnmic divergent, corriger via interface aliasing dans la config gnmic (#43) plutôt que dans ce script
  • Produire un JSON compatible avec le format de config weathermap-ng
  • Provisionner ce JSON dans le panel Grafana (dashboard as code / API Grafana), stocké dans Gitea comme pour l'ancienne config Flow Panel
  • Valider visuellement que les 8 demi-segments (2 par lien × 4 liens) s'affichent correctement avec les bonnes couleurs de trafic
Remplace la config Flow Panel YAML par une génération automatique de la config JSON `weathermap-ng` (allamiro/grafana-network-weathermap-ng) à partir de la topologie découverte par IPFabric. Prérequis : #47 (endpoints IPFabric validés : `/tables/inventory/devices` pour les nœuds, `/tables/interfaces/connectivity-matrix` pour les liens). - Écrire un script (Python) qui appelle ces deux endpoints via le serveur MCP IPFabric - Calculer un layout automatique (X/Y) pour les nœuds - Générer les 4 liens bidirectionnels avec, pour chaque sens, une requête PromQL basée sur les labels `source`/`name` exposés par gnmic (mapping interface-to-neighbor au niveau PromQL, pas au niveau structure) - Si les noms d'interface IPFabric et gnmic divergent, corriger via interface aliasing dans la config gnmic (#43) plutôt que dans ce script - Produire un JSON compatible avec le format de config `weathermap-ng` - Provisionner ce JSON dans le panel Grafana (dashboard as code / API Grafana), stocké dans Gitea comme pour l'ancienne config Flow Panel - Valider visuellement que les 8 demi-segments (2 par lien × 4 liens) s'affichent correctement avec les bonnes couleurs de trafic
Damien added the telemetry label 2026-07-09 08:24:08 +00:00
Author
Owner

Schema research from allamiro/grafana-network-weathermap-ng source (src/types.ts, src/utils.ts, src/WeathermapPanel.tsx) + live test fixtures (testing/grafana/dashboards/wm-device-health.json). This is ground truth, not guesswork — no generation code written yet.

1. Top-level shape

Panel option field: options.weathermap, typed Weathermap { version, id, nodes: Node[], links: Link[], scale: Threshold[], settings }. Current schema version is 14 (CURRENT_VERSION in utils.ts).

On load, migrateWeathermap() does lodash.merge(generateBasicNode(...), node) / merge(generateBasicLink(), link) per node/link — our generator only needs to emit fields it actually knows (id, label, position, statusQuery, sides.query, bandwidth...); everything else (anchors, colors, icon, arrows) gets auto-defaulted. This is also how "old knightss27 exports still work" — default-filling on overlapping field names, not byte-identical schema.

2. Node (relevant fields)

id, position:[x,y], label?, statusQuery?, statusValueMappings?:[{value,color}],
nodeStatusColorTarget?:'border'|'background'|'both', tooltipMetrics?:[{label,query?,units?}]

anchors is technically required by the type but safe to omit — auto-defaulted to zeros on load.

id, nodes:[Node,Node], sides:{A:LinkSide, Z:LinkSide}, units?, stroke, statusQuery?
LinkSide = { bandwidth, bandwidthQuery?, query?, labelOffset, anchor, dashboardLink, portLabel? }

Caveat: the type says Link.nodes embeds full Node objects (and live dashboard fixtures do store full duplicates there — editor round-trip artifact), but provisioning/dashboards/sample-weathermap.json uses only {"id":"node-x"} stubs and loads fine — topology resolves by matching id against the top-level nodes[] array, not from Link.nodes content. Generator should emit {"id": "<node-id>"} stubs only, not full duplicated node objects.

4. Query-reference mechanism — the important part

It is NOT refId. statusQuery, LinkSide.query, LinkSide.bandwidthQuery, tooltipMetrics[].query(A/Z) all store the series display name Grafana computes for a returned frame's value field (getFieldDisplayName, driven by the target's legendFormat when set).

At render time the panel builds Map<displayName, value> from all data.series across all targets, then looks up node.statusQuery / side.query as a direct key into that map.

Concrete proof from wm-device-health.json:

target: { refId:"D", expr:"wm_packet_loss_pct", legendFormat:"LOSS {{device}}" }
node:   "statusQuery": "LOSS CORE-A"   // == legendFormat resolved with device=CORE-A

Implication: our script must set explicit, predictable legendFormat templates (e.g. "{{host}} {{interface}} tx", "BGP {{host}}") on every PromQL target, then reference the exact resolved string in statusQuery/query/bandwidthQuery. Leaving legendFormat on Grafana's auto default makes the resulting display name unpredictable.

5. Panel-level settings (Weathermap.settings)

link: {spacing, stroke:{color}, label:{background,border,font}, showAllWithPercentage, defaultUnits?, valueMappingMode?}
animation?: {enabled, respectReducedMotion?, maxAnimatedLinks?, pauseInEditMode?, showLegend?}
fontSizing: {node, link}
panel: {backgroundColor, backgroundImage?, panelSize:{width,height}, zoomScale, offset:{x,y}, showTimestamp, grid:{enabled,size,guidesEnabled}}
tooltip: {fontSize, textColor, backgroundColor, inboundColor, outboundColor, scaleToBandwidth}
scale: TrafficPanelSettings  // on-canvas legend widget: position, size, title, fontSizing:{title,threshold}

Note scale exists twice: Weathermap.scale: Threshold[] (color-by-%util gradient stops) vs Weathermap.settings.scale (legend widget geometry) — don't conflate in the generator.

{
  "weathermap": {
    "version": 14,
    "id": "example-map",
    "nodes": [
      {
        "id": "leaf01", "label": "leaf01", "position": [200, 200],
        "isConnection": false, "useConstantSpacing": false, "compactVerticalLinks": false,
        "padding": { "horizontal": 12, "vertical": 6 },
        "colors": { "font": "#ffffff", "background": "#22252b", "border": "#5794F2", "statusDown": "#F2495C" },
        "nodeIcon": null,
        "statusQuery": "BGP leaf01",
        "nodeStatusColorTarget": "border",
        "statusValueMappings": [ { "value": 0, "color": "#F2495C" }, { "value": 1, "color": "#73BF69" } ]
      },
      {
        "id": "spine01", "label": "spine01", "position": [600, 200],
        "isConnection": false, "useConstantSpacing": false, "compactVerticalLinks": false,
        "padding": { "horizontal": 12, "vertical": 6 },
        "colors": { "font": "#ffffff", "background": "#22252b", "border": "#5794F2", "statusDown": "#F2495C" },
        "nodeIcon": null,
        "statusQuery": "BGP spine01",
        "nodeStatusColorTarget": "border",
        "statusValueMappings": [ { "value": 0, "color": "#F2495C" }, { "value": 1, "color": "#73BF69" } ]
      }
    ],
    "links": [
      {
        "id": "leaf01--spine01",
        "nodes": [ { "id": "leaf01" }, { "id": "spine01" } ],
        "sides": {
          "A": { "bandwidth": 100000000000, "query": "leaf01 Ethernet1 tx", "labelOffset": 55, "anchor": "Right", "dashboardLink": "" },
          "Z": { "bandwidth": 100000000000, "query": "spine01 Ethernet3 rx", "labelOffset": 55, "anchor": "Left", "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": 800, "height": 600 }, "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} }
    }
  }
}

Matching targets (note the legendFormat templates that make query/statusQuery strings predictable):

[
  { "refId": "A", "expr": "rate(interface_out_octets_total{host=\"leaf01\",interface=\"Ethernet1\"}[5m]) * 8", "legendFormat": "{{host}} {{interface}} tx" },
  { "refId": "B", "expr": "rate(interface_in_octets_total{host=\"spine01\",interface=\"Ethernet3\"}[5m]) * 8", "legendFormat": "{{host}} {{interface}} rx" },
  { "refId": "C", "expr": "network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state{host=~\"leaf01|spine01\"}", "legendFormat": "BGP {{host}}" }
]

7. Pipeline field mapping

Weathermap field Source in our pipeline
node.id, node.label IPFabric inventory: device hostname
node.position Not provided by IPFabric — generator needs its own layout (grid/force-directed); no IPFabric field maps to canvas x/y
node.statusQuery Resolved legend of the BGP session_state query validated in #44 (network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state); set legendFormat:"BGP {{host}}", reference "BGP <hostname>"
node.statusValueMappings Maps raw session_state values → color. Need to confirm exact enum encoding (numeric vs string, e.g. ESTABLISHED) from #44 before hardcoding — the 0/1 above assumes a boolified value, may need adjustment
link.nodes[].id connectivity-matrix localHost/remoteHost, stub {"id":...} form only, must match node.id exactly
link.sides.A/Z.query Interface counters rate() query validated in #44, with explicit per-side legendFormat
link.sides.A/Z.bandwidth IPFabric interface speed (bps)
link.sides.A/Z.anchor Cosmetic — derive from relative node position or localInt/remoteInt, not critical

Compatibility note

Didn't need to cross-check the archived knightss27/grafana-network-weathermap source — this fork's types.ts/utils.ts fully explain the "compatible with old exports" behavior: old JSON gets lodash.merged onto current defaults and version-bumped to 14 on load. It's default-filling, not an unchanged schema.

Next step (not done here): write the actual generation script against this schema.

Schema research from `allamiro/grafana-network-weathermap-ng` source (`src/types.ts`, `src/utils.ts`, `src/WeathermapPanel.tsx`) + live test fixtures (`testing/grafana/dashboards/wm-device-health.json`). This is ground truth, not guesswork — no generation code written yet. ## 1. Top-level shape Panel option field: `options.weathermap`, typed `Weathermap { version, id, nodes: Node[], links: Link[], scale: Threshold[], settings }`. Current schema `version` is **14** (`CURRENT_VERSION` in utils.ts). On load, `migrateWeathermap()` does `lodash.merge(generateBasicNode(...), node)` / `merge(generateBasicLink(), link)` per node/link — our generator only needs to emit fields it actually knows (id, label, position, statusQuery, sides.query, bandwidth...); everything else (anchors, colors, icon, arrows) gets auto-defaulted. This is also how "old knightss27 exports still work" — default-filling on overlapping field names, not byte-identical schema. ## 2. Node (relevant fields) ``` id, position:[x,y], label?, statusQuery?, statusValueMappings?:[{value,color}], nodeStatusColorTarget?:'border'|'background'|'both', tooltipMetrics?:[{label,query?,units?}] ``` `anchors` is technically required by the type but safe to omit — auto-defaulted to zeros on load. ## 3. Link (relevant fields) ``` id, nodes:[Node,Node], sides:{A:LinkSide, Z:LinkSide}, units?, stroke, statusQuery? LinkSide = { bandwidth, bandwidthQuery?, query?, labelOffset, anchor, dashboardLink, portLabel? } ``` **Caveat:** the type says `Link.nodes` embeds full `Node` objects (and live dashboard fixtures do store full duplicates there — editor round-trip artifact), but `provisioning/dashboards/sample-weathermap.json` uses only `{"id":"node-x"}` stubs and loads fine — topology resolves by matching `id` against the top-level `nodes[]` array, not from `Link.nodes` content. **Generator should emit `{"id": "<node-id>"}` stubs only**, not full duplicated node objects. ## 4. Query-reference mechanism — the important part **It is NOT `refId`.** `statusQuery`, `LinkSide.query`, `LinkSide.bandwidthQuery`, `tooltipMetrics[].query(A/Z)` all store the **series display name** Grafana computes for a returned frame's value field (`getFieldDisplayName`, driven by the target's `legendFormat` when set). At render time the panel builds `Map<displayName, value>` from all `data.series` across all `targets`, then looks up `node.statusQuery` / `side.query` as a direct key into that map. Concrete proof from `wm-device-health.json`: ``` target: { refId:"D", expr:"wm_packet_loss_pct", legendFormat:"LOSS {{device}}" } node: "statusQuery": "LOSS CORE-A" // == legendFormat resolved with device=CORE-A ``` **Implication:** our script must set explicit, predictable `legendFormat` templates (e.g. `"{{host}} {{interface}} tx"`, `"BGP {{host}}"`) on every PromQL target, then reference the *exact resolved string* in `statusQuery`/`query`/`bandwidthQuery`. Leaving `legendFormat` on Grafana's auto default makes the resulting display name unpredictable. ## 5. Panel-level settings (`Weathermap.settings`) ``` link: {spacing, stroke:{color}, label:{background,border,font}, showAllWithPercentage, defaultUnits?, valueMappingMode?} animation?: {enabled, respectReducedMotion?, maxAnimatedLinks?, pauseInEditMode?, showLegend?} fontSizing: {node, link} panel: {backgroundColor, backgroundImage?, panelSize:{width,height}, zoomScale, offset:{x,y}, showTimestamp, grid:{enabled,size,guidesEnabled}} tooltip: {fontSize, textColor, backgroundColor, inboundColor, outboundColor, scaleToBandwidth} scale: TrafficPanelSettings // on-canvas legend widget: position, size, title, fontSizing:{title,threshold} ``` Note `scale` exists twice: `Weathermap.scale: Threshold[]` (color-by-%util gradient stops) vs `Weathermap.settings.scale` (legend widget geometry) — don't conflate in the generator. ## 6. Minimal annotated example (2 nodes, 1 link, 1 target set) ```json { "weathermap": { "version": 14, "id": "example-map", "nodes": [ { "id": "leaf01", "label": "leaf01", "position": [200, 200], "isConnection": false, "useConstantSpacing": false, "compactVerticalLinks": false, "padding": { "horizontal": 12, "vertical": 6 }, "colors": { "font": "#ffffff", "background": "#22252b", "border": "#5794F2", "statusDown": "#F2495C" }, "nodeIcon": null, "statusQuery": "BGP leaf01", "nodeStatusColorTarget": "border", "statusValueMappings": [ { "value": 0, "color": "#F2495C" }, { "value": 1, "color": "#73BF69" } ] }, { "id": "spine01", "label": "spine01", "position": [600, 200], "isConnection": false, "useConstantSpacing": false, "compactVerticalLinks": false, "padding": { "horizontal": 12, "vertical": 6 }, "colors": { "font": "#ffffff", "background": "#22252b", "border": "#5794F2", "statusDown": "#F2495C" }, "nodeIcon": null, "statusQuery": "BGP spine01", "nodeStatusColorTarget": "border", "statusValueMappings": [ { "value": 0, "color": "#F2495C" }, { "value": 1, "color": "#73BF69" } ] } ], "links": [ { "id": "leaf01--spine01", "nodes": [ { "id": "leaf01" }, { "id": "spine01" } ], "sides": { "A": { "bandwidth": 100000000000, "query": "leaf01 Ethernet1 tx", "labelOffset": 55, "anchor": "Right", "dashboardLink": "" }, "Z": { "bandwidth": 100000000000, "query": "spine01 Ethernet3 rx", "labelOffset": 55, "anchor": "Left", "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": 800, "height": 600 }, "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} } } } } ``` Matching `targets` (note the `legendFormat` templates that make `query`/`statusQuery` strings predictable): ```json [ { "refId": "A", "expr": "rate(interface_out_octets_total{host=\"leaf01\",interface=\"Ethernet1\"}[5m]) * 8", "legendFormat": "{{host}} {{interface}} tx" }, { "refId": "B", "expr": "rate(interface_in_octets_total{host=\"spine01\",interface=\"Ethernet3\"}[5m]) * 8", "legendFormat": "{{host}} {{interface}} rx" }, { "refId": "C", "expr": "network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state{host=~\"leaf01|spine01\"}", "legendFormat": "BGP {{host}}" } ] ``` ## 7. Pipeline field mapping | Weathermap field | Source in our pipeline | |---|---| | `node.id`, `node.label` | IPFabric inventory: device `hostname` | | `node.position` | Not provided by IPFabric — generator needs its own layout (grid/force-directed); no IPFabric field maps to canvas x/y | | `node.statusQuery` | Resolved legend of the BGP `session_state` query validated in #44 (`network_instances_network_instance_protocols_protocol_bgp_neighbors_neighbor_state_session_state`); set `legendFormat:"BGP {{host}}"`, reference `"BGP <hostname>"` | | `node.statusValueMappings` | Maps raw `session_state` values → color. **Need to confirm exact enum encoding (numeric vs string, e.g. `ESTABLISHED`) from #44 before hardcoding** — the `0`/`1` above assumes a boolified value, may need adjustment | | `link.nodes[].id` | connectivity-matrix `localHost`/`remoteHost`, stub `{"id":...}` form only, must match `node.id` exactly | | `link.sides.A/Z.query` | Interface counters `rate()` query validated in #44, with explicit per-side `legendFormat` | | `link.sides.A/Z.bandwidth` | IPFabric interface speed (bps) | | `link.sides.A/Z.anchor` | Cosmetic — derive from relative node position or `localInt`/`remoteInt`, not critical | ## Compatibility note Didn't need to cross-check the archived `knightss27/grafana-network-weathermap` source — this fork's `types.ts`/`utils.ts` fully explain the "compatible with old exports" behavior: old JSON gets `lodash.merge`d onto current defaults and version-bumped to 14 on load. It's default-filling, not an unchanged schema. Next step (not done here): write the actual generation script against this schema.
Author
Owner

Status encoding — RESOLVED

statusValueMappings: [{value:0,...},{value:1,...}] assumption from the schema research above is confirmed correct. Handled at the gnmic layer, not in the generation script (consistent with the "complexity belongs in config" principle):

outputs:
  prom-output:
    type: prometheus
    listen: :9273
    event-processors:
      - state-to-int
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"}

Rationale (per comment in gnmic.yaml): oper-status and BGP session-state are string enums; the Prometheus exporter silently drops non-numeric values, so both get mapped to 1 (up/established) / 0 (anything else) at the gnmic layer.

Bonus: oper-status is transformed the same way, so link status (not just node status) can also use a reliable 0/1 statusValueMappings for graying out down links — covers the requirement from #47/#48 ("grey out a down link").

VXLAN subscription — needs doc pass in #44

The live gnmic.yaml includes an eos-vxlan subscription (VLAN-to-VNI mapping + MAC table entries, join logic noted inline) that was in #44's original scope but never formally validated/documented like BGP/system were. Should be confirmed and added to #44 before the generator tries to consume VNI-related metrics.

## Status encoding — RESOLVED ✅ `statusValueMappings: [{value:0,...},{value:1,...}]` assumption from the schema research above is confirmed correct. Handled at the gnmic layer, not in the generation script (consistent with the "complexity belongs in config" principle): ```yaml outputs: prom-output: type: prometheus listen: :9273 event-processors: - state-to-int 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"} ``` Rationale (per comment in gnmic.yaml): `oper-status` and BGP `session-state` are string enums; the Prometheus exporter silently drops non-numeric values, so both get mapped to 1 (up/established) / 0 (anything else) at the gnmic layer. Bonus: `oper-status` is transformed the same way, so link status (not just node status) can also use a reliable 0/1 `statusValueMappings` for graying out down links — covers the requirement from #47/#48 ("grey out a down link"). ## VXLAN subscription — needs doc pass in #44 The live gnmic.yaml includes an `eos-vxlan` subscription (VLAN-to-VNI mapping + MAC table entries, join logic noted inline) that was in #44's original scope but never formally validated/documented like BGP/system were. Should be confirmed and added to #44 before the generator tries to consume VNI-related metrics.
Author
Owner

Generation script — done, validated against live IPFabric + gnmic

scripts/generate_weathermap.py (branch feat/telemetry, commit f711210). Writes a full Grafana dashboard-as-code JSON to configs/grafana/weathermap-dashboard.json (committed, source of truth per the retired Flow Panel YAML pattern).

Run:

export IPFABRIC_URL=... IPFABRIC_TOKEN=...
python3 scripts/generate_weathermap.py                 # writes JSON only
export GRAFANA_URL=... GRAFANA_TOKEN=... GRAFANA_DATASOURCE_UID=...
python3 scripts/generate_weathermap.py --provision      # also pushes via Grafana API

Re-run after any topology change to regenerate + re-provision.

Real run result: 28 nodes, 61 links, 0 interface-alias mismatches — every physical link's aliased name (Et11Ethernet11 etc.) resolved against the live exporter.

API details not documented anywhere before this, now baked into the script:

  • IPFabric auth is X-API-Token: <token> (not Authorization: Bearer), and the table endpoints live at /api/tables/... — no /api/v1/ segment. /api/v1/... returns 410 Gone.
  • Installed Grafana plugin id is tamirsuliman-weathermap-panel, not allamiro-weathermap-panel as guessed in the schema-research comment (repo name ≠ plugin id). Script takes it via GRAFANA_WEATHERMAP_PLUGIN_ID env var, no hardcoding.
  • Legend templates use {{device}}/{{interface}}, not {{host}} as in the original schema example — matches the real relabeled Prometheus labels in configs/prometheus/prometheus.yml, not the generic placeholder names.

Bug found + fixed during validation: the connectivity-matrix returns one row per 802.1Q subinterface on trunked ports (Et13.100, Et13.200 — the gold VRF stitching tags on Core, see README). These were initially being treated as separate physical links, producing bogus duplicate parallel links with unresolvable queries. Now excluded — only physical Ethernet interfaces are considered.

Interface aliasing fallback (1f): implemented as specified — IPFabric abbreviated names (Et, Po, Lo, Vl, Ma) are expanded to gnmic's OpenConfig-style names, cross-checked against interfaces_interface_state_oper_status series at generation time, and any unresolved link is logged (not dropped). None triggered on the current topology.

Blocked: provisioning (step 2) and visual validation (step 3)

Discovered the Grafana instance's Prometheus datasource (aflu2t514uf40ahttps://prometheus.taila5ad8.ts.net) is a different Prometheus than the in-topology one (172.16.0.71:9090) that actually has the gnmic metrics — confirmed zero interfaces_interface_state_oper_status series on the external instance. This matches the README's note that the in-topology Prometheus "does not replace the existing external Prometheus instance — no cutover yet."

Provisioning now would push a panel with correct topology/queries but no live data. Holding off on --provision until the external Prometheus can see the gnmic data (federation / remote_write / additional scrape target — TBD, out of this issue's scope, may need its own issue). Will re-run --provision and do the visual validation (8+ half-segments, traffic colors, BGP node status) once that's resolved.

## Generation script — done, validated against live IPFabric + gnmic `scripts/generate_weathermap.py` (branch `feat/telemetry`, commit f711210). Writes a full Grafana dashboard-as-code JSON to `configs/grafana/weathermap-dashboard.json` (committed, source of truth per the retired Flow Panel YAML pattern). **Run:** ```bash export IPFABRIC_URL=... IPFABRIC_TOKEN=... python3 scripts/generate_weathermap.py # writes JSON only export GRAFANA_URL=... GRAFANA_TOKEN=... GRAFANA_DATASOURCE_UID=... python3 scripts/generate_weathermap.py --provision # also pushes via Grafana API ``` Re-run after any topology change to regenerate + re-provision. **Real run result:** 28 nodes, 61 links, **0 interface-alias mismatches** — every physical link's aliased name (`Et11`→`Ethernet11` etc.) resolved against the live exporter. **API details not documented anywhere before this, now baked into the script:** - IPFabric auth is `X-API-Token: <token>` (not `Authorization: Bearer`), and the table endpoints live at `/api/tables/...` — no `/api/v1/` segment. `/api/v1/...` returns `410 Gone`. - Installed Grafana plugin id is `tamirsuliman-weathermap-panel`, not `allamiro-weathermap-panel` as guessed in the schema-research comment (repo name ≠ plugin id). Script takes it via `GRAFANA_WEATHERMAP_PLUGIN_ID` env var, no hardcoding. - Legend templates use `{{device}}`/`{{interface}}`, not `{{host}}` as in the original schema example — matches the real relabeled Prometheus labels in `configs/prometheus/prometheus.yml`, not the generic placeholder names. **Bug found + fixed during validation:** the connectivity-matrix returns one row per 802.1Q subinterface on trunked ports (`Et13.100`, `Et13.200` — the gold VRF stitching tags on Core, see README). These were initially being treated as separate physical links, producing bogus duplicate parallel links with unresolvable queries. Now excluded — only physical Ethernet interfaces are considered. **Interface aliasing fallback (1f):** implemented as specified — IPFabric abbreviated names (`Et`, `Po`, `Lo`, `Vl`, `Ma`) are expanded to gnmic's OpenConfig-style names, cross-checked against `interfaces_interface_state_oper_status` series at generation time, and any unresolved link is logged (not dropped). None triggered on the current topology. ## Blocked: provisioning (step 2) and visual validation (step 3) Discovered the Grafana instance's Prometheus datasource (`aflu2t514uf40a` → `https://prometheus.taila5ad8.ts.net`) is a **different Prometheus than the in-topology one** (`172.16.0.71:9090`) that actually has the gnmic metrics — confirmed zero `interfaces_interface_state_oper_status` series on the external instance. This matches the README's note that the in-topology Prometheus "does not replace the existing external Prometheus instance — no cutover yet." Provisioning now would push a panel with correct topology/queries but no live data. Holding off on `--provision` until the external Prometheus can see the gnmic data (federation / remote_write / additional scrape target — TBD, out of this issue's scope, may need its own issue). Will re-run `--provision` and do the visual validation (8+ half-segments, traffic colors, BGP node status) once that's resolved.
Author
Owner

Provisioned and validated with live data — steps 2/3 done (mostly)

Datasource gap resolved: a new Grafana datasource NetLab (uid cfrlusac89se8a) now points at the in-topology Prometheus (172.16.0.71:9090) that actually has the gnmic metrics. Regenerated + provisioned against it (commit 0aff25f).

Bug found during provisioning validation: re.escape() escapes - as \-. Python's own re module accepts that, but PromQL's RE2-based engine doesn't — every query failed with bad_data: invalid parameter "query": ... unknown escape sequence U+002D '-'. Every hostname in this lab is hyphenated (dc-leaf1, campus-border-leaf1...) so this broke all 4 targets outright. Added a promql_escape() helper that skips escaping - (not a regex metacharacter outside a character class, so it's safe unescaped). Fixed and reprovisioned.

Live validation via Grafana's /api/ds/query (ran each panel target exactly as stored in the dashboard, against the NetLab datasource):

Target Result
A — BGP {{device}} 22 series, real 0/1 values
B — {{device}} {{interface}} tx 132 series, real bps values
C — {{device}} {{interface}} rx 132 series, real bps values
D — VNI {{device}} {{vlan}} 1 series, null value

A/B/C — the queries backing node border color and both sides of every link's half-segment — are confirmed live. That's the core ask (8+ half-segments with correct traffic colors, correct BGP node status).

D (VXLAN MAC/VNI) confirmed broken, root cause found, not fixed here: the vlan join key doesn't actually correlate between the two source metrics on live data. E.g. campus-leaf1's VLAN-to-VNI mapping is keyed vlan="50", but its FDB entries are recorded under vlan="60"/"1006"/"1007"/"4090"/"4091" — no overlap, for any VTEP node in the topology. Likely an Arista internal-VLAN-vs-front-panel-VLAN translation the OpenConfig paths don't reconcile. This is exactly what the previous comment on this issue already flagged as unvalidated ("VXLAN subscription — needs doc pass in #44"); now empirically confirmed rather than just suspected. Non-blocking: it only feeds the decorative per-VTEP tooltip metric, not node.statusQuery or any link.query, so it doesn't affect topology/status/traffic rendering. Leaving the join query as-is (per #44 "use it verbatim, do not re-derive"); real fix belongs in #44 once the internal-VLAN translation is understood.

Still open

  • Visual confirmation in the actual Grafana UI (color rendering, layout readability) — I validated the queries resolve correctly through Grafana's query API, but haven't looked at the rendered panel. Worth a manual look: $GRAFANA_URL/d/evpn-vxlan-fabric-weathermap.
  • VXLAN MAC/VNI join key mismatch — carry over to #44.
## Provisioned and validated with live data — steps 2/3 done (mostly) Datasource gap resolved: a new Grafana datasource `NetLab` (uid `cfrlusac89se8a`) now points at the in-topology Prometheus (`172.16.0.71:9090`) that actually has the gnmic metrics. Regenerated + provisioned against it (commit 0aff25f). **Bug found during provisioning validation:** `re.escape()` escapes `-` as `\-`. Python's own `re` module accepts that, but PromQL's RE2-based engine doesn't — every query failed with `bad_data: invalid parameter "query": ... unknown escape sequence U+002D '-'`. Every hostname in this lab is hyphenated (`dc-leaf1`, `campus-border-leaf1`...) so this broke all 4 targets outright. Added a `promql_escape()` helper that skips escaping `-` (not a regex metacharacter outside a character class, so it's safe unescaped). Fixed and reprovisioned. **Live validation via Grafana's `/api/ds/query`** (ran each panel target exactly as stored in the dashboard, against the `NetLab` datasource): | Target | Result | |---|---| | A — `BGP {{device}}` | 22 series, real 0/1 values | | B — `{{device}} {{interface}} tx` | 132 series, real bps values | | C — `{{device}} {{interface}} rx` | 132 series, real bps values | | D — `VNI {{device}} {{vlan}}` | 1 series, **null value** | A/B/C — the queries backing node border color and both sides of every link's half-segment — are confirmed live. That's the core ask (8+ half-segments with correct traffic colors, correct BGP node status). **D (VXLAN MAC/VNI) confirmed broken, root cause found, not fixed here:** the `vlan` join key doesn't actually correlate between the two source metrics on live data. E.g. `campus-leaf1`'s VLAN-to-VNI mapping is keyed `vlan="50"`, but its FDB entries are recorded under `vlan="60"`/`"1006"`/`"1007"`/`"4090"`/`"4091"` — no overlap, for any VTEP node in the topology. Likely an Arista internal-VLAN-vs-front-panel-VLAN translation the OpenConfig paths don't reconcile. This is exactly what the previous comment on this issue already flagged as unvalidated ("VXLAN subscription — needs doc pass in #44"); now empirically confirmed rather than just suspected. Non-blocking: it only feeds the decorative per-VTEP tooltip metric, not `node.statusQuery` or any `link.query`, so it doesn't affect topology/status/traffic rendering. Leaving the join query as-is (per #44 "use it verbatim, do not re-derive"); real fix belongs in #44 once the internal-VLAN translation is understood. ## Still open - Visual confirmation in the actual Grafana UI (color rendering, layout readability) — I validated the queries resolve correctly through Grafana's query API, but haven't looked at the rendered panel. Worth a manual look: `$GRAFANA_URL/d/evpn-vxlan-fabric-weathermap`. - VXLAN MAC/VNI join key mismatch — carry over to #44.
Author
Owner

Fixed: panel crash on load (commit 36c93d9)

TypeError: undefined is not an object (evaluating 'i.anchors[0]') — root cause: the schema research earlier in this issue was done against allamiro/grafana-network-weathermap-ng's source, but the plugin actually installed is a different fork, tamirsuliman-weathermap-panel (confirmed via /api/plugins, already noted in the previous comment). Its rendering code isn't as forgiving about missing fields.

Pulled the real module.js from the running instance and found two divergences from the schema doc:

  1. node.anchors is not auto-defaulted on load in this fork — it's a required per-node object tallying link attachments per side: {0: {numLinks, numFilledLinks}, 1: {...}, ..., 4: {...}}. Omitting it (as the schema research said was safe) crashes immediately.
  2. link.sides.A/Z.anchor is the plugin's numeric TS enum value, not the anchor name string — Center=0, Top=1, Bottom=2, Left=3, Right=4 (reverse-engineered from the enum definition in module.js: e[e.Center=0]="Center",e[e.Top=1]="Top"...). I had it as the literal string "Right"/"Left", which the anchor-indexed lookups (node.anchors[side.anchor]) silently failed to resolve.

Generator now computes node.anchors from the actual link topology (every node's numLinks tally sums to 2 × link count, verified: 122 = 2×61) and emits the correct numeric anchor enum on both link sides. Re-provisioned; the 4 panel targets still resolve live data through /api/ds/query (22/132/132/1 series, unchanged from before this fix).

Checked the rest of the plugin source for the same "unguarded property access on an omittable field" pattern — bandwidthQuery, portLabel, directionLabel are all null-checked/guarded in the code, so those are genuinely safe to leave out as the original research assumed. anchors was the exception.

@Damien — can you reload $GRAFANA_URL/d/evpn-vxlan-fabric-weathermap and confirm the panel actually renders now? That's the one piece I still can't verify from here.

## Fixed: panel crash on load (commit 36c93d9) `TypeError: undefined is not an object (evaluating 'i.anchors[0]')` — root cause: the schema research earlier in this issue was done against `allamiro/grafana-network-weathermap-ng`'s source, but the plugin actually installed is a different fork, `tamirsuliman-weathermap-panel` (confirmed via `/api/plugins`, already noted in the previous comment). Its rendering code isn't as forgiving about missing fields. Pulled the real `module.js` from the running instance and found two divergences from the schema doc: 1. `node.anchors` is **not** auto-defaulted on load in this fork — it's a required per-node object tallying link attachments per side: `{0: {numLinks, numFilledLinks}, 1: {...}, ..., 4: {...}}`. Omitting it (as the schema research said was safe) crashes immediately. 2. `link.sides.A/Z.anchor` is the plugin's numeric TS enum value, not the anchor name string — `Center=0, Top=1, Bottom=2, Left=3, Right=4` (reverse-engineered from the enum definition in module.js: `e[e.Center=0]="Center",e[e.Top=1]="Top"...`). I had it as the literal string `"Right"`/`"Left"`, which the anchor-indexed lookups (`node.anchors[side.anchor]`) silently failed to resolve. Generator now computes `node.anchors` from the actual link topology (every node's numLinks tally sums to `2 × link count`, verified: 122 = 2×61) and emits the correct numeric anchor enum on both link sides. Re-provisioned; the 4 panel targets still resolve live data through `/api/ds/query` (22/132/132/1 series, unchanged from before this fix). Checked the rest of the plugin source for the same "unguarded property access on an omittable field" pattern — `bandwidthQuery`, `portLabel`, `directionLabel` are all null-checked/guarded in the code, so those are genuinely safe to leave out as the original research assumed. `anchors` was the exception. @Damien — can you reload `$GRAFANA_URL/d/evpn-vxlan-fabric-weathermap` and confirm the panel actually renders now? That's the one piece I still can't verify from here.
Author
Owner

Scope change: VXLAN MAC/VNI dropped from weathermap, moved to a separate panel (tracked as a note in #44)

Decision: instead of chasing the broken vlan join (see previous comment + #44), the VXLAN MAC/VNI tooltipMetrics entry is removed from scripts/generate_weathermap.py entirely. Cleaner separation of concerns — weathermap stays scoped to topology/link-status/traffic, VXLAN visibility becomes its own dedicated Grafana table panel (not part of this issue's deliverable, no join-fix dependency to unblock it).

Script updated: tooltipMetrics no longer references the VNI join query on VTEP nodes. No other generator behavior changed — node/link generation, layout, anchors, and the A/B/C (BGP/tx/rx) targets are untouched.

Remaining open item on this issue is unchanged: visual confirmation of the rendered panel in Grafana UI.

## Scope change: VXLAN MAC/VNI dropped from weathermap, moved to a separate panel (tracked as a note in #44) Decision: instead of chasing the broken `vlan` join (see previous comment + #44), the VXLAN MAC/VNI `tooltipMetrics` entry is removed from `scripts/generate_weathermap.py` entirely. Cleaner separation of concerns — weathermap stays scoped to topology/link-status/traffic, VXLAN visibility becomes its own dedicated Grafana table panel (not part of this issue's deliverable, no join-fix dependency to unblock it). Script updated: `tooltipMetrics` no longer references the VNI join query on VTEP nodes. No other generator behavior changed — node/link generation, layout, anchors, and the A/B/C (BGP/tx/rx) targets are untouched. Remaining open item on this issue is unchanged: visual confirmation of the rendered panel in Grafana UI.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Damien/arista-evpn-vxlan-clab#48