Files
home-network-k3s-impl/runbooks/longhorn-faulted.md
T
jakman85andClaude Opus 5 b7341b329b fix(runbooks): declare whole-repo scope, drop the bespoke l0 stage, +cnpg-primary-spread
Three corrections after rebasing onto main.

1. THE GUARD CAUGHT A REAL ONE ON ITS FIRST CONTACT WITH main. main had gained
   `CNPGPrimariesCoLocated` (platform/cnpg/alerts.yaml) pointing at
   /runbooks/cnpg-primary-spread, with no runbook. The check failed the rebase exactly as
   designed — 46 annotations, 35 names, 34 present — naming both the missing file AND the
   missing configMapGenerator entry. runbooks/cnpg-primary-spread.md now exists: what
   co-located primaries actually cost here (blast radius, not an outage, because
   instances: 3 + required anti-affinity), why the drift is uncorrectable-by-declaration
   (CNPG has no primary-placement field), the readiness gate that keeps the rule quiet
   during a real node outage, and the ~9s-per-database switchover rebalance, one cluster
   at a time. The 2026-09-07 outage in which all three primaries sat on one node is the
   reason the alert exists at all.

2. WHOLE-REPO SCOPE, NOT A HAND-WRITTEN STAGE. main's refactor replaced ci/l0.sh's
   per-check "UNCONDITIONAL" stages with a declaration:
   `conformance.home.k3s/scope: "whole-repo"`, unioned into every gate run by Stage 3b's
   `--include-scope`. My +38-line Stage 3f is removed (ci/l0.sh is now byte-identical to
   main) and the annotation is declared instead. Assertion (7) of the check was rewritten
   to match: it no longer asserts its own path appears in ci/l0.sh — it asserts the test
   DECLARES the scope and that ci/l0.sh still passes --include-scope, since neither half
   alone makes the check load-bearing. That matters here because
   ci/touched-requirements.sh maps platform/monitoring/* to "R-OBS-01 R-OBS-02", not
   R-OBS-04, and maps platform/kubevirt/* and runbooks/* to nothing at all.

3. A FACTUAL CORRECTION IN ALL 35 RUNBOOKS. Every page claimed `dev.sh` defaults its
   current-context to k3s-dev. main's `9b4744d fix(devcontainer): dev.sh defaults to
   prod` made that false, and a runbook that misstates which cluster a command talks to is
   worse than one that says nothing. The pages now say what is durably true: the default
   has already changed once, so naming `--context k3s-prod` is what makes a command mean
   the same thing next month, and the per-invocation banner is there to be read. The
   check's own failure message was corrected the same way.

Re-verified after all three: check green at 374 assertions (46 annotations, 35/35 runbooks
present, 35/35 projected), re-applied to prod, and re-probed in-cluster through the
Gateway — node-down, keycloak-down, keycloak-client-secrets, cnpg-archiving,
longhorn-faulted, vpn-down, node-io and cnpg-primary-spread all HTTP 200 with
text/plain; charset=utf-8 and 8.9-12.1 KB of real body; /runbooks/ index 200; an unknown
name 404s with a pointer rather than a blank page; and homepage still answers the apex `/`,
confirming Gateway API longest-path precedence.

Full merge gate: PASS (spec-immutability, lint, l0, render-golden, l1 all green;
prom-rules SKIPPED for want of promtool in the image; L2/L3 SKIPPED, no cluster).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-13 00:00:24 -05:00

165 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# longhorn-faulted — A Longhorn volume is FAULTED (data at risk, volume offline)
**Alert:** `LonghornVolumeFaulted` (critical, 1m)
**File:** `platform/monitoring/alerts.yaml`, group `platform-health.longhorn`
**Expression:** `longhorn_volume_robustness == 3`
## What this means here
Faulted means Longhorn has **no usable replica**. The volume is offline; the workload's writes are
failing. This is the most serious storage state on the cluster and the `for: 1m` is deliberate — there
is no benign transient version of this.
Understand what is and is not recoverable before you touch anything:
- The volumes on this cluster are 3-replica with **hard** node anti-affinity, so faulted normally
means two or three replicas are gone at once — two nodes lost, or one node lost while another
volume's rebuild had not finished, or a disk that ate its replica.
- **Standbys and replicas are not backups.** Recorded verbatim for the databases and true for all of
this: *"Standbys replicate corruption and deletions faithfully; only the Barman archive is a
backup."* For a CNPG volume (`keycloak-db`, `nextcloud-db`, `semaphore-db`) the real recovery path
is the object-store archive, not Longhorn. For everything else it is Velero.
- If this is a **`longhorn-vm`** volume (RWX Block, `migratable: true`) the VM is down, not just the
disk. If it is **`longhorn-scratch`** (single replica, CDI scratch space) losing it is expected and
harmless — re-run the import.
### Verify the alert can even fire (measured 2026-09-13)
Prometheus on prod scrapes 19 jobs: `apiserver`, `cdi-prometheus-metrics`, `coredns`,
`keycloak-discovery`, `keycloak-service`, `keycloak/keycloak-db`, `kube-prometheus-stack-alertmanager`,
`kube-prometheus-stack-grafana`, `kube-prometheus-stack-operator`, `kube-prometheus-stack-prometheus`,
`kube-state-metrics`, `kubelet`, `kubevirt-prometheus-metrics`, `nextcloud-db/nextcloud-db`,
`node-exporter`, `oauth2-proxy`, `semaphore-db/semaphore-db`, `velero`, `velero/node-agent`.
There is **no** Longhorn, cert-manager, Flux-controller or Hubble/Cilium job. Check the series this
rule depends on before you trust its silence:
```
infra/devcontainer/dev.sh run bash -c \
'kubectl --context k3s-prod -n monitoring port-forward svc/kube-prometheus-stack-prometheus 19090:9090 & \
sleep 5; curl -s -G --data-urlencode "query=count by (job) (up)" localhost:19090/api/v1/query'
```
**This alert is currently INERT.** `count(longhorn_volume_robustness)` returned **0 series** on prod
on 2026-09-13 — Longhorn ships no ServiceMonitor here, so the whole `platform-health.longhorn` group
has no data. A faulted volume today announces itself as an application outage. That is a known open
defect; treat `volumes.longhorn.io` as the source of truth, not this alert's silence.
## Running the commands in this runbook
`kubectl` does not work on the operator workstation. The toolchain lives only in the pinned dev
container, and the host's `k3s-oidc` context hangs on a browser prompt once its token expires —
that hang is not the cluster being down. Every command below runs as:
infra/devcontainer/dev.sh run bash -c 'kubectl --context k3s-prod get nodes'
Always pass `--context k3s-prod` explicitly. `dev.sh` merges BOTH clusters into one kubeconfig and
picks a current-context per invocation, and **that default has already changed once** — it was k3s-dev
until dev was decommissioned on 2026-09-13, and is k3s-prod now. A command that names its context means
the same thing next month as it does today; one that relies on the default does not, and the failure is
silent (it answers confidently about the other cluster). `CLUSTER=prod infra/devcontainer/dev.sh run
kubectl ...` selects it explicitly, and every invocation prints a banner naming the cluster it is about
to talk to — read it. Do not run two `dev.sh` invocations at once; they share one container and
interfere.
USE FULLY-QUALIFIED RESOURCE NAMES, ALWAYS. Three CRDs on this cluster register the kind `Backup`:
`longhorn.io/v1beta2`, `postgresql.cnpg.io/v1` and `velero.io/v1`. The bare short name resolves to
Longhorn's, which holds zero objects — so `kubectl get backup -A` prints **"No resources found"** on
a cluster holding 6 Velero backups and 27 CNPG backups. Verified on prod 2026-09-13. Reading that as
"the backups are gone" and acting on it turns a false negative into real data loss. Write
`backups.velero.io`, `backups.postgresql.cnpg.io`, `clusters.postgresql.cnpg.io`,
`settings.longhorn.io`, `volumes.longhorn.io`, `nodes.longhorn.io`.
Mutating kubectl (`apply`, `patch`, `annotate`, `rollout`) and `flux reconcile` are blocked for
agents here: every durable change goes through git (`git push origin main` **is** the deploy).
Where this runbook gives a mutating command it is for an operator, and it says so.
## First commands
Stop and take inventory before changing anything. Which volume, which workload, which replicas exist:
```
infra/devcontainer/dev.sh run bash -c \
'kubectl --context k3s-prod -n longhorn-system get volumes.longhorn.io \
-o custom-columns=NAME:.metadata.name,ROBUST:.status.robustness,STATE:.status.state,NODE:.status.currentNodeID | grep -Ev "healthy"'
```
```
infra/devcontainer/dev.sh run bash -c \
'kubectl --context k3s-prod -n longhorn-system get replicas.longhorn.io \
-o custom-columns=NAME:.metadata.name,VOL:.spec.volumeName,NODE:.spec.nodeID,STATE:.status.currentState,FAILED:.spec.failedAt | sort -k2'
```
```
infra/devcontainer/dev.sh run bash -c \
'kubectl --context k3s-prod get pv -o custom-columns=PV:.metadata.name,CLAIM:.spec.claimRef.namespace/.spec.claimRef.name,SC:.spec.storageClassName | grep <volume-name>'
```
What recovery material exists — **fully-qualified names, or you will be told there is nothing**:
```
infra/devcontainer/dev.sh run bash -c \
'kubectl --context k3s-prod get backups.velero.io -A; echo; \
kubectl --context k3s-prod get backups.postgresql.cnpg.io -A; echo; \
kubectl --context k3s-prod -n longhorn-system get backups.longhorn.io'
```
`kubectl get backup -A` prints "No resources found" on this cluster even when six Velero and 27 CNPG
backups exist. Never make a recovery decision from the short name.
Node and disk state:
```
infra/devcontainer/dev.sh run bash -c \
'kubectl --context k3s-prod get nodes; kubectl --context k3s-prod -n longhorn-system get nodes.longhorn.io -o wide'
```
## Likely causes, most likely first
### 1. Two nodes were unavailable at once
Usually a second reboot or cordon while the first node's rebuilds were still running. Remediation:
bring the nodes back. Longhorn will reattach if the replica data survived on disk — which is why you
must not delete replicas before checking.
### 2. A disk failed and took its replica with it
`k3s-prod-1`'s original 2011 SSD had 61,440 reallocated sectors before it was replaced; the dev-era
Seagate had 448 current-pending plus 448 offline-uncorrectable. Check SMART on the node holding the
failed replica (`/runbooks/node-io` has the command). If the disk is bad, the replica is gone — the
path forward is restore, not repair.
### 3. All replicas were evicted or deleted
Check whether a disk was disabled, a node removed from Longhorn, or a `replicas.longhorn.io` object
deleted. `staleReplicaTimeout: 30` (minutes) means Longhorn eventually discards replicas it considers
stale.
### 4. It is a single-replica volume by design
`longhorn-scratch` is `numberOfReplicas: 1`. Losing it is not an incident — delete the DataVolume and
re-import.
## Remediation, in order
1. **Do not delete the volume, the PVC, or any replica.** A faulted volume with a stale replica is
recoverable; a deleted one is not.
2. If a node is down, restore the node (`/runbooks/node-down`). That is the highest-probability fix.
3. If Longhorn shows a replica with data but marked failed, an operator can salvage it from the
Longhorn UI (*Volume → Salvage*) — this is the documented path for "all replicas failed but the
data is still there".
4. If the volume backs a **CNPG database**, do not try to salvage Postgres files. Recover the Cluster
from the Barman archive (`/runbooks/cnpg-archiving` for how to confirm the archive is real first).
5. Otherwise restore from Velero. Note honestly: `dr-restore` is **authored and has never been
executed** (`conformance.home.k3s/never-executed`), and the R2 restore path has not been rehearsed.
Expect to be the first person to run it, and read `/runbooks/backup-failure` for the R2 plugin
caveats before you rely on a backup's metadata.
## When to escalate or stop
- **Get the operator now.** This is a critical, data-at-risk alert; it is not a runbook-and-carry-on
situation. Everything above is diagnosis, and steps 3–5 are operator actions.
- **Stop before any destructive command.** Specifically: no `delete pvc`, no `delete
volumes.longhorn.io`, no `delete replicas.longhorn.io`, no touching files under `/data/longhorn`.
- **Do not reboot anything** while a volume is faulted.
- If the affected volume is `keycloak-db`, expect a full SSO outage as a knock-on: kubectl OIDC,
Grafana, Headlamp, Gitea and forward-auth all depend on it. The certificate-based kubeconfig is an
independent break-glass path and still works.
---
Source of truth: `runbooks/longhorn-faulted.md`. Index: `/runbooks/`
Records: `DECISIONS-LOG.md` (drive failures 2026-08-22 and 2026-09-07; `dr-restore` never executed).