Layered default layout + preserve manually-repositioned nodes across regeneration #53
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follow-up to #52. Two fixes for the weathermap node layout, reported after visual review: the auto-generated layout doesn't follow standard network diagram conventions (spine on top, leaf on bottom, etc.), and any manual repositioning done in the Grafana UI gets destroyed on the next script regeneration.
1. Layered default layout, derived from hostname convention
Same standardization principle already used for the
sitelabel (#49): hostname encodes device role (*-spine*,*-leaf*,*-border-leaf*,*-access*,core*). No new IPFabric field needed — parse role from hostname, same trust level as the site parsing already in place.Default Y-tiers (top to bottom): spine → core → border-leaf → leaf → access. X position: grouped/columned by site within each tier. This replaces whatever grid/cluster heuristic is currently used for brand-new nodes only (see point 2 — existing nodes keep their live position regardless of this change).
2. Preserve manually-repositioned nodes across regeneration
Root cause: the script fully regenerates
node.positionfor every node on every run (declarative pattern from #48/#52), which stomps any manual drag-and-drop repositioning done directly in the Grafana UI.Decision: for position specifically, deviate from #52's "file is the only source of truth" rule. Position is edited live in the Grafana UI by design (drag-and-drop, not JSON editing), so it must be read from live Grafana, not the git-committed manual base file. Everything else from #52 (manual panels sourced from the file, weathermap content otherwise fully regenerated) is unchanged.
New behavior:
node.id → positionfrom what's live right nowpositiongets this preserve-if-existing treatmentTasks
Implemented in
fcd7ea2(branchfeat/telemetry).Role parsing + tiered layout
parse_role(hostname), checked in this order (first match wins, socampus-border-leaf1matches border-leaf before the more general leaf pattern):Y-tiers top to bottom:
spine(0) → core(300) → border-leaf(600) → leaf(900) → access(1200). X grouped by site within each tier (dcat x-offset 100,coreat 1300,campusat 1600, wide enough that the largest group — 8 dc leafs atGRID_COLUMNS=6— doesn't spill into the next site's band), same grid-wrapping as before within a (role, site) group.Ran
parse_roleagainst all 28 live hostnames — all 28 matched cleanly, no fallback-tier placements needed. The fallback path (log + place belowaccess) is implemented and exercised only in the "no unmatched" sense — i.e. confirmed it doesn't fire spuriously on any real hostname, but there's no live example of an actually-unmatched hostname to show, since the naming convention holds fabric-wide.Live-position preservation
fetch_live_node_positions()GETs/api/dashboards/uid/<uid>, finds the panel titled"Fabric Weathermap"(the merged title from #52 — same stable identifier), and returns{node_id: position}. 404 →{}(first-ever run / no dashboard yet). Any other HTTP error (e.g. bad token) is raised, not swallowed — silently falling back to defaults on a real auth/config error would risk quietly overwriting every manual position instead of failing loudly.Confirmed this is the right source vs. #52's file-based rule: position is edited by drag-and-drop directly in the Grafana UI, never in
dashboard-base.json— there's no file representation of it to read from.GRAFANA_URL/GRAFANA_TOKENare no longer provision-only; they're read unconditionally now (optional — skip with a warning if unset, falling back to defaults for everything, rather than hard-requiring Grafana access just to generate a JSON file for inspection).Merge logic:
positions = {host: live_positions.get(host, default) for host, default in default_positions.items()}— iterating the current run's device set (not the live map) means a removed device's stale live-position entry is never looked up again and can't leak into the output; there's structurally no way for it to produce an orphaned position or dangling link reference, since nodes/links are built from live IPFabric data every run regardless.Validation performed
dc-spine1 → [-793, 31],dc-leaf2 → [-1289, 532]) — negative/arbitrary coordinates consistent with actual UI drag-and-drop, not generator output. Reran the generator: both nodes' positions came back byte-identical.dc-leaf1'spositionfrom the live-provisioned dashboard via the API (functionally identical to "a node the live map has never seen": no prior position to preserve). Rerun reported27 node(s) with an existing live position(down from 28), anddc-leaf1landed at[100, 900]— exactlyleaftier (3 × 300 = 900) ×dcsite offset (100) × first alphabetically in its (leaf, dc) group (col=0, row=0). All other 27 nodes' positions were unaffected.Provisioned the corrected dashboard to Grafana;
dc-leaf1now has a sensible permanent position instead of the temporarily-stripped state from the test.feat/telemetryto feat/telemetry