v0.1.21 Notification templates, MCP read parity (9 new tools), CDP/LLDP neighbour history, configuration bundles, operator-configurable retention
📋 Full release notes: RELEASE_NOTES.md
Images (published by CI to GHCR):
ghcr.io/horryworks/yagra-core:v0.1.21
ghcr.io/horryworks/yagra-poller:v0.1.21
ghcr.io/horryworks/yagra-web:v0.1.21
Breaking changes
- Core no longer starts when
YAGRA_KEK_FILEpoints at a file it cannot read. It used to log an
error and boot on a freshly generated random key — which meant the deployment looked healthy while
every stored monitoring credential had become permanently undecryptable, and nothing said so until
the next poll failed. That silent data-loss path is now a loud startup failure naming the path
(load KEK from <path>). Unset stays unchanged: noYAGRA_KEK_FILEstill means the ephemeral
dev key, so the dev compose is byte-identical. Only a deployment that was already broken is
affected; point the variable at the real key file, or unset it. YAGRA_FLOW_RETENTION_DAYSnow seeds a brand-new deployment only. On an existing one, flow
retention comes from Settings ▸ System settings ▸ Data retention. The env var previously had no
effect at all on an existing ClickHouse volume (see Bug Fixes), so nothing changes on upgrade —
existing deployments keep the 30 days their tables are actually enforcing.
New Features
- Three new MCP tools, and the biggest gap in that surface is closed (ADR-042 increment 2).
/mcpcould open a maintenance window but could not list one back, so an AI client could silence
a node and then report the fleet healthy — having caused the silence itself.list_suppressions— every maintenance window and mute the caller may see, in one answer.
Check it before concluding a fleet is quiet; a silenced fleet looks the same as a healthy one.alert_trends— how alerting has behaved over time:top_nodes(which nodes alert most
often — chronic offenders, which the active-alert list cannot show),transitions(the latest
fires and recoveries), andcalendar(fires bucketed by weekday and hour, for spotting a
nightly pattern).search_analysis_findings— Troubleshoot findings across every run, filtered by node,
folder, diagnostic, severity or time. Distinct from reading one run you already know about.list_analysestakeskind=schedulesto list recurring analyses, andlist_node_groups
takesinclude_stateto return each folder's health tally alongside its name.- MCP remains read-only: no tool was added that changes anything.
- Notification templates (Alerts ▸ Notification routing ▸ a channel ▸ Edit notification
template, ADR-039). Each channel can override the subject and body it sends, written as a Jinja2
template over a fixed set of alert variables. The immediate reason: the built-in subject named the
node by UUID —node 6f1c9d2a-0b3e-4a71-9c8d-2e5f7a1b4c60 is critical— so a template can now
say{{ severity | upper }}: {{ node_name }} ({{ group }})instead. Conditionals work, which is
the usual second request:{% if event == 'resolve' %}Recovered{% endif %}.- Existing channels are unchanged, byte for byte. No template means the wording Yagra has
always sent; upgrading changes no notification. - A broken template never costs you a notification. If it cannot be rendered when an alert
fires — a bad filter, output too large, or a body that stopped being valid JSON on a channel
that sends JSON — that field falls back to the built-in wording and the alert still goes out.
The fallback is per field, so a mistake in the body does not discard a subject that was right.
Each occurrence incrementsyagra_notification_template_errors_total{reason}and is logged. - Preview before you save. A template's first real execution is during an outage, so the
editor renders it against a representative alert and shows exactly what would be sent,
including whether the body parses as JSON. A template that does not compile is refused at save
time rather than at 3am. - Variables available: node name/id/address, group, profile, severity, state, metric, value,
threshold, direction, time, flapping, the root cause when an alert is rolled up, and the dedup
key Yagra sends to PagerDuty/JSM. Credentials are never in scope. Interpolating into a JSON
body wants thetojsonfilter, which the editor's error message tells you. - Editing is Admin-only and audited, and takes effect within ~30 seconds without a restart.
- Not covered: a webhook or SMTP destination configured through
YAGRA_WEBHOOK_URL/
YAGRA_SMTP_*keeps the built-in wording — it has no channel record to attach a template to.
Add it as a channel in the UI to template it.
- Existing channels are unchanged, byte for byte. No template means the wording Yagra has
- The MCP tool surface now answers the questions the WebUI answers (ADR-042). It had drifted
into a subset — 110 read endpoints against 17 tools — and the gaps were the ones a troubleshooting
session hits first. Six new read-only tools, taking the surface to 23:get_interface_series— one interface's in/out throughput and error rates over time.
query_metricsis node-level only, so per-interface history had no tool at all.top_metrics/top_interfaces— fleet-wide rankings. "Which nodes are worst on CPU
right now", and "which links are busiest, most erroring, or moved the most". Previously an AI
client could only read one node's metric at a time.get_neighbors— a node's CDP/LLDP adjacency and its recent changes (ADR-038).list_node_groups— the folder tree, which is also how a caller finds the group id
run_analysis(scope="group")asks for.fleet_throughput— total in/out bits per second across every exporter.top_flowsnow takes an optionalnode_id: omit it for the fleet-wide aggregations that
previously had no tool. Like the REST endpoints, the fleet-wide form is refused for a token
limited to a group, since the rows keep no exporter attribution to narrow.- MCP stays read-only. The write surface is unchanged — acknowledging an alert, opening a
maintenance window, and triggering a poll — and this release adds none. Reading Yagra's own
configuration over MCP still requires the same permission the UI does; read-only does not mean
readable by anyone.
- CDP/LLDP neighbor discovery (node detail ▸ Neighbors, ADR-038). Every SNMP node's LLDP and
CDP tables are walked on a slow cadence, and the result is recorded as what changed, and when —
the tab shows which local port faces which peer right now, plus a timeline of every time that
moved. A history row is written only when the adjacency actually changes: the agent's own
churn (LLDP'sTimeMark, its remote-row renumbering, row ordering) is normalized away, so a rack
nobody is repatching writes nothing at all. Both protocols are normalized onto one model, so a
device running both shows one table and one capability legend.- On by default. After upgrading, each SNMP node gets one extra walk per hour. A device that
speaks neither protocol answers in a single round trip and costs essentially nothing; a
48-port switch returns a few dozen rows. Switch it off, or change the cadence, in
Settings ▸ System settings ▸ Neighbor discovery. Turning it off keeps everything already
recorded. - Read-only, like everything else Yagra does to a device — no configuration is written.
Neighbor data raises no alerts and is deliberately not wired into dependency
suppression: LLDP reports adjacency, which has no direction, and guessing the upstream wrong
would silence a real outage rather than surface it (ADR-015). - New endpoints:
GET /api/v1/nodes/{id}/neighborsand
GET /api/v1/nodes/{id}/neighbors/history(View, node-scoped), plus
GET/PUT /api/v1/settings/neighbors(View / ManageConfig).
- On by default. After upgrading, each SNMP node gets one extra walk per hour. A device that
- Data retention is configurable from the UI (Settings ▸ System settings ▸ Data retention,
ADR-040). Alert-linked data, unmatched events, report runs and traffic flows each get their own
window; changes apply on the next sweep with no restart, and a flow change is applied to
ClickHouse immediately. The card lists every retained subject, including the ones Yagra cannot
change: VictoriaMetrics and VictoriaLogs take retention as a container start flag with no runtime
API, so those rows are read-only and show the value read back from the store's own/flags
endpoint — what it is really enforcing, rather than a number mirrored from configuration. The
audit log is listed as kept indefinitely, by design. GET /api/v1/settings/retention(View) andPUT(ManageConfig) expose the same policy.GET /api/v1/credentials/health(ManageCredentials) reports whether every stored credential
still decrypts under the loaded key. This is the assertion a database restore cannot make on its
own: rows can come back whole while the key-encryption key is a different one.- Configuration bundles (Settings ▸ Configuration bundle,
GET/POST /api/v1/config/bundle,
ADR-040). Export a deployment's monitoring configuration — profiles, metric sets, classification
rules, groups, nodes, thresholds, URL/DNS monitors, forwarding destinations, event sources and
rules, report and analysis schedules — as one JSON file, and apply it to another deployment. This
is for migration, not backup: a bundle carries no credentials, no notification-channel
settings, no ingest tokens and no history. Import is upsert only — nothing is ever deleted and
there is no replace mode — runs in one transaction, and?dry_run=trueperforms the real import
and rolls it back, so its report is exactly what applying would do. The report names every row it
skipped or changed and why: a missing required reference skips the row rather than widening it, a
destination or webhook source that needs a secret arrives disabled, and schedules are recomputed
on the target's clock. Notification channels and routing rules are deliberately not carried —
a channel is its sealed config and no API can attach one to an existing channel id, so an
imported rule would notify nobody, silently. The export refuses rather than truncating when a
table exceeds 10,000 rows; use a database dump for a deployment that size. - A backup procedure that ships as scripts, and a way to prove it works.
scripts/yagra-backup.shtakes the tier-1 set (KEK first, then a fullpg_dump, then a
VictoriaMetrics snapshot) with a manifest;scripts/yagra-restore-verify.shrestores it into a
throwaway stack and asserts/readyz, the node count, the audit-log row count, and that
credentials actually decrypt. ADR-017 has required a backup and rollback path for destructive
migrations since it was written, and until now the repository contained nopg_dumpat all.
Bug Fixes
- The MCP
top_flowsandflow_fanouttools did not clamp their row limit. The REST flow
endpoints cap a request at 1000 rows; the MCP tools had their own copy of that query builder and
it had lost the cap, so an AI client asking for 100000 rows got a ClickHouse query with no bound
on it. Both surfaces now share one window-and-limit rule, so the cap applies wherever the
query comes from. A tool call asking for more than 1000 rows now receives 1000. The default
(100 rows) is unchanged. - A site pin on the Geo map counted only the nodes filed directly in that folder. Nodes
normally live in sub-folders — racks, floors, closets — so an operator who placed their Tokyo
site and filed the switches under Tokyo ▸ Floor 2 ▸ Rack A got a pin that was permanently
green and empty, showing nothing about a site that was on fire. Group coordinates are now
inherited: a folder with none of its own belongs to its nearest placed ancestor, and a pin
counts everything that resolves to it. Both the Geo map page and the dashboard widget change
together, so a site cannot read amber in one and green in the other.- Inheritance does not add pins. Thirty racks under one building stay one pin — thirty
exactly-overlapping ones would only hide the building. The number of pins is still the number
of folders carrying their own coordinates; what changed is what each one counts. - The group dialog now says when a folder is already on the map through its parent, so an empty
pair of coordinate boxes no longer reads as "this site is missing from the map". - API:
GET /api/v1/node-groupsrows gaineffective_latitude,effective_longitude,
geo_source(own/inherited/unset) andgeo_group— the folder whose pin this one
belongs to.latitude/longitudestill mean the folder's own coordinates and are unchanged.
- Inheritance does not add pins. Thirty racks under one building stay one pin — thirty
YAGRA_FLOW_RETENTION_DAYSdid nothing on an existing deployment. The ClickHouse TTL was only
ever emitted insideCREATE TABLE IF NOT EXISTS, which is a no-op once the tables exist, so the
retention an existing volume ran with was whatever it was created with — whileDEPLOYMENT.md
documented the variable as live. Retention changes are now applied withALTER TABLE … MODIFY TTL,
and only when the declared TTL actually differs (issuing it unconditionally would re-mutate every
part on each restart). Lowering the window deletes flow rows older than it, so the change is
logged atwarnwith the old and new values.- The five envelope-encrypted stores each loaded their own key. Credentials, notification
channels, forwarding destinations, OIDC and LLM config called the key loader independently, so on
the ephemeral (unset) path each got a different random key — despite the code's own comment
saying they shared one. They now share a single loaded key.