Ragnar updates itself from GitHub. There are two ways to do it and they now behave identically:
| Web UI | Settings → System Updates → Update System |
| Terminal | sudo /home/ragnar/Ragnar/update_ragnar.sh |
The web path is handled by git_updater.py, a dedicated
update engine, plus scripts/post_update.sh which
finishes the job and restarts the service.
- Preflight. Checks that git runs, that the checkout has repository
metadata, that there is disk space, whitelists the directory for root
(
safe.directory), sweeps stale*.lockfiles, and aborts any merge or rebase a previous crashed run left half-finished. - Fetch from
origin, retrying transient network errors. - Land the new code. A clean checkout is fast-forwarded — no merge
commit, nothing that can conflict. Anything else (local edits, local
commits, a diverged history, untracked files in the way) is synced to
origin/<branch>after your local changes are stashed. - Replay your local changes, if there were any, and drop the stash once they apply cleanly. If they clash with the update, the box stays on the new version and your changes stay in the stash (see below).
- Post-update tasks run in a transient systemd unit: Python dependencies
(only when
requirements.txtchanged), data-file templates, network-tool provisioning, permissions — then the service restarts. - The browser verifies the result, waiting for the box to report the new commit rather than just answering HTTP again.
Everything a box generates — the database, captures, logs, keys, configs — is
gitignored, and every recovery path uses git clean -fd, never -x. By
definition that cannot remove an ignored file. data/ is not walked, chowned,
cleaned or reset by the updater.
If you have edited tracked files, they are stashed before the update and replayed afterwards. If they conflict with the incoming version, the update still lands (the box is left running a known-good tree) and the edits stay in the stash:
cd /home/ragnar/Ragnar
git stash list # your changes are the newest "Ragnar auto stash" entry
git stash pop # reapply them by handAn update never leaves conflict markers in a source file — that used to end with a service that would not start.
Local commits are a different matter: a box that has diverged from upstream is
force-synced to the released version. The old commits are still reachable
through git reflog if you need them.
Every failure now carries a code and one sentence of what to do about it, in the card and in the console panel underneath it.
| Code | Meaning | What to do |
|---|---|---|
offline |
The box could not reach github.com | Check its internet connection and retry |
timeout |
A git command stopped responding and was cancelled | Usually a slow or dropped link; retry when back online |
auth |
origin asked for credentials |
Point it at the public URL: git remote set-url origin https://github.com/PierreGode/Ragnar.git |
disk_full |
Not enough space to pull | Free space — data/logs and old captures first |
not_a_repo |
No .git (tarball install) and upstream unreachable |
Check the box's internet connection; the check repairs this by itself once it can reach github.com |
branch_missing |
The tracked branch is gone upstream | Nothing — the updater falls back to the default branch automatically |
ownership |
Git refused the checkout's file ownership | Usually self-repairing; else sudo chown -R ragnar:ragnar /home/ragnar/Ragnar |
locked |
Another git process holds the repository | Wait a moment and retry; stale locks are swept automatically |
permission |
The service cannot write its own directory | sudo chown -R ragnar:ragnar /home/ragnar/Ragnar |
busy |
An update is already running | Wait for it — a second click is refused, not queued |
git_missing |
git does not run on this board | See git is broken on this board |
The Check for Updates card also reports when the box is ahead of upstream (local commits), when the working tree is dirty, and when a branch no longer exists upstream — all states that used to silently show "up to date".
The service restarts as part of an update, so the browser cannot simply wait for
a response. It polls GET /api/system/update-status:
{
"commit": "ab099c4e…",
"branch": "main",
"update_in_progress": false,
"post_update": { "state": "running", "step": "python dependencies" },
"service_started": 1785016739.5
}An update is only reported as successful once the box reports the commit that was pulled and the post-update tasks have finished. Progress steps appear in the console panel as they happen, including across the restart.
That line is scripts/post_update.sh reporting where it is. In order:
| Step | What it does | Why it can be slow |
|---|---|---|
python dependencies |
pip install -r requirements.txt |
Only runs when requirements.txt changed in the pull; a source build (cryptography, sslyze) takes minutes on a Pi |
data file templates |
init_data_files.sh |
Fast |
network tools |
scripts/provision_network_tools.sh — rfkill, traceroute/mtr/lldpd/arp-scan, lldpd config |
Fast when everything is already installed. When something is missing it refreshes the apt index first, which is the slowest thing in an update |
permissions |
exec bits and ownership | Fast |
restart |
systemctl restart ragnar |
The web UI is briefly unreachable here — expected |
Each step is safe to re-run and skips work already done, so a normal update
passes through all of them in seconds. network tools is the one that can sit
there: a package that is simply not in your Debian suite (speedtest-cli is the
usual one) counts as "missing" forever, so it used to trigger a full
apt-get update on every update. The index refresh is now reused for six
hours, and a missing package is still retried against the index already on disk.
If the card sits on a step, data/logs/post_update.log says what is happening.
A run that never finishes — power cut, a hung apt — is ignored after 30 minutes
rather than pinning the card on a step from an update that ended days ago.
Transcripts live on the box:
data/logs/post_update.log # full output of the post-update run
data/logs/post_update.json # machine-readable state, step and outcome
That should no longer happen — but if it does, the console panel under the button carries the raw git message, the code and the hint. The same run can be reproduced with more detail from a terminal:
sudo /home/ragnar/Ragnar/update_ragnar.shgit --versionIf that prints Illegal instruction, git itself is broken, not Ragnar — Debian Trixie arm64 has shipped a git built with ARMv8.1 atomics that crashes on the Pi Zero 2 W's Cortex-A53:
sudo apt update && sudo apt install --reinstall gitBecause the installer falls back to a release tarball when git is unusable, such
a box has no .git at all. Nothing needs reinstalling: the update check rebuilds
it in place on its own, keeping every file on disk, and update_ragnar.sh does
the same. A box in that state is not "behind" — it is running the release it was
installed from — so once the metadata is back the card simply reads Up to
Date. Only a box that cannot reach github.com at all reports not_a_repo, and
the repair is retried at most every 10 minutes until it succeeds.
Post-update work (dependency installs especially) runs before the restart, so give it a few minutes on a Pi Zero. Then:
sudo systemctl status ragnar
tail -50 /home/ragnar/Ragnar/data/logs/post_update.logThe updater is designed so this is never necessary, but the equivalent of what it does is:
cd /home/ragnar/Ragnar
sudo systemctl stop ragnar
sudo -u ragnar git stash push -u -m "manual backup"
sudo -u ragnar git fetch origin
sudo -u ragnar git reset --hard origin/main
sudo -u ragnar git clean -fd # note: NOT -x, your data stays
sudo bash scripts/post_update.sh --depsgit_updater.py— the engine. Every git call is non-interactive (GIT_TERMINAL_PROMPT=0, sshBatchMode=yes) and time-bounded, the repository path comes from__file__rather than the process working directory, and failures are classified into the codes above.scripts/post_update.sh— dependencies, data templates, provisioning, permissions, restart. Launched throughsystemd-runso it survives the restart it triggers (anything the service spawns otherwise dies with the service's cgroup).tests/test_git_updater.py— drives real git repositories through every broken checkout state the engine is meant to survive. Run withpython3 -m pytest tests/test_git_updater.py.
| Endpoint | Purpose |
|---|---|
GET /api/system/check-updates |
Commits behind/ahead, branch, working-tree state, failure code + hint |
POST /api/system/update |
Run an update (handles dirty checkouts too) |
POST /api/system/stash-update |
Kept for compatibility; identical to the above |
POST /api/system/resolve-conflicts |
Discard a half-finished merge, then update |
GET /api/system/update-status |
Current commit, post-update step, process start time |
A second concurrent update returns 409 with code busy instead of blocking.