Skip to content

Commit c1b298c

Browse files
Split clinic preparation into its own runbook; correct private endpoint guidance (#171)
clinic-preparation.md is now the checklist for running a clinic (week-before / day-before / morning / during / after), carrying the July dummy-run lessons: the gateway_images flag lives in the Manage repo's flags.production.yml and needs a Manage deploy to change; the VM must be confirmed on and unable to sleep (it has been found switched off, which also silently hangs deploys by queueing run commands forever); accession numbers must be fresh per clinic; the relay path is proven by traffic, not configuration. Also: ship debug_toolkit.ps1 in the release package so it is already on the VM; add deploy-hang troubleshooting to the onboarding runbook (machine off, stale run-command queue); and add the manual-images-mode gotcha to the promotion runbook.
1 parent 0fb0417 commit c1b298c

6 files changed

Lines changed: 108 additions & 34 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Clinic preparation
2+
3+
> **When to use**: Preparing for a live clinic (or a dummy run) that uses the
4+
> gateway imaging flow. Work through it in order — the sections have lead times.
5+
6+
---
7+
8+
## The week before
9+
10+
- [ ] **Software currency**: the intended gateway release is deployed to the site VM
11+
(check the deploy pipeline history, or `current` junction target on the VM),
12+
and any Manage changes the clinic depends on are in prod.
13+
- [ ] **Feature flag**: `gateway_images` is **on** for prod — it lives in
14+
**`flags.production.yml` in the Manage repo**.
15+
- [ ] **Prod Manage records**: `Gateway` record with the machine's managed-identity
16+
OID; `Relay` record whose **Setting matches the clinic's Setting**.
17+
- [ ] **Test data prepared with fresh accession numbers** — never reuse a previous
18+
clinic's CSV: Manage's per-gateway accession uniqueness rejects reused values,
19+
and gateway-side reuse serves stale participant data.
20+
- [ ] **First time at a site, or after a promotion**: run a full remote dummy run
21+
first — one test participant end to end, glove-box exposure included.
22+
23+
## The day before
24+
25+
- [ ] **The VM is on and will stay on**`az connectedmachine show … --query status`
26+
returns **Connected**.
27+
- [ ] **The relay path passes traffic** — echo probe from the Manage container, or
28+
a fresh successful check-in.
29+
- [ ] **Fresh appointments only** — any appointment that ever had manual image
30+
details recorded stays in manual mode by design.
31+
- [ ] **Gateway worklist DB is clean** — no stale items from previous tests
32+
(`Get-GwWorklist`, or the run-command SELECT; wipe if needed).
33+
- [ ] **People and access roster confirmed for the whole clinic window**:
34+
- someone with a **prod Rubie login** including Django admin (Gateway actions)
35+
- someone with **az / portal access** to the prod subscriptions
36+
- the **hospital contact** who can operate the modality and physically reach the VM
37+
- RDP to the VM if the hospital can provide it (live tails beat run-commands)
38+
- [ ] **Fallback agreed**: if the gateway flow breaks mid-clinic, the manual-images
39+
flow carries the clinic and debugging happens without time pressure.
40+
- [ ] **No stale Arc run-commands queued** on the machine
41+
(`az connectedmachine run-command list -o table`; delete non-terminal stragglers —
42+
they block deploys and diagnostics).
43+
44+
## The morning (~20 minutes before the first participant)
45+
46+
On the VM (RDP or run-command):
47+
48+
1. `Restart-Service Gateway-Relay` — fresh relay connection, freshly-started token
49+
clock. Wait for **`Connected - waiting for worklist actions...`** in the log.
50+
2. `Get-GwHealth` — all four services **Running**, ports 104 and 11112 listening.
51+
3. After the first (test) participant is checked in on Manage: `Get-GwWorklist`
52+
shows the item almost immediately — that proves Manage → Relay → VM
53+
end to end.
54+
4. On the modality: query the worklist and confirm the participant is listed.
55+
56+
From a laptop:
57+
58+
1. `az relay hyco show … --query listenerCount` returns ≥ 1.
59+
2. Gateway actions in the Django admin: the test check-in shows **`confirmed`**
60+
with an empty `last_error`.
61+
62+
## During the clinic — passive monitoring
63+
64+
On the VM, keep two windows open:
65+
66+
```powershell
67+
Watch-GwLog PACS # window 1 — see each C-STORE arrive
68+
Watch-GwLog Upload # window 2 — see each upload to Manage
69+
```
70+
71+
Elsewhere: the **Gateway actions** admin page (each check-in → `confirmed`), and
72+
App Insights **Live Metrics** if telemetry is up.
73+
74+
After each participant: `Get-GwImages` shows the expected image count with
75+
`uploaded` equal to `images` within a minute or two.
76+
77+
## Afterwards
78+
79+
- [ ] Wipe test items from the gateway worklist DB; note any test studies/images
80+
created in prod Rubie and clean up or record them.
81+
- [ ] Write up anything that surprised you — against this checklist — and fold the
82+
lesson back into this document. That is how everything above got here.

docs/deployment/runbooks/live-clinic-debugging.md

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ in pipeline order.
3030

3131
## On the gateway VM
3232

33-
Copy `debug_toolkit.ps1` to the VM (or paste it into an editor there), then:
33+
The toolkit ships with the deployed release:
3434

3535
```powershell
36-
. .\debug_toolkit.ps1
36+
. 'C:\Program Files\NHS\ManageBreastScreeningGateway\current\scripts\powershell\debug_toolkit.ps1'
3737
Get-GwHealth # start here: services, ports, relay connection, disk
3838
```
3939

@@ -158,8 +158,15 @@ python scripts/diagnostics/relay_echo_probe.py \
158158
There is also `--bearer-token` mode (see the script's docstring) to test the AAD
159159
bearer path Manage uses in production. To run the probe from *inside* the Manage
160160
container (testing Manage's actual network/DNS/identity), see the container-side
161-
snippet in the script's docstring — during the July 2026 incident that variant is
162-
what exposed the relay hostname resolving to a private endpoint IP inside the VNet.
161+
snippet in the script's docstring.
162+
163+
**Interpreting in-container DNS resolution**: prod Manage reaches the relay via a
164+
**private endpoint**, so the relay hostname resolving to a **private IP** inside
165+
the VNet is *correct*. During the July 2026 incident the PE silently blackholed
166+
all traffic **while showing Approved** — likely an Azure-side fault or race at
167+
PE creation; the interim fix was pointing DNS at the public endpoint, superseded
168+
by deleting and recreating the PE. The health question is never "which IP family"
169+
or "what does the PE state say" — it is **"does the echo come back"**.
163170

164171
Rotate the namespace key after a debugging session — nothing in a deployed
165172
environment uses SAS, so rotation is free.
@@ -200,30 +207,3 @@ az monitor log-analytics query --workspace <workspace-customer-id> \
200207
Manage-side data checks (worklist item actually created and sent?) are quickest in
201208
the Django admin: **Gateway actions** — a healthy send is `status: confirmed`;
202209
`failed` rows carry `last_error` and the retry schedule.
203-
204-
## Pre-flight checklist (run before the clinic starts)
205-
206-
On the VM:
207-
208-
1. `Get-GwHealth` — all four services **Running**, ports 104 and 11112 listening,
209-
relay log shows "Connected - waiting for worklist actions", disk has headroom.
210-
2. `Get-GwWorklist` — after the first participant is checked in on Manage, the item
211-
appears here within seconds. That one check proves Manage → Relay → VM end to end.
212-
3. On the modality: query the worklist and confirm the participant is listed.
213-
214-
From the laptop:
215-
216-
4. `az relay hyco show ... --query listenerCount` returns ≥ 1.
217-
5. App Insights errors query over `ago(12h)` is quiet (no crash loops overnight).
218-
219-
## During the clinic — a useful passive setup
220-
221-
On the VM, keep two windows open:
222-
223-
```powershell
224-
Watch-GwLog PACS # window 1 — see each C-STORE arrive
225-
Watch-GwLog Upload # window 2 — see each upload to Manage
226-
```
227-
228-
After each participant: `Get-GwImages` should show the expected image count with
229-
`uploaded` equal to `images` within a minute or two.

docs/deployment/runbooks/onboard-hospital-vm.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,13 @@ If disconnected, re-run the script. It is safe to re-run — the agent will reco
284284

285285
The Arc machine must appear in the resource group before Terraform can create the HC. Confirm the machine is **Connected** in the portal (Step 2 verify), then re-run the pipeline.
286286

287+
### Deploy pipeline hangs with no output after the ring banner
288+
289+
The deploy job prints the `--- Ring: … ---` banner and then goes silent for 30+ minutes. Possible causes:
290+
291+
1. **The VM is powered off or asleep.** `az connectedmachine show -n <machine> -g <rg> --query status` — anything other than **Connected** means run commands queue indefinitely against an absent machine.
292+
2. **Stale run commands blocking the queue.** Run commands execute serially per machine, and diagnostic commands accumulate as persistent resources. `az connectedmachine run-command list -o table` — delete anything stuck in a non-terminal state.
293+
287294
### Gateway services not starting after deploy
288295

289296
Check the deployment log on the VM:

docs/deployment/runbooks/promote-vm-preprod-to-prod.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ With the modality reconfigured and prod Rubie seeded:
142142

143143
If step 2 fails, the Gateway action's `last_error` names the failing phase — see the [live clinic debugging runbook](./live-clinic-debugging.md) for the symptom → check mapping.
144144

145+
Before the site's first real clinic on the promoted gateway, work through [Clinic preparation](./clinic-preparation.md).
146+
145147
---
146148

147149
## Gotchas
@@ -153,7 +155,8 @@ If step 2 fails, the Gateway action's `last_error` names the failing phase — s
153155
| Modality C-ECHO fails after promotion | Modality still targeting `..._PRE` AE titles | Reconfigure modality to `..._PROD` (Step 5) |
154156
| Hybrid Connection not created by Terraform | Arc machine not yet **Connected** in the prod RG | Confirm Step 3 verify, then re-run the infra pipeline |
155157
| Pre-prod relay still shows connection attempts | Orphaned pre-prod HC / registration | Complete Step 6 |
156-
| Rubie check-ins time out with `phase: connecting` while the listener is healthy | Rubie-side network/DNS — historically, the relay hostname resolving to a private endpoint IP inside the Rubie VNet | See [live clinic debugging](./live-clinic-debugging.md); verify the relay FQDN resolves to a **public** IP from the Rubie container |
158+
| Rubie check-ins time out with `phase: connecting` while the listener is healthy | Rubie-side network — most likely a **broken relay private endpoint**. Note the PE can blackhole all traffic **while showing Approved** | Delete the PE and let Terraform recreate it, then prove the path with the echo probe from the Rubie container — see [live clinic debugging](./live-clinic-debugging.md) |
159+
| The whole UI runs in manual-images mode | `gateway_images` flag off in the Manage repo's **`flags.production.yml`**, or Relay↔Setting mismatch, or the appointment already has a manual study | Flag change requires a Manage deploy (one-line PR, leave the other flags alone); Setting fix is one field in admin; otherwise use a fresh appointment |
157160
| Image uploads rejected with 403 after promotion | `Gateway.oid` still holds the old pre-prod identity | Step 4.4 — update to the new principal ID |
158161
| Rubie shows the manual-images flow instead of awaiting images | `Relay` record's Setting doesn't match the clinic's Setting, or the appointment already has a manual study | Fix the Setting FK in the admin; retest with a fresh appointment |
159162
| Sends fail after the gateway has been idle overnight | Relay listener connection died silently (known issue) | `Restart-Service Gateway-Relay`; restart before every clinic until the listener liveness fix ships |

scripts/bash/package_release.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ echo ""
6262
# ── Validate required files ───────────────────────────────────────────────────
6363

6464
REQUIRED_FILES=("src/" "sample_images/" "scripts/python/database.py" \
65-
"scripts/powershell/maintenance.ps1" "pyproject.toml" "uv.lock" "README.md" "LICENCE.md")
65+
"scripts/powershell/maintenance.ps1" "scripts/powershell/debug_toolkit.ps1" \
66+
"pyproject.toml" "uv.lock" "README.md" "LICENCE.md")
6667

6768
for item in "${REQUIRED_FILES[@]}"; do
6869
if [[ ! -e "${REPO_ROOT}/${item}" ]]; then

scripts/diagnostics/relay_echo_probe.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
3434
az containerapp exec -n <app-name> -g <resource-group> --command sh
3535
36-
# 1. What does the relay hostname resolve to? Must be a PUBLIC IP.
36+
# 1. What does the relay hostname resolve to? In an environment using a
37+
# private endpoint (prod does), a PRIVATE IP is correct.
3738
/app/.venv/bin/python -c "import socket; print(socket.getaddrinfo(
3839
'relay-manbrs-prod.servicebus.windows.net', 443)[0][4])"
3940

0 commit comments

Comments
 (0)