The serial & fetch pipeline (SKYNET)
This page is the maintainer's contract for how a merged commit physically reaches every live box. There are exactly two transports, and every change you ship rides one of them:
- Per-file, serial-gated fetches —
_fetch_versionedinBOA.sh.txt, driven by the SKYNET self-update tick. Coversaegir/tools/bin/*,aegir/tools/system/*monitors, and the privilegedfix-drupal-*helpers. Shipping a change here means decrementing an fNN serial — in both fetch sites if the tool has two. - Whole-release rides —
lib/functions/*.sh.inc,lib/settings/*.sh.cnf, Nginx/PHP config templates, and everything else in the build tree. No per-file serial exists; the code lands when a box runs a fullbarracuda up-*pass against the release. Their "serial" is the release identity itself (_xSrl/_X_VERSION).
Pick the wrong transport model and the failure is silent: the commit merges, the mirror serves it, and no box ever installs it.
The heartbeat: clear.sh → BOA.sh.txt → autoupboa
The self-update tick is a root cron line, not a daemon
(aegir/tools/system/cron/crontabs/root):
*/5 * * * * bash /var/xdrago/clear.sh >/dev/null 2>&1
Each pass, clear.sh prunes stale pidfiles, stands down only while a live
PHP-idle stack swap holds /run/boa_php_idle_quiesce.pid (owner-PID keyed;
a dead-owner marker is cleaned and the pass continues — proxy nodes run the
full channel since the 2026-08-09 marker narrowing, so BOA.sh.txt is never
stale on any box, proxies included), and — only when no install pass is in
flight (/run/boa_run.pid, /run/boa_wait.pid and
/run/octopus_install_run.pid all absent, and no live installer process) —
re-executes the meta installer straight off the mirror, then the
auto-upgrade scheduler:
wget -qO- https://${_USE_MIR}/versions/${_tRee}/boa/BOA.sh.txt | bash
bash /opt/local/bin/autoupboa
BOA.sh.txt is therefore never stale on a box — it is streamed fresh from
the mirror every tick, and everything below hangs off whatever identity block
that fresh copy carries.
Inside it, the dispatch at the bottom of the script:
_if_boa_key_tools_update_allowed— parks the whole update path while a distro migration marker (/root/.run-to-<codename>.cnf) is present._SKYNET_MODE=OFF→_if_update_boa_key_tools_onlyruns_update_boa_tools(the core tools still refresh, per-file serial-gated) andexit 0. This is the documented "critically important tools are still updated" floor — and since the once-per-release function latch was retired, an OFF box picks up a bumped core-tool serial on its next tick like any other box.- SKYNET active →
_boa_setup(mirror health check, then_update_boa_toolsunconditionally — every line inside self-gates on its own serial stamp), then — only on an installed box (/var/log/barracuda_log.txtexists) — the_fix_*series, Drupal core patch checks (_if_drupal_patches_update,_fix_drupal_core_ten/_fix_drupal_core_eleven) and_update_agents.
Beyond fetching the current copy, two of those steps also verify that a core
tool still works and rebuild it in place if it does not. _boa_setup — and
the _SKYNET_MODE=OFF floor, which reaches it through
_if_update_boa_key_tools_only — runs _if_reinstall_curl: if the curl
binary's libraries have drifted out of sync (curl --version shows no OpenSSL,
a stale libcurl.so.4, or empty output) it backs up the legacy libs and
reinstalls curl, logging OOPS: cURL is broken! Re-installing... cURL is
therefore repaired on every dispatch path, even with SKYNET off. On the
SKYNET-active installed-box path the _fix_* series adds _if_fix_lshell,
which reinstalls the lshell limited shell (with python3-pip) when its
version check comes back wrong or its Python runtime fails with a Traceback,
bad interpreter or ImportError. The serial fetches keep the tools
current; these integrity checks keep them working, which is why an OS
or library change rarely leaves BOA's own tooling stranded.
Separately, the nightly owl.sh (04:15 cron) fetches
${_urlHmr}/conf/version/barracuda-release.txt (shipped from
aegir/conf/version/) and mails _MY_OCTO_EMAIL when
/var/log/barracuda_log.txt does not yet contain that release string — the
"new release available" notice. It runs only when _SKYNET_MODE is unset or
ON — and only when owl.sh runs at all: not on a proxy node
(/root/.proxy.cnf, a deliberately kept gate), not while heavy maintenance
is paused, and not while a PHP-idle stack swap holds its quiesce marker.
The identity block
The first exported lines of BOA.sh.txt are the release identity every other
mechanism keys off:
export _tRee=dev # tree: dev | pro | lts (per published branch copy)
export _xSrl=<NNNN><tree>TNN # whole-release serial — in EVERY ctrl stamp name
export _rLsn="BOA-X.Y.Z" # release label
(Concrete values churn every release — read them from BOA.sh.txt at HEAD;
NNNN squashes the version, e.g. X.Y.Z → XYZ-style digits.)
_tReeselects the mirror tree: all serial-gated fetches resolve against_urlHmr="https://${_USE_MIR}/versions/${_tRee}/boa/aegir". The value is baked into each branch's published copy of the installer, so a box follows whichever tree'sBOA.sh.txtitsclear.shstreams._xSrlappears in every control stamp (<name>.ctrl.<serial>.${_tRee}.${_xSrl}.pid). Bumping it at a release invalidates all stamps at once — the first tick after a release refetches the entire serial-gated tool set exactly once. One-shot migration blocks (e.g.cv-phar-symlink.ctrl.${_tRee}.${_xSrl}.pid) and thedragonbackup names inlib/functions/use the same key, which is why whole-release code needs no fNN of its own.- A release bump is not the only thing that clears stamps. Every full
upgrade pass that reaches the tools deploy drops the whole stamp set.
_xdrago_install_upgrade(lib/functions/system.sh.inc) copies the release tarball'saegir/tools/system/*over/var/xdrago/, which reverts fetch-delivered tools to the release cut's vintage while their fNN stamps still claim the newer serial — a box upgrading between a mid-cycle tool wave and the next release cut would otherwise keep the reverted bytes with no stamp left to trigger a re-fetch. The function therefore runsrm -f ${_pthLog}/*.ctrl.*.pidimmediately after the copy, and the next_update_boa_toolstick re-fetches every current serial within minutes. The glob is deliberately wide: the one-shot whole-release markers (cv-phar-symlink.ctrl.${_tRee}.${_xSrl}.pidand friends) match it too, so those idempotent blocks re-run once after an upgrade. Byte/marker coherence after a tar deploy therefore holds whether or not the release serial moved. _rLsnfeeds_rlsE="${_rLsn}-${_tRee}"; thebarracuda/octopus/boawrappers pin it into the fetched settings file as_X_VERSION=${_rlsE}(see the release-ride section below). The same values are duplicated at the top of the wrappers themselves — a release commit updates all of them together._bTsis RETIRED (with the wholeupdateBOAtoolsfunction latch it fed)._update_boa_toolsnow runs unconditionally on every dispatch path and every tick; each tool line self-gates on its own fNN stamp, so there is no function-level serial to bump any more — a tool change ships with its own fNN bump alone, and staleupdateBOAtools*.pidlatches are swept on every run. (The up-counting<NNNN>vNNshape survives elsewhere: the/etc/crontabrewrite markers insideautoupboastill carry it as file-local literals, rewritten by the release bump — same rule there: bump means increment, never re-use.)
All stamps live in _pthLog=/var/log/boa (a one-time migration block at the
top of BOA.sh.txt moved them from the legacy /var/xdrago/log).
_fetch_versioned — the per-file transport
One helper replaces every hand-rolled fetch block (defined in BOA.sh.txt,
above _update_agents):
_fetch_versioned <dest> <url> <serial> \
[--mode 700] [--owner root:root] [--symlink PATH] [--guard PATTERN] \
[--require-token TOK]
Semantics, in order:
- Up-to-date test. Return 0 when the stamp
${_pthLog}/<basename>.ctrl.<serial>.${_tRee}.${_xSrl}.pidexists and the destination is non-empty (-s) and, when--symlinkwas given, the symlink is intact and, when--require-tokenwas given, the deployed copy still carries the token in its last 4 KiB and, on a box that carries the published tree itself and therefore holds this file locally, the deployed bytes still match that local copy exactly (cmp -s) and the stamp is younger than the revalidation window (_FETCH_REVALIDATE_MIN, default 60 minutes — an aged stamp re-checks the source, so a publish race or CDN skew self-heals within the hour instead of pinning stale bytes forever). The serial alone cannot spot a stamp written against content older than it implies: the local comparison catches that on the very next pass, and a box with no local copy has the window as its only backstop. An hour rather than a day is affordable because revalidation normally costs nothing — see item 4. Missing file, broken link, failed token, byte drift against the local copy or an aged stamp = refetch now. - Run guard.
--guard PATTERNdoes apgrep -fc; a running match skips the fetch entirely (no stamp written) — a live tool is never clobbered, and the fetch retries next tick. The match is anchored to genuine executions only — the tool as an interpreter's script argument, or its path asargv[0]— so an editor, a checksum or an operator'ssshcommand that merely names the path does not hold an update back. The guard is skipped entirely when the destination is missing or empty: such a copy cannot be running, and skipping the fetch would leave a dangling symlink behind. - A box that holds the published tree reads from disk, not the network.
/var/www/static/getboamarks a box carrying the tree these URLs are served from. Where it exists the candidate source is resolved by stripping scheme and host from the URL and grafting the webroot on; if that file exists and is non-empty it is copied withcp -pfand no network request is made at all. Such a box must never fetch its own bytes back over HTTP, because an intermediary can answer with pre-publish content after the local tree is already current, and cache purges are best-effort. A URL that is not ours, or a tree the box does not hold, has no local file and falls through to curl unchanged. - Revalidation is a conditional request, and a 304 is success. When the
deployed copy is intact (non-empty, and still carrying its
--require-tokensentinel where one is required), a marker for this serial already exists, and that marker is younger than_FETCH_HARD_REVALIDATE_MIN(default 1440 minutes), the fetch adds-z <dest>, sendingIf-Modified-Sincefrom the deployed copy's mtime. An unchanged source then answers 304 with no body — treated as positive confirmation, not a failed fetch: the marker is re-armed and the optional symlink is still repaired, which is what a revalidation triggered by a missing link came here to fix. That is what makes a short revalidation window cheap.curl -R(andcp -pon the local path) are load-bearing here, not cosmetic: they stamp the deployed copy with the source's Last-Modified, so the comparison is "is the source newer than the version I hold" rather than "newer than the moment this box happened to write it". Each gate exists because a 304 would be the wrong answer without it — a changed serial means new content exists, so it is never revalidated conditionally, and once past the hard window the request goes out unconditionally. A deployed copy carrying a wrong mtime therefore degrades to the unconditional refetch this replaced: it can delay a heal, never prevent one. - Validated atomic replace. The payload lands in a private temp
(
<dest>.new.$$) — the live dest is never written directly. The temp is discarded when curl itself reports failure (a partial body would otherwise survive), when it is empty, when it starts with an HTML tag (the mirror answers a missing path with an HTTP 200 "Under Construction" stub that--failcannot catch), or when a--require-tokentoken is absent from its last 4 KiB (tail placement makes the sentinel a truncation check as well as an identity check — used forwebsh, which doubles as/bin/shon hosted boxes). A surviving payload identical to the deployed copy just re-arms the stamp; a differing one getschmod/chown(defaults700 root:root) and an atomicmvonto the dest, then the optional symlink, all old<basename>.ctrl.*stamps removed and the new stamp touched. Any discard writes no stamp — mirror lag or a transient failure retries on the next 5-minute tick instead of being skipped forever.
Retry is two-layered: _crlGet itself carries --retry 9 --retry-delay 9 --fail (so a non-2xx is refused before the validation chain even starts),
and the stamp-only-on-success rule makes the cron tick the outer retry
loop.
This replaced two pre-5.10.3 mechanisms:
- The
/opt/local/binbulkcurlloop (~30 tools) tested-einstead of-s, so a 0-byte--faildownload could overwrite a good tool and delete its.prevbackup, and it had no retry. - The six privileged helpers (
fix-drupal-*-permissions/ownership.sh,lock-local-drush-permissions.sh,fix-drupal-site-symlinks.sh) sat behind oneupdateFx31.ctrlone-shot marker that stamped even on a failed fetch, so a partial pull was never retried.
Both are now per-file _fetch_versioned calls (/opt/local/bin group and
privileged helpers alike); the old updateFx*.ctrl markers are unused and
harmless.
Two residual hand-rolled blocks remain in _update_agents
(manage_ltd_users.sh, manage_solr_config.sh) — same stamp shape, inline
logic; treat their serials identically.
fNN serials count DOWN — and every tool has exactly ONE fetch site
The in-code rule (comment above the /opt/local/bin group in
_update_boa_tools): "bump a script's serial to force a refresh after
editing it; move it to a value not currently deployed (stamps are
wildcard-cleared, so direction does not matter; convention counts down)."
- Bump = decrement, by convention.
f99 → f98 → … → f01. Concrete values churn every release — read them fromBOA.sh.txtat HEAD, never from docs. - Never re-use. The mechanism itself is direction-free
(
_fetch_versionedwipes old stamps on every successful fetch), but only for boxes that actually completed the previous fetch. A box that skipped an intermediate serial — offline, mirror lag, guard always busy — may still hold a stamp bearing an older value. "Bump" a serial back to a previously-used value and that box matches its stale stamp and silently skips the refetch. Monotonic decrement makes stamp collision impossible. - One tool, one fetch line, one stamp. The historical second listing of
the core tool group in
_update_agentsis gone — it existed only to work around the retired once-per-release_update_boa_toolslatch, at the price of a paired-serial discipline (a divergent pair made the two sites wipe each other's stamp and refetch every 5-minute tick, fleet-wide). If you ever see two_fetch_versionedlines whose destinations share a basename, that is a defect, not a convention: the stamp namespace is keyed on the basename, so the pair will flip-flop. A tool's single line lives in_update_boa_toolsif every box needs it, or in_update_agentsif only installed Skynet-active boxes do. - A serial mistake is corrected by decrementing again — never by putting an earlier value back.
_update_agents vs _update_boa_tools — two run contexts, disjoint lists
The split is by audience, not by duplication (the old "duplication is intentional, bump both together" contract is retired along with the function latch):
_update_boa_tools |
_update_agents |
|
|---|---|---|
| Runs | Always, every tick — fresh installs, _SKYNET_MODE=OFF, installed boxes |
Only with SKYNET active on an installed box (/var/log/barracuda_log.txt) |
| Function gate | none — every tick, each file self-gates on its own stamp | none — every tick, each file self-gates on its own stamp |
| Coverage | the core /opt/local/bin CLI tool set every box carries (incl. websh, the backup tools, the synproxy family) + /usr/local/bin privileged fix-drupal-* helpers |
/var/xdrago monitors (scan_nginx.sh, nginx_guard.sh, …), system scripts (clear.sh, owl.sh, runner.sh, night jobs), backup runners, unbound-helper, and a few bin tools scoped to such boxes (edgetest, clearwebbans, defaultkeys, bee, the build-box pair) |
| Serial change required | fNN per file — nothing else | fNN per file — nothing else |
Whole-release rides: lib/functions and the build tree
lib/functions/*.sh.inc carry no fNN and are never fetched per-file.
They reach a box only inside a full installer run:
- The
barracudawrapper (itself a serial-gated tool) mapsup-lts/up-pro/up-devto_tReeand sets_rgUrl="https://files.boa.io/versions/${_tRee}/boa",_rlsE,_bRnh(_set_tree_vars)._proceedthen fetchesBARRACUDA.sh.txtandlib/settings/barracuda.sh.cnffresh into/var/backups/and calls_up_action, which seds the release pins into the fetched settings file:_X_VERSION=${_rlsE},_BRANCH_PRN=5.x-${_tRee},_AEGIR_VERSION=${_tRee}.boaandoctopusdo the equivalent for their chains. BARRACUDA.sh.txtsources the fetchedbarracuda.sh.cnf, then_download_helpers_libsstages the whole build tree at_bldPth=/opt/tmp/boa:_DL_MODE=BATCH(default) pullsboa.tar.gzfromhttps://<mirror>/dev/<tree>/via_get_dev_ext(10 attempts, 9 s apart);_DL_MODE=GITclones${_BOA_REPO_GIT_URL}/boa.gitat${_BRANCH_BOA}instead.- The include list is fixed:
_FL="helper dns system sql valkey redis nginx php solr master xtra firewall hotfix", each sourced from${_bldPth}/lib/functions/with_panic_exiton a missing file. _xdrago_install_upgrade(lib/functions/system.sh.inc) then wholesale copies${_bldPth}/aegir/tools/system/*over/var/xdrago/, after snapshotting the old tree and root crontab to${_vBs}/dragon/…-pre-${_xSrl}-${_X_VERSION}-${_NOW}.
Consequence for shipping: a monitor or system script in the fNN set updates
both ways (serial fetch within minutes, wholesale copy at the next full
upgrade); a lib/functions change updates only at the next full
barracuda up-* — there is no faster path, so never put an urgent fleet fix
solely in lib/functions if a serial-gated tool can carry it.
The _*_VRN block at the top of BARRACUDA.sh.txt (_CURL_VRN,
_NGINX_VRN, _CSF_VRN, _GIT_VRN, _MSS_VRN, …) is the software-pin layer
of the same ride: the pin is defined there, but the build step that consumes it
lives in the sourced lib/functions/*.sh.inc, and its build/rebuild stamp
embeds the pin value together with _xSrl/_X_VERSION (e.g. the MySecureShell
build in lib/functions/system.sh.inc gates on
${_pthLog}/mss-build-${_MSS_VRN}-${_xSrl}-${_X_VERSION}.log).
Changing a pin therefore re-triggers exactly that component's build on the next full run.
Mirrors and the _crlGet convention
Every fetch in the pipeline goes through one flag set, defined near the top of
BOA.sh.txt and duplicated verbatim in the barracuda/boa/octopus
wrappers and the BARRACUDA.sh.txt/OCTOPUS.sh.txt meta-installers (the
lib/settings/*.sh.cnf files inherit it from the sourcing *.sh.txt, they do
not redefine it):
_crlGet="-L --max-redirs 3 -s --fail --retry 9 --retry-delay 9 -A iCab"
_wgetGet="--max-redirect=3 -q --tries=9 --wait=9 --user-agent='iCab'"
--fail is load-bearing (non-2xx → empty destination → _fetch_versioned
rollback instead of an HTML error page saved as a tool).
-A iCab is the fetch identity every BOA downloader sends; the mirror cluster
is operated around that convention, so when hand-testing a mirror URL, test with
curl -A iCab — a default UA is not a representative probe.
Mirror selection: _find_fast_mirror_early seeds
/var/backups/boa-mirrors-2026-05.txt with the current pool —
files.boa.io (default/fallback), files.o8.io, files.host8.biz — and
runs ffmirror to pick the fastest; the winner becomes _USE_MIR.
ffmirror probes each candidate with a hard two-second timeout and a single
try, so a mirror that accepts a connection and then goes silent cannot stall
the run probing it — that run may be holding a box-wide guard (the 5-minute
channel, the nightly worker, the user-management sweep, a migration tool), so
the bound matters fleet-wide, not just at install time. And when no candidate
answers in time it returns the first listed mirror rather than an empty
string, so mirror selection never yields an empty hostname or a malformed
fetch URL.
The list filename is date-stamped, so shipping a new pool means a new filename (old boxes regenerate rather than merge). Two URL bases derive from it:
_urlHmr="https://${_USE_MIR}/versions/${_tRee}/boa/aegir"— per-tree tool and config fetches (paths mirror the repo'saegir/subtree)._urlDev="https://${_USE_MIR}/dev"— source tarballs andboa.tar.gz.
For the pro and dev trees, _verify_boa_keys additionally checks a
per-hostname key (enc/2024/<md5(hostname)> on the mirror) before
proceeding; lts has no licence gate.
How a tagged release propagates
Tags exist only on 5.x-pro and 5.x-lts; 5.x-dev (with its -base /
-edge feeders) is consumed untagged by dev-tree boxes. The mirror cluster
serves each tree's installers, tools and boa.tar.gz under
versions/<tree>/ and dev/<tree>/; tagging a release is what promotes the
new file set into the pro/lts trees (mirror-side publication — the repo
carries the content, the tag marks it releasable).
From the box's point of view, everything after that is the mechanics above:
- Within one 5-minute tick,
clear.shstreams the newBOA.sh.txt. Its new identity block does the rest: a bumped_xSrlinvalidates every ctrl stamp, so the full tool/monitor set refetches once; individually decremented fNN serials refetch just their files — between releases as promptly as at one. -
The same tick runs
autoupboa, which maintains the scheduled full-upgrade cron entries (barracuda up-${_AUTO_VER} …/octopus up-${_AUTO_VER} …in/etc/crontab) from the_AUTO_UP_*variables, and the weekly wrapper mode (autoupboa weekly-system up-<tier> system … noscreen). Two populations, one mechanism:- Hosted-fleet boxes (
_if_hosted_sys—/root/.host8.cnfpresent or hostname ending.aegir.cc):autoupboahard-sets the schedule in-code, with the full-upgrade date parked on_AUTO_UP_MONTH=2/_AUTO_UP_DAY=29(Feb 29 — a leap-day-only slot) while the weekly system upgrade stays live; release ops edit the shippedautoupboato open a real fleet window and re-park it afterwards. - Operator boxes: the same variables are read from
/root/.barracuda.cnf— no month+day set means_crontab_cleanupstrips the full-upgrade lines.
The crontab rewrite is itself a one-shot per
${_tRee}.${_xSrl}(an up-countingvNNctrl stamp), reset when the/root/.turn.off.auto.update.cnflock toggles; dev-server boxes (/root/.dev.server.cnf) never get auto-upgrade lines.That weekly wrapper line is also where a PHP 7.4
intlself-heal rides. On each weekly runautoupboa weekly-systemchecks for a 7.4 that an ICU bump left withoutintl, and only then idles the inactive PHP versions (barracuda php-idle disable) to shrink the rebuild surface before running the upgrade twice: the pin pass pins ICU to a 7.4-safe version and rebuilds the active PHPs onto it (7.4 regainsintl), then the unpin pass drops the pin and rebuilds the 8.x versions back onto the OS-default ICU while 7.4 keeps its pinned-ICUintl. It is self-limiting — once 7.4 carriesintlthe plain upgrade runs — and tree-independent, and the orchestration lives only inautoupboasobarracudastays a leaf call that cannot self-loop. Shipping it to existing boxes is a paired serial bump in one commit: decrementautoupboa's fNN (single fetch line in_update_boa_tools) so the new script lands per-file, and increment the up-counting crontab-updatevNNmarker so each box regenerates/etc/crontabonce and picks up the routedweekly-systemline. - Hosted-fleet boxes (
- Nightly,
owl.shcomparesbarracuda-release.txtagainst/var/log/barracuda_log.txtand mails the operator until the box has been upgraded. - The full stack upgrade — new
lib/functions, rebuilt services,/var/xdragowholesale refresh — happens whenbarracuda up-lts|up-proactually runs: manually, or at the scheduledautoupboawindow.
So: tools and monitors converge fleet-wide in minutes; the deep stack converges at the next full run. Design any fix's rollout around that split.
Shipping checklist (serial mechanics only)
- Tool under
aegir/tools/bin/oraegir/tools/system/changed → decrement its fNN inBOA.sh.txtat its single fetch line (grep the tool name; exactly one hit is the invariant — two hits sharing a basename is a defect). Same commit as the change. - Never increment or re-use an fNN value; never copy a serial from another tool.
-
_update_boa_toolsbody changed → nothing extra: the function runs every tick; the per-file serials are the only knobs. (The old_bTsfunction serial is retired.) -
lib/functions/*.sh.inc/lib/settings/*.sh.cnf/ template change → no serial exists; it rides the next fullbarracuda up-*. If the fix is urgent fleet-wide, carry it (also) in a serial-gated tool. - New tool → add a
_fetch_versionedline starting atf99, plus thechmodgroup and symlink block in_update_boa_toolsif it is operator-facing. - Verify fetch URLs against the repo layout (
_urlHmrmaps to theaegir/subtree) and test withcurl -A iCab.
Related
- Code style & conventions — the lint gates and commit rules every serial-bumping commit must also pass.
- Contributing — how changes reach the public branches this pipeline publishes from.
- Reference appendix — consolidated
_VARtables, including the_AUTO_UP_*scheduler variables.