These scripts drive the status table at the top of Computing Hardware.
The three servers are not reachable from the same place:
| Host | Public DNS | How it is checked |
|---|---|---|
raven.fish.washington.edu |
no | TCP 8787 (RStudio Server), falling back to 22 — UW network only |
gannet.fish.washington.edu |
yes | HTTPS GET / |
klone.hyak.uw.edu |
yes | TCP 22 — there is no web port to probe |
Raven is not in public DNS, so nothing outside the UW network can see it. Klone has no HTTP port at all, and browsers refuse to connect to port 22, so no purely client-side check can work either.
So there are two probers writing to the same place:
internal— cron on a machine inside the UW network. The only source that can see raven. This is the primary.external— theserver-statusGitHub Action. Sees gannet and klone from the public internet, so a dead internal prober does not take out all three lights at once.
Each writes status/<profile>.json to the orphan server-status branch. The
page fetches both from raw.githubusercontent.com (which sends
access-control-allow-origin: *, so there is no CORS problem) and takes the
most recent fresh reading per host.
Raven also drops a daily df + CPU-load snapshot at
https://gannet.fish.washington.edu/v1_web/owlshell/bu-github/ghr.log,
generated by a cron on raven around 7am and synced to gannet's public web
root. Because that URL is on gannet (public), check_servers.py fetches and
parses it (fetch_raven_stats) under either profile, unlike the live
TCP/8787 check, which only the internal profile can reach.
The parsed result is written as a top-level raven_stats field (not nested
under hosts), separate from the up/down lights:
"raven_stats": {
"generated": "2026-08-18T14:01:02Z",
"cpu_percent": 0.58,
"disks": [
{
"filesystem": "/dev/sdd1",
"mount": "/home/shared/8TB_HDD_02",
"size": "7.3T",
"used": "6.3T",
"available": "602G",
"use_percent": 92
}
]
}It has to stay out of hosts because hosts entries are merged by picking
the freshest whole document, and the external prober can run more often
than the internal cron. If raven_stats lived under hosts.raven, an
external run with no real port-check result would periodically overwrite a
correct, live up/down reading. Instead, docs/javascripts/server-status.js
merges raven_stats on its own generated timestamp.
It renders into its own "Raven Disk / CPU" table on the page
([data-raven-stats-base], one row per drive, fullest first) rather than a
column in the status table — the snapshot is daily rather than live, and
covers only raven, so mixing it into the live up/down table misrepresented
both. CPU load and the snapshot time go in the .ss-raven-meta line above
the table, flagged stale if the snapshot is older than 30 hours. Drives at
90%+ full are marked critical and 75%+ warn, in the text as well as the bar
color.
The parser also reads the memory line, the Winners list (top 5 accounts
by summed %CPU, with summed %MEM), and the top memory processes into
raven_stats.memory_percent, winners and top_memory.
Raven overwrites ghr.log each morning and keeps no dated copies, so the
probers accumulate the history themselves. When run with --out,
check_servers.py also maintains raven_history.json next to the output
file (update_raven_history): one entry per snapshot date (CPU, memory,
per-mount use %, Winners), replaced if the same day is seen again, capped at
365 days. publish_status.sh checks out the server-status branch before
running the checker, so the previous copy is on disk and is carried forward
when the branch is re-orphaned. Losing that branch loses the history.
A collapsible Usage history section under the Raven Disk / CPU table
on Computing Hardware (docs/javascripts/raven-history.js,
[data-raven-history]) reads raven_history.json plus the latest
raven_stats and draws CPU/memory over time, the latest Winners table, and a
per-user grid of appearances in the Winners list. It uses the shared line
chart from gannet-health.js.
Gannet runs gannet_health.sh once a day and writes the result to
https://gannet.fish.washington.edu/v1_web/owlshell/latest.txt, plus a dated
copy, gannet_health_YYYY-MM-DD.txt, in the same directory. The report covers
uptime and load, df and inode use, memory, RAID (/proc/mdstat), services
and failed systemd units, kernel errors, and a summary of alerts.
Gannet does not send CORS headers, so the page cannot fetch the report itself.
Instead check_servers.py fetches and parses it (fetch_gannet_stats) under
either profile and writes a top-level gannet_stats field. It is kept out of
hosts for the same reason as raven_stats. The field holds the parsed
sections, the raw text (raw), and a history list with one small entry per
dated report for the last 30 days (load, per-mount use %, alert count). That
list is built by reading the directory index and fetching each dated file.
docs/javascripts/gannet-health.js merges gannet_stats on its own
generated timestamp and renders it in three places:
- the Daily health cell for gannet in the status table
(
[data-health="gannet"]), - the Gannet Health section on Computing Hardware
(
[data-gannet-summary]: alerts and the disk table), and - the Gannet Dashboard page (
docs/Gannet-Dashboard.md,[data-gannet-dashboard]), with the full report and history charts.
A report older than 30 hours is flagged stale. If the report format changes,
parse_gannet_report treats every section as optional, so a partial parse
still renders.
Klone requires Duo for SSH, so neither prober can run hyakalloc itself.
Instead a scrontab job on klone (as sr320, --account=srlab --partition=ckpt) runs it at 06:00 Pacific and copies the output, under a
Generated: YYYY-MM-DD HH:MM:SS PDT line, to
https://gannet.fish.washington.edu/v1_web/owlshell/hyakalloc.txt plus a
dated hyakalloc_YYYY-MM-DD.txt copy.
check_servers.py fetches and parses it (fetch_hyak_stats) under either
profile into a top-level hyak_stats field, kept out of hosts for the same
reason as raven_stats: one entry per account/partition with total, used
and free CPUs, memory and GPUs, plus the checkpoint idle counts and the raw
text. It also carries a history list with one entry per dated copy over
the last 30 days (used and total CPUs and memory per partition, checkpoint
idle CPUs), built the same way as gannet's. The parser splits rows on the │ column separator, so it depends on
hyakalloc's box-drawn table format.
The Action has a dedicated run at 15:00 UTC (08:00 PDT) to pick it up.
docs/javascripts/hyak-alloc.js renders it in the Klone Allocation
section on Computing Hardware ([data-hyak-alloc]), flagged stale after 30
hours, with a collapsible Usage history chart per resource. The charts
reuse the line chart from gannet-health.js (exposed as
window.RobertsLabCharts), so gannet-health.js must stay listed before
hyak-alloc.js in mkdocs.yml. The snapshot reflects the accounts visible to the user who ran it.
The branch is rewritten as a single root commit on every run. At one check every 10 minutes an append-only branch would add roughly 50,000 commits a year to a repo that everyone clones.
check_servers.py— runs the probes, prints or writes the JSON. Stdlib only.publish_status.sh— runs the checker and pushes the result to theserver-statusbranch. Used by both the cron job and the Action.
Check without publishing anything:
./scripts/check_servers.py --profile internalOff the UW network, raven will report DNS lookup failed — that is expected,
and is exactly why the internal prober has to run inside.
Run this on a machine inside the UW network that is up continuously. Gannet is the natural choice.
1. Give the machine push access. Generate a deploy key on that host:
ssh-keygen -t ed25519 -f ~/.ssh/robertslab_status -C "roberts-lab status bot" -N ""Add the public key (~/.ssh/robertslab_status.pub) at
https://github.com/RobertsLab/resources/settings/keys as a deploy key with
write access checked. A deploy key is scoped to this one repo, so it cannot
be used to touch anything else in the org.
Then point git at it in ~/.ssh/config:
Host github-robertslab
HostName github.com
User git
IdentityFile ~/.ssh/robertslab_status
IdentitiesOnly yes
2. Clone the repo somewhere on that host, e.g. ~/robertslab-resources.
3. Add the cron entry with crontab -e:
*/10 * * * * ~/robertslab-resources/scripts/publish_status.sh --profile internal --repo github-robertslab:RobertsLab/resources.git >> ~/status-cron.log 2>&1
publish_status.sh keeps its own scratch clone under
~/.cache/robertslab-server-status, so it will not disturb the checkout it is
run from.
4. Confirm that status/internal.json appears on the
server-status branch
and that the lights on the handbook page go green within a few minutes.
- A green light means the port answered. It says nothing about Slurm health or whether jobs are running. Raven's disk space and CPU load, and gannet's daily health report, and klone's hyakalloc snapshot are covered separately as described above.
raw.githubusercontent.comcaches for about 5 minutes, so the page can lag the actual check by that much on top of the check interval.- GitHub's scheduled workflows are best-effort: delayed under load, minimum
5-minute interval, and disabled automatically after 60 days without repo
activity. That is why the in-network cron is primary and the Action is only a
backstop. Measured on this repo, the Action asks for
*/15but lands every 30-71 minutes (median 46). The 90-minute stale threshold indocs/javascripts/server-status.jsis sized around that; if you tighten it, make sure the in-network cron is actually running first, or the lights will spend most of each cycle showingunknown. - To add a host: add a probe in
check_servers.pyand a<tr data-host="...">row indocs/Computing-Hardware.md. The JavaScript matches the two by name and needs no change.