Files-symlink tools — autosymlink, sudo wrapper, symlinkinfo
Three executables do the work described in the subsystem overview:
autosymlink (the root worker), fix-drupal-site-symlinks.sh (the hardened sudo
entry point Provision calls), and symlinkinfo (the read-only forensic query).
autosymlink and symlinkinfo live in /opt/local/bin; the wrapper lives in
/usr/local/bin with the rest of the fix-drupal-*.sh family. All three run as
root — the per-account static store is root-managed by design.
autosymlink (and its scheduler updatesymlinks, owned by
Nightly automation) predate the native subsystem as
batch-only precursors; the narrow single-site mode, --force-unshare, the
--batch-if-clean discipline and the safety properties below are what turned them
into the subsystem's tools. Sources: aegir/tools/bin/autosymlink,
aegir/tools/bin/fix-drupal-site-symlinks.sh, aegir/tools/bin/symlinkinfo.
autosymlink — the worker
Converts a site's real sites/<url>/files and sites/<url>/private directories
into symlinks pointing at the owning account's store
(/data/disk/<account>/static/files/<url>/{files,private}), detects and breaks
accidental cross-site sharing, and reports orphaned stores. Everything it does —
plans, applies, warnings, reports — goes to /var/log/boa/autosymlink.log.
Global modes
| Invocation | Mode | Effect |
|---|---|---|
autosymlink |
DRY | Default. No changes; plans every conversion, runs the space checks, and records in the state file whether the run is CLEAN (no errors, no edge cases). |
autosymlink report |
REPORT | Read-only report of shared files/private symlinks, including [REPORT] ORPHAN lines for store entries with no matching active site. |
autosymlink live |
LIVE | Apply, with a per-site confirmation prompt. |
autosymlink batch |
BATCH | Apply to every site with no prompt — allowed only after a clean DRY run (below). |
autosymlink --batch-if-clean |
BATCH_IF_CLEAN | DRY pass, and if CLEAN, an immediate BATCH pass — the cron-safe one-shot the nightly automation uses. Exits 10 (no apply) when the DRY pass is not clean. |
The batch gate is the state file /var/log/boa/autosymlink.state: batch refuses
to run (exit 1) unless the file records _LAST_MODE=DRY with
_LAST_STATUS=CLEAN, and it invalidates that status the moment it is consumed, so
one clean DRY authorises exactly one batch. A DRY run flagged NOT CLEAN (any
[WARN]/[ERROR] or unresolved edge case) blocks batch until the cause is fixed
and DRY re-run.
Global sweeps iterate /data/disk/* accounts that carry tools/drush, skipping
/data/disk/arch. The master hostmaster account is never touched — only Octopus
accounts are in scope, so the master keeps plain directories.
Narrow single-site mode
autosymlink --site <url> [--account <oNNN>] [--apply] [--force-unshare]
This is what the Provision install, clone and migrate/rename hooks reach through the sudo wrapper, and the mode to use for one-off fixes:
--site <url>scopes the run to one site. Without--applyit is a read-only per-site DRY.--account <oNNN>— with an explicit account the tool trusts the Drush alias alone as the "site exists" marker. That is deliberate: the alias is written byprovision-save, but the nginx vhost only appears at the site's first verify, so the install hook runs before any vhost exists. Without--account, the account is auto-resolved by locating the one/data/disk/*account holding both the alias and the vhost for that site; no pair, no action.--applymirrors the global discipline at site scope: it runs its own per-site clean dry-run first and applies only if that dry-run is clean — without ever reading or writing the global CLEAN-DRY state file, so a narrow run can never authorise or poison a later global batch.--force-unsharebreaks an inherited cross-site/cross-account link even when ashare.files.<site>.infocontrol file exists — used by cloning and migrate/rename, because a freshly cloned/renamed site never opted into sharing.
Both tokens are validated fail-closed before anything runs (site
^[a-z0-9][a-z0-9._-]*$, account ^[A-Za-z0-9._-]+$, no .., no /).
A narrow --apply defers while the nightly maintenance pause marker
(/etc/boa/.pause_tasks_maint.cnf) is present, so a task-time conversion never
interleaves with the batch sweep on the same store; the nightly batch in turn
bails while a provision/clone process runs, so the two cannot overlap. Exit codes:
always 0 (done or safe-skip), except 3 — a non-fatal advisory meaning a
requested clone unshare did not complete (deferred, dry-run not clean, or the copy
failed) and the site may still share the source's files. That is the case behind
the [ALERT] Clone unshare for <site> not completed ... manual review required
log line; the recovery command is in the wrapper section below.
Safety properties
- Crash-safe conversion. The move is rc-checked: on any
mvfailure the real directory stays in place and no symlink is created. The only non-atomic window (between a successful move and theln -s) is closed by self-heal on the next run: store holds the data, in-site link missing → the link is recreated. Concurrent converters are corruption-safe — the losingmvfails because the source is already gone. - Owner-matched links. Each new symlink's own owner:group is set with
chown -h --referenceto its store target. On a clone the unshare'srm+ln -sis the last step with no perms-fix behind it, so without this the link would lingerroot:rootuntil the next verify. - Filesystem-aware space checks. Source and target devices are compared via
df -P: same filesystem means a move is a free rename and needs no headroom; cross-filesystem moves and break-sharing copies get a realdu-measured check, with cumulative per-filesystem reservations summarised at the end of the run so a planned batch cannot overcommit storage. The target device is resolved by walking up to the nearest existing ancestor — a brand-new account with no store base yet previously produced a false "Unable to determine available space" DRY failure that permanently blocked its nightly auto-convert. - Stale stores archived, never overwritten. If a conversion finds an existing
store target from a prior lifecycle of a reused site name, it is moved aside to
static/files/.archived/<stamp>/<site>/before the move — a defined, non-destructive remedy, so the narrow install/clone apply proceeds instead of skipping. Layout, accumulation alert and pruning are on Orphans & archiving.
fix-drupal-site-symlinks.sh — the privileged entry point
Provision runs unprivileged, and the store is root-managed, so the install/clone/
migrate hooks cannot call autosymlink directly. This wrapper
(/usr/local/bin/fix-drupal-site-symlinks.sh, 700 root:root) is the NOPASSWD
sudo entry point that lets aegir and the per-account oN users trigger the
root-level conversion for one site without any path to the global modes:
sudo /usr/local/bin/fix-drupal-site-symlinks.sh --site=<url> [--account=<oNNN>] [--force-unshare]
Hardening, all fail-closed:
- The
autosymlinkpath is hardcoded (/opt/local/bin/autosymlink); the caller can only influence which site/account is passed through, never the mode. - Only
--site=,--account=,--force-unshareand--helpare accepted; any other argument exits1. - Both tokens are validated with the same charset allowlists as
autosymlinkitself (no traversal, no path separators) before the value reaches a root tool, andautosymlinkre-validates them. - It forwards only the narrow single-site
--apply— it cannot be coaxed into batch or live, which operate on every account on the box.
This is also the documented manual recovery when a clone or migrate logged the
[ALERT] Clone unshare ... not completed line (typically low disk at clone time):
rerun sudo /usr/local/bin/fix-drupal-site-symlinks.sh --site=<clone> --force-unshare once space is available — see
Site cloning.
Wiring on the box:
- Sudoers — NOPASSWD entries for
aegirand each per-account user are written to/etc/sudoers.d/fix-drupal-site-symlinks(0440) during install/upgrade, alongside the rest of thefix-drupal-*.shfamily (lib/functions/satellite.sh.inc,lib/functions/master.sh.inc). - AppArmor — every PHP profile (
opt.php56.bin.phpthroughopt.php85.bin.php) carriesmrixexec edges for both the wrapper and/opt/local/bin/autosymlink, so confined FPM/CLI PHP can exec them. See AppArmor.
symlinkinfo — read-only history query
Answers, for one or more sites, was it symlinked and when, was its store archived (and where), and what state is it in right now — from the autosymlink logs plus the live filesystem. It changes nothing.
symlinkinfo bar.tkm.cc # one site
symlinkinfo foo.tkm.cc clone.foo.tkm.cc # several
| Option | Default | Effect |
|---|---|---|
--logs-only |
— | Parse logs only; skip the filesystem scan. |
--fs-only |
— | Scan the filesystem only; skip the log parse. |
--log-dir DIR |
/var/log/boa |
Where the autosymlink logs live. |
--disk-root DIR |
/data/disk |
Aegir account root to scan. |
Per site it reports:
- Symlinked — every
files/privateconversion, with timestamp and store target. - State NOW —
active(alias + real-docroot vhost),disabled(alias + placeholder vhost — files kept live, never archived),deleted(neither alias nor vhost: an orphan), orpartial(only one of the two survives). - Store NOW — the current store dir, and whether the in-site path is a live symlink right now.
- Archived — each archive event (name-reuse vs deleted-site orphan) with its
static/files/.archived/<stamp>/…path, plus archived copies still present on disk with sizes, so they can be located or pruned if not yet purged.
It reads /var/log/boa/autosymlink.log* and
autosymlink.verbose.archive.log* including rotated .gz copies, plus
/data/disk/*/static/files[/.archived]. Sites are matched as whole path tokens,
so foo.tkm.cc never pulls in clone.foo.tkm.cc; malformed site names are
skipped fail-closed.
Troubleshooting — "a new site is not getting symlinked"
Check, in order:
- Wrapper installed?
ls -l /usr/local/bin/fix-drupal-site-symlinks.sh— expect700 root:root. Missing means the box has not fetched it yet (see Deployment below). - Sudoers entry?
grep -r fix-drupal-site-symlinks /etc/sudoers.d/should list the account user (andaegir). - Kill-switch present? Box-wide
/data/conf/disable_native_files_symlink.cnfor the account'sstatic/control/no_native_files_symlink.infodisables the subsystem on purpose — see Configuration. - Account in scope? Only
/data/disk/<account>(Octopus) accounts are symlinked; the master hostmaster account keeps plain directories. - Tool ran? Look for the
[native-symlink]line in the install task log and a matching entry in/var/log/boa/autosymlink.log.symlinkinfo <site>sums this up in one shot. - Store ownership? The store dir should be
<account>:www-data; the in-site symlink itself is owner-matched to the store target byautosymlink, so it should never be leftroot:root.
Deployment
autosymlink, updatesymlinks and symlinkinfo reach every box through the
regular SKYNET self-update: serial-gated _fetch_versioned fetches in
_update_agents (mirrored with matching serials in _update_boa_tools, which
runs even before BOA is installed and when _SKYNET_MODE=OFF exits before
_update_agents is reached, so it must fetch them independently), landing in
/opt/local/bin with chmod 700. The privileged wrapper is fetched per-file by
_update_boa_tools and also copied into place with the rest of the
fix-drupal-*.sh family by _aegir_bin_extra_check_fix during install/upgrade
(700 root:root). If a freshly bumped tool does not appear, the box simply has
not run its daily self-update yet.
Related
- Overview — store layout, event table, privilege model, kill-switches and deliberate sharing.
- Nightly automation —
updatesymlinks --auto-fix/--orphan-report, the cron cadence and--debug. - Configuration — the
.barracuda.cnftoggles and every control file the tools honour. - Orphans & archiving —
.archived/layout, the accumulation alert, operator-only pruning. - Site cloning — the clone unshare
and the
[ALERT]line these tools back. - AppArmor — the
mrixexec edges for the wrapper andautosymlink. - Task failures — the task-log side of a failed install/clone.
- Reference appendix — the auto-generated command
catalogue these
commands:entries feed.