From 62f6d8e7d50e19ac6aaabdfb180c2afc46efb189 Mon Sep 17 00:00:00 2001 From: Andrew Stoltz Date: Mon, 1 Jun 2026 22:29:18 -0500 Subject: [PATCH] Add SignalControl platform telemetry manifests --- apps/fc-signalcontrol/README.md | 33 +++ .../grafana-dashboard-signalcontrol.yaml | 260 ++++++++++++++++++ apps/monitoring/noc-monitoring.yaml | 19 ++ .../SignalControlPlatformManifestTests.cs | 51 ++++ 4 files changed, 363 insertions(+) create mode 100644 apps/fc-signalcontrol/README.md create mode 100644 apps/monitoring/grafana-dashboard-signalcontrol.yaml create mode 100644 tests/bluejay-infra-lint/SignalControlPlatformManifestTests.cs diff --git a/apps/fc-signalcontrol/README.md b/apps/fc-signalcontrol/README.md new file mode 100644 index 0000000..93c47e7 --- /dev/null +++ b/apps/fc-signalcontrol/README.md @@ -0,0 +1,33 @@ +# FlowerCore SignalControl platform notes + +This app owns the cluster web manager at `signalcontrol.iamworkin.lan` and documents the physical Pi pilot at `signal-a.iamworkin.lan` / `pirelay`. + +## mTLS enrollment pattern + +Do not install or restart anything from this repo. The intended pirelay pattern is the Pi-signage step-ca-agent shape: + +- stable node identity: `pirelay` +- local private key and CSR generated on the node +- CSR submitted through the approved DeviceManagement/step-ca enrollment path +- client certificate and chain stored node-local under `/etc/flowercore/signalcontrol/mtls/` +- daily renewal timer, renewing only when fewer than 30 days remain +- certificate used for DM-agent to DM-web traffic and future SignalControl inter-service calls + +Secrets, enrollment codes, private keys, p12 passphrases, and OIDC client secrets stay out of Git. + +## Telemetry + +Monitoring manifests add a dedicated Prometheus job: + +- `signalcontrol-pi-app` +- target `10.0.58.113:5200` +- path `/metrics/prometheus` +- labels `instance="pirelay"`, `host="signal-a.iamworkin.lan"`, `service="signalcontrol-pi"` + +Host metrics continue through the `edge-nodes` node_exporter target at `10.0.58.113:9100`. + +## Physical-control audit + +The app ships with `FlowerCore:SignalControl:PhysicalAudit:Enabled=false` and `ForwardingEnabled=false`. Enabling local audit creates a SHA-256 hash chain for physical-control mutations. Forwarding to `https://audit.iamworkin.lan/api/v1/audit/signalcontrol` requires flipping the forwarding gate separately. + +Telemetry reads and `/metrics` scrapes are not audited. diff --git a/apps/monitoring/grafana-dashboard-signalcontrol.yaml b/apps/monitoring/grafana-dashboard-signalcontrol.yaml new file mode 100644 index 0000000..f761926 --- /dev/null +++ b/apps/monitoring/grafana-dashboard-signalcontrol.yaml @@ -0,0 +1,260 @@ +# Grafana dashboard ConfigMap for FlowerCore.SignalControl on pirelay. +# +# The Grafana Deployment in noc-monitoring.yaml mounts this ConfigMap at +# /var/lib/grafana/dashboards/signalcontrol. The paired Prometheus jobs are: +# - signalcontrol-pi-app: 10.0.58.113:5200 /metrics/prometheus +# - edge-nodes: 10.0.58.113:9100 with instance="pirelay" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-signalcontrol + namespace: monitoring +data: + signalcontrol.json: | + { + "annotations": { "list": [] }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "green", "value": 1 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 5, "w": 6, "x": 0, "y": 0 }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, + "textMode": "auto" + }, + "targets": [ + { "editorMode": "code", "expr": "up{job=\"signalcontrol-pi-app\",instance=\"pirelay\"}", "range": true, "refId": "A" } + ], + "title": "SignalControl App Up", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "green", "value": 1 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 5, "w": 6, "x": 6, "y": 0 }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, + "textMode": "auto" + }, + "targets": [ + { "editorMode": "code", "expr": "up{job=\"edge-nodes\",instance=\"pirelay\"}", "range": true, "refId": "A" } + ], + "title": "pirelay node_exporter Up", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] }, + "gridPos": { "h": 5, "w": 6, "x": 12, "y": 0 }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, + "textMode": "name" + }, + "targets": [ + { "editorMode": "code", "expr": "signalcontrol_active_pattern{job=\"signalcontrol-pi-app\",instance=\"pirelay\"}", "legendFormat": "{{pattern}}", "range": true, "refId": "A" } + ], + "title": "Active Pattern", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] }, + "gridPos": { "h": 5, "w": 6, "x": 18, "y": 0 }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, + "textMode": "name" + }, + "targets": [ + { "editorMode": "code", "expr": "signalcontrol_phase{job=\"signalcontrol-pi-app\",instance=\"pirelay\"}", "legendFormat": "{{phase}}", "range": true, "refId": "A" } + ], + "title": "Current Phase", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "ops" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 5 }, + "id": 5, + "options": { "legend": { "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "single" } }, + "targets": [ + { + "editorMode": "code", + "expr": "sum by (channel, state) (rate(signal_relay_writes_total{job=\"signalcontrol-pi-app\",instance=\"pirelay\"}[$__rate_interval]))", + "legendFormat": "channel {{channel}} {{state}}", + "range": true, + "refId": "A" + } + ], + "title": "Relay Activations", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "ops" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 5 }, + "id": 6, + "options": { "legend": { "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "single" } }, + "targets": [ + { + "editorMode": "code", + "expr": "sum by (source, to_phase) (rate(signal_transitions_total{job=\"signalcontrol-pi-app\",instance=\"pirelay\"}[$__rate_interval]))", + "legendFormat": "{{source}} -> {{to_phase}}", + "range": true, + "refId": "A" + } + ], + "title": "Phase Dwell / Transitions", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 13 }, + "id": 7, + "options": { "legend": { "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "single" } }, + "targets": [ + { + "editorMode": "code", + "expr": "sum by (action) (increase(signal_schedule_fires_total{job=\"signalcontrol-pi-app\",instance=\"pirelay\"}[24h]))", + "legendFormat": "{{action}}", + "range": true, + "refId": "A" + }, + { + "editorMode": "code", + "expr": "sum by (from_pattern, to_pattern) (increase(flowercore_signalcontrol_pattern_switches_total{job=\"signalcontrol-pi-app\",instance=\"pirelay\"}[24h]))", + "legendFormat": "{{from_pattern}} -> {{to_pattern}}", + "range": true, + "refId": "B" + } + ], + "title": "Schedule Fires and Pattern Switches", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "percentunit" }, "overrides": [] }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 13 }, + "id": 8, + "options": { "legend": { "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "single" } }, + "targets": [ + { + "editorMode": "code", + "expr": "1 - avg by (instance) (rate(node_cpu_seconds_total{job=\"edge-nodes\",instance=\"pirelay\",mode=\"idle\"}[$__rate_interval]))", + "legendFormat": "CPU", + "range": true, + "refId": "A" + }, + { + "editorMode": "code", + "expr": "1 - (node_memory_MemAvailable_bytes{job=\"edge-nodes\",instance=\"pirelay\"} / node_memory_MemTotal_bytes{job=\"edge-nodes\",instance=\"pirelay\"})", + "legendFormat": "Memory", + "range": true, + "refId": "B" + } + ], + "title": "pirelay Host Utilization", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] }, + "gridPos": { "h": 6, "w": 12, "x": 0, "y": 21 }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, + "textMode": "auto" + }, + "targets": [ + { "editorMode": "code", "expr": "signalcontrol_screen_saver_enabled{job=\"signalcontrol-pi-app\",instance=\"pirelay\"}", "range": true, "refId": "A" } + ], + "title": "Screen-saver Enabled", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] }, + "gridPos": { "h": 6, "w": 12, "x": 12, "y": 21 }, + "id": 10, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, + "textMode": "name" + }, + "targets": [ + { "editorMode": "code", "expr": "signalcontrol_animation_active{job=\"signalcontrol-pi-app\",instance=\"pirelay\"}", "legendFormat": "{{planner}}", "range": true, "refId": "A" } + ], + "title": "Screen-saver / Animation Engaged", + "type": "stat" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "style": "dark", + "tags": [ "flowercore", "signalcontrol", "pirelay" ], + "templating": { "list": [] }, + "time": { "from": "now-24h", "to": "now" }, + "timezone": "browser", + "title": "FlowerCore SignalControl", + "uid": "flowercore-signalcontrol", + "version": 1 + } diff --git a/apps/monitoring/noc-monitoring.yaml b/apps/monitoring/noc-monitoring.yaml index 84a3151..777a45f 100644 --- a/apps/monitoring/noc-monitoring.yaml +++ b/apps/monitoring/noc-monitoring.yaml @@ -230,6 +230,19 @@ data: vlan: "home" device: "pi3-ks0212" + # SignalControl Pi-edition app metrics (pirelay / signal-a) + - job_name: "signalcontrol-pi-app" + scrape_interval: 15s + metrics_path: /metrics/prometheus + static_configs: + - targets: ["10.0.58.113:5200"] + labels: + instance: "pirelay" + host: "signal-a.iamworkin.lan" + service: "signalcontrol-pi" + vlan: "home" + device: "pi3-ks0212" + # Epson ET-3750 EcoTank Printer SNMP - job_name: "snmp-printer" scrape_interval: 5m @@ -4051,6 +4064,9 @@ spec: - name: dashboards-remotedesktop mountPath: /var/lib/grafana/dashboards/remotedesktop readOnly: true + - name: dashboards-signalcontrol + mountPath: /var/lib/grafana/dashboards/signalcontrol + readOnly: true - name: datasource-provisioning mountPath: /etc/grafana/provisioning/datasources readOnly: true @@ -4104,6 +4120,9 @@ spec: - name: dashboards-remotedesktop configMap: name: grafana-dashboard-remotedesktop + - name: dashboards-signalcontrol + configMap: + name: grafana-dashboard-signalcontrol - name: datasource-provisioning configMap: name: grafana-datasource-provisioning diff --git a/tests/bluejay-infra-lint/SignalControlPlatformManifestTests.cs b/tests/bluejay-infra-lint/SignalControlPlatformManifestTests.cs new file mode 100644 index 0000000..2ddebb9 --- /dev/null +++ b/tests/bluejay-infra-lint/SignalControlPlatformManifestTests.cs @@ -0,0 +1,51 @@ +using FluentAssertions; +using Xunit; + +namespace BluejayInfraLint.Tests; + +[Trait("Category", "Unit")] +public sealed class SignalControlPlatformManifestTests +{ + private static readonly string Root = ManifestInventory.Load().BluejayRoot; + + [Fact] + public void Monitoring_PrometheusScrapesSignalControlPiAppAndPirelayNodeExporter() + { + var monitoring = File.ReadAllText(Path.Combine(Root, "apps", "monitoring", "noc-monitoring.yaml")); + + monitoring.Should().Contain("job_name: \"signalcontrol-pi-app\""); + monitoring.Should().Contain("metrics_path: /metrics/prometheus"); + monitoring.Should().Contain("targets: [\"10.0.58.113:5200\"]"); + monitoring.Should().Contain("host: \"signal-a.iamworkin.lan\""); + monitoring.Should().Contain("targets: [\"10.0.58.113:9100\"]"); + monitoring.Should().Contain("instance: \"pirelay\""); + } + + [Fact] + public void Monitoring_GrafanaMountsSignalControlDashboard() + { + var monitoring = File.ReadAllText(Path.Combine(Root, "apps", "monitoring", "noc-monitoring.yaml")); + var dashboard = File.ReadAllText(Path.Combine(Root, "apps", "monitoring", "grafana-dashboard-signalcontrol.yaml")); + + monitoring.Should().Contain("name: dashboards-signalcontrol"); + monitoring.Should().Contain("mountPath: /var/lib/grafana/dashboards/signalcontrol"); + monitoring.Should().Contain("name: grafana-dashboard-signalcontrol"); + dashboard.Should().Contain("\"uid\": \"flowercore-signalcontrol\""); + dashboard.Should().Contain("signalcontrol_active_pattern"); + dashboard.Should().Contain("signal_relay_writes_total"); + dashboard.Should().Contain("node_cpu_seconds_total"); + } + + [Fact] + public void FcSignalControlReadme_DocumentsMtlsTelemetryAndDefaultOffAudit() + { + var readme = File.ReadAllText(Path.Combine(Root, "apps", "fc-signalcontrol", "README.md")); + + readme.Should().Contain("step-ca-agent"); + readme.Should().Contain("10.0.58.113:5200"); + readme.Should().Contain("10.0.58.113:9100"); + readme.Should().Contain("PhysicalAudit:Enabled=false"); + readme.Should().Contain("ForwardingEnabled=false"); + readme.Should().Contain("Secrets, enrollment codes, private keys"); + } +}