Adopting vanilla Ægir into BOA — aegir2boa
aegir2boa adopts a vanilla Ægir 3.x estate — the classic community Ægir on
Apache or Nginx, everything under /var/aegir, a single aegir system user —
into a BOA Octopus account on a separate BOA box. It is the vanilla
counterpart to the BOA-to-BOA xoct move, and it exists because a vanilla
source has none of the control files, agents, or conventions xoct relies on,
and because converting a vanilla box to BOA in place has no cheap revert.
The safe shape is the one xoct already uses: adopt the estate remotely
onto the target, then demote the old box to an HTTP(S) proxy whose per-site
revert is a single file move. Nothing on the source is destroyed — the original
configuration stays on disk untouched until you retire the machine, and every
step is reversible until the final DNS cutover.
The migration is three acting stages plus a read-only discovery phase:
stage 0 aegir2boa-preflight read-only discovery; PASS/WARN/FAIL per stage
stage 1 aegir2boa-stage1 in-place Apache -> Nginx flip on the source
stage 2 aegir2boa-stage2 remote adoption into BOA + proxy window
stage 3 (manual runbook) final DNS cutover + source decommission
Stage 1 exists because the whole stage-2 source-side machinery — the vhost swap, the 503 pause, the proxy templates — is nginx-vhost-based. Vanilla Ægir supports Nginx natively, so the first real step flips the box to Nginx; the stage-1 revert is cheap (flip back to Apache — both stacks stay installed until sign-off).
The toolset
| Tool | Runs on | Role |
|---|---|---|
aegir2boa-preflight |
source (vanilla) | read-only discovery + stage gates |
aegir2boa-stage1 |
source (vanilla) | Apache→Nginx flip, revert, status |
aegir2boa-stage2 |
both boxes | the migrator; dual-resident, verbs validate the box class |
All three ship inside the BOA tree at aegir/tools/bin/, but — unlike xoct,
xcopy, and xmass — they are deliberately not registered for fleet fetch.
No BOA box downloads or runs them by itself, and they never self-update. Place
them yourself (scp from a box that already has the BOA tree, or pull from the
files mirror), chmod 755, and run as root.
aegir2boa-stage2 is dual-resident: put the same script on the source and
the target. Its source verbs refuse to run on a BOA box and its target verbs
refuse to run on a vanilla box, so the same binary can never act on the wrong
side. The source-side tools assume nothing modern — bash 3.2/4.1-era and PHP
5.3-era safe — so an old vanilla box is fine; the target verbs assume a healthy
BOA box.
Validation status — read before using on a client box
Proven end-to-end on disposable VMs: both stage-2 routes, every revert path (single-site, full two-box, target reset, db-import undo, resume), the stage-1 flip/revert/re-flip, and public serving through the proxy window. That drill estate was Drupal 7 only and HTTP only. Which leaves, honestly:
- The HTTPS proxy window is undrilled. The per-site proxy cert store, the
https proxy vhosts, and the
cert-syncrefresh loop are implemented but have not been exercised on an ssl-bearing estate. Rehearse them on a disposable estate with real https sites before migrating any client box that has https. - Drupal 6 and Drupal 8+ composer platforms are untested. They route to the per-site path by design; the D6 PHP-pool gating exists in code only so far. Drupal 7 estates are the proven surface.
- Pre-3.x Ægir sources are refused by the preflight floor — recognised and named, never mangled. There is no supported path for them yet.
- Panel-domain continuity is not implemented. The adopted panel lives at
<oN>.<target-fqdn>; the old panel URL goes dark (a permanent 503) at proxy time. Communicate the new URL to the client.
Safety model
The doctrine holds across every acting verb, every stage:
- Every acting verb is a DRY RUN by default. Append
--liveto act.--liveis accepted only after a clean dry run of the same verb and scope on the same box, and it consumes that clean token on entry — one dry run arms exactly one live run, and any failed live run forces a fresh dry run against the changed box before you can retry. - The serving path is never cut before its replacement is functionally proven.
The stage-1 flip probes real PHP through nginx+FPM on a scratch port while
Apache still serves; the stage-2 proxy swap refuses any site the target does
not already answer for; every nginx change is
nginx -t-gated with automatic restore on failure. Every pre-handover failure is therefore a no-outage abort. - Frontend tasks are polled by latest vid only (finished Ægir tasks retain stale status rows), and the tools never force-run the task queue.
Every verb is idempotent behind marker files and safe to re-run. The preflight
and the stage-2 tool take a coarse per-scope lock against concurrent runs;
stage 1's guard is its consumed dry-run token. The logs —
/var/log/aegir2boa-stage1.log and /var/log/aegir2boa-stage2.log, both falling
back to /tmp if /var/log is not writable — are the first place to look on any
failure.
Prerequisites
- Whitelist the source IP in CSF on the target before any key exchange. The
target's lfd reads an
ssh-keyscan/ first-contact burst as abuse and temp-blocks port 22 — proven the hard way. On the target:
echo "source-ip # aegir2boa migration" >> /etc/csf/csf.allow
echo "source-ip # aegir2boa migration" >> /etc/csf/csf.ignore
csf -ra
# if the source already tripped a temp block:
csf -tr source-ip
- Root SSH key exchange source → target (
ssh-copy-id root@target-ipfrom the source, or append the source's/root/.ssh/id_ed25519.pubto the target'sauthorized_keys). Use a dedicated, commented key so it is easy to revoke at decommission;checkverifies that BatchMode ssh works. - A fresh preflight report on the source. The report lands under
/tmp, which is tmpfs on many boxes — a reboot eats it, andcheckrefuses a report from a different host. Re-run the preflight after any reboot, and copy the.txt/.envpair somewhere durable for the record. - Stage 1 signed off: the source serves the whole estate on Nginx.
- Target PHP pools for the estate's needs (a D6 site needs a php56 pool on
the target;
checkgrades this per site and later skips a site whose pool is missing rather than blocking the run). - Disk headroom: per site roughly 2× its DB size free under
/var/aegiron the source for dumps; the whole estate + 500 MB free under/data/diskon the target (transfermeasures and refuses without it). - A shared target box can have other provisioning actors (billing
automation, another operator). The existence gates catch a collision, but never
pre-assume the next free
oNaccount name — check first, and expect a dry run to fail honestly if the name got taken between your check and your live run.
Stage 0 — discovery preflight
Run on the source, as root:
aegir2boa-preflight # writes report + machine contract under /tmp
aegir2boa-preflight --help # options; --aegir-root for non-standard layouts
Read-only by contract: it writes only under /tmp, installs nothing, changes
no service, and its SQL is SELECT/SHOW-only. The one sanctioned exception is
the optional drush @hostmaster status health check (a Drupal bootstrap writes
cache tables); set A2B_NO_DRUSH=1 to suppress it — at the cost of a permanent
frontend_bootstrap_failed WARN in that run. --aegir-root points the probe at
a non-standard layout.
Outputs, freshly timestamped every run:
/tmp/aegir2boa-preflight.<host>.<timestamp>.txt human report
/tmp/aegir2boa-preflight.<host>.<timestamp>.env machine contract
The report opens with the two verdicts that gate everything:
- STAGE 1 / STAGE 2: PASS — proceed.
- WARN — proceed with named caveats. WARN does not mean non-migratable; it lists items to resolve or consciously accept. A typical healthy Ægir-on-Nginx box (after the stage-1 flip) reports WARN on both stages.
- FAIL — do not proceed with that stage. Reasons are named tokens, e.g.
cluster_pack_topology,dump_headroom,boa_box_not_vanilla.
Three hard floors refuse the box with exit 3: a BOA box (use xoct
instead), a non-Ægir box, and pre-3.x Ægir. Most are caught before deep probing;
a pre-3.x box recognisable only by its hostmaster DB schema is refused after the
full probe pass. A2B_ALLOW_BOA=1 exists only for inspecting a BOA box's data
out of curiosity — it never authorises a migration, and the verdicts stay FAIL.
Exit codes: 0 report emitted, 2 precondition failure (not root, lock held,
bad usage), 3 floor bail (the report is still written).
The .env machine contract (shell-safe scalars plus TSV tables) is the sole
input the later stages trust — stage 2's check sources its verdict scalars
rather than re-discovering the box. Keep the pair with the migration record.
Stage 1 — Apache to Nginx, in place
Run on the source, as root:
aegir2boa-stage1 --flip # dry run: gates + baseline capture
aegir2boa-stage1 --flip --live # the flip
aegir2boa-stage1 --status # both config planes + daemons + per-site HTTP
aegir2boa-stage1 --revert # dry run for the way back
aegir2boa-stage1 --revert --live # nginx -> apache
Scope: a concrete http_service_type of apache only. apache_ssl is
wired but experimental; cluster and pack topologies are refused — flip each
member box instead.
What the live flip does, in order: installs nginx+php-fpm without starting
daemons (a temporary policy-rc.d guard; Apache keeps :80 throughout),
starts the versioned FPM pools, adds the aegir user's sudoers line for the
exact nginx reload binary the backend will call, enables the hosting_nginx
frontend feature, flips the server node's http_service_type exactly as the GUI
radio would (queueing the server verify that writes the nginx config tree), waits
out the platform verify cascade, then verifies every site (hostmaster first —
the cascade stops at platforms, so this per-site loop is what populates
nginx/vhost.d), asserting each site's nginx vhost file exists. Only then:
nginx -t, a scratch-port FCGI probe that executes real PHP through nginx+FPM
while Apache still serves, and the daemon handover (stop+disable apache,
start+enable nginx and FPM — reboot-persistent). Finally every site's HTTP code
is compared against the pre-flip baseline.
The revert proves the Apache config without binding a port while nginx still
serves — the dry run parses a temp wrapper conf with apache2 -t -f; the live
revert runs a2enconf aegir + apache2ctl configtest before nginx stops, then
hands the daemons back in reverse order and flips the config plane back.
Do not prune the Apache tree during the stage-1/stage-2 window. Provision
never deletes it, and it is your way back — so until sign-off, leave
/var/aegir/config/server_master/apache*, the apache2 /
libapache2-mod-php packages, and /etc/apache2 state all in place.
Timing from the drill (with the hosting-queued daemon running): flip ≈ 65 s, revert ≈ 43 s, re-flip ≈ 54 s. On a cron-dispatch-only box every queued verify waits for the next cron minute, so expect materially longer. If a flip completes with sites differing from baseline, the tool prints the instant daemon-level fallback:
service nginx stop; a2enconf aegir; service apache2 start
Stage 2 — remote adoption
Place aegir2boa-stage2 on both boxes. The verb surface, verbatim from
--help:
Source-resident verbs (vanilla box, root):
check --target <ip> [--route per-site|db-import] [--report <env>]
pre-mig --target <ip> [--live]
create --target <ip> --account <oN> --email <e> --tree <dev|lts|pro>
[--option EDGE] [--subscr M] [--cores 1] [--live]
export --site <dom>|--all [--live]
transfer --target <ip> --account <oN> --site <dom>|--all [--live]
proxy --target <ip> --site <dom>|--all [--accept-http-diff] [--live]
cert-sync --target <ip> --account <oN> [--install-cron] [--live]
revert --site <dom>|--all [--live]
resume [--live]
status
Target-resident verbs (BOA box, root):
import --account <oN> --route per-site|db-import
[--site <dom>|--all] [--source-fqdn <fqdn>]
[--welcome-node] [--live]
import --account <oN> --revert-db-import [--live]
import --account <oN> --reset-sites [--live]
target-status --account <oN>
The two routes
Stage 2 ships two adoption routes; check decides eligibility from discovery
output — the route is never operator-asserted:
- Per-site adoption (the DEFAULT). Each site is registered natively on the
target through the provision import ladder (
hosting-import+ verify), one at a time. Risk-isolated — one bad site fails alone — with BOA-native node identity from the first write, and it needs none of the db-import reconciliation machinery. Cost: O(n) tasks, and the source frontend history (clients, users, task log) is not carried over. -
db-import (the validated bridge for homogeneous current-3.x estates only). The whole vanilla hostmaster DB is imported into the fresh account's panel and reconciled with a mandated package of deltas. It preserves frontend history and is estate-size-independent (one DB operation + one reconcile pass instead of per-site task chains).
checkcomputes the eligibility triad from the preflight report plus its own enumeration and refuses--route db-importif any leg fails:- every platform D7-class,
hostingschema_version at the high-water mark,- single-box topology (no cluster/pack, no remote web/db servers).
Two procedural gates ride along: the source must be in nginx mode (a universal
checkgate, both routes), and the enabled-but-code-absent module list must be acknowledged withcheck --accept-scrub-list—checkonly warns without the flag;transferis what refuses a db-import run without the recorded acknowledgment. Anything mixed, aged, or unknown takes the per-site route.
Order of operations
[stage 0/1 done: preflight WARN-or-better for stage 2, box on nginx]
check → pre-mig → create → export → transfer →
import (ON TARGET) → proxy → cert-sync --install-cron
Per-site downtime is the window from a site's export (a 503 goes up) to its
proxy swap (now served by the target through the proxy). On the db-import route
the frontend-history cutoff is the hostmaster dump timestamp — the queue is paused
from pre-mig, so nothing should post-date it.
Source automation is never resumed on the success path. After adoption the
source is a proxy shell; its hostmaster must never verify or regenerate vhosts
over the proxy files. resume exists only as part of a full-estate revert. The
consequence for a long, batched migration: a site still waiting on the source
gets no Drupal cron while the dispatcher is paused — size your batches so nothing
waits for weeks.
check — route + prerequisites (read-only)
On the source:
aegir2boa-stage2 check --target <target-ip>
# db-import candidates, after reviewing the printed module list:
aegir2boa-stage2 check --target <target-ip> --route db-import --accept-scrub-list
It validates and records, refusing on failure: the newest preflight report under
/tmp (root-owned, not world-writable, for this host, stage-2 verdict not FAIL
— pass --report <env> to pin one), that stage 1 is actually done (vhost.d
populated, nginx-mode type of record), that the current nginx config passes
nginx -t, that BatchMode root ssh to the target works, that the target looks
like a BOA box, and the target PHP pools. It computes db-import eligibility,
enumerates every enabled non-core module on the hostmaster (the scrub review
list), grades per-site PHP parity (a D6 site with no php56 pool on the target is
flagged and later skipped, not blocking), and records route+target for the
following verbs. Re-run check freely — it is always read-only.
pre-mig — pause the source automation
On the source:
aegir2boa-stage2 pre-mig --target <target-ip> # dry
aegir2boa-stage2 pre-mig --target <target-ip> --live
It comments out the aegir crontab's dispatch line (saved verbatim for resume),
stops+disables the hosting-queued daemon (systemd or init.d, prior state
recorded), and drains in-flight backend work (up to 10 minutes). From here the
estate is operationally frozen: no task dispatch, no queue daemon.
create — build the target account
On the source (it drives the target over ssh):
aegir2boa-stage2 create --target <target-ip> --account o1 \
--email [email protected] --tree pro # dry, then --live
It runs boa in-octopus on the target (defaults --option EDGE, --subscr M,
--cores 1), guarded for the window by the target-wide
.dont.upgrade.octopus.on.install.cnf flag so post-install upgrade churn cannot
race the migration, then waits for real quiescence (process checks, up to 30
minutes; typically a few). The dry run refuses if /data/disk/<oN> already
exists — on a shared box that catches another actor having taken the name. BOA
sends its welcome email with the panel/SSH credentials to --email: use the
address that should receive them (yours during the window; hand over to the
client later).
export — pause + dump each site
On the source:
aegir2boa-stage2 export --all # dry: per-site plan + skip reasons
aegir2boa-stage2 export --all --live
# or one site at a time: --site example.com
Per site, live: it captures the HTTP baseline, raises the 503 stub — a shadow
server block in nginx pre.d/ that wins over the real vhost (first-defined wins),
leaving the original vhost file untouched; nginx -t-gated with automatic stub
removal on failure — then dumps the site's DB with the site's own credentials
from its drushrc (no root DB access is ever needed on the source) into
/var/aegir/src/a2b/, and writes the site's manifest. It skips honestly, per
site: missing vhost or alias paths, a multi-host DB, unparsable credentials, a 443
vhost whose cert files are missing, or insufficient dump headroom. A failed dump
leaves the stub up (data consistency over uptime) — revert --site <dom>
unpauses it.
On the db-import route the estate export also dumps the hostmaster DB and puts the source panel into maintenance mode, so nothing post-dates the frontend snapshot.
transfer — ship everything to the target
On the source:
aegir2boa-stage2 transfer --target <target-ip> --account o1 --all # dry, then --live
It measures total size against the target's free /data/disk space (refuses
without need + 500 MB), then rsyncs each platform tree to
/data/disk/<oN>/static/a2b/<platform>/ chowned to the account (a source-uid
tree is unreadable to the account and breaks every later import), each site dump +
manifest to /data/disk/<oN>/src/a2b/, plus the ssl.d trees and the nginx configs
as reference copies (never into the target's live config — vhosts are
regenerated natively by verify tasks). Drush aliases are deliberately not
transferred on either route: vanilla aliases carry /var/aegir roots that would
poison the target; everything is regenerated fresh. The route marker ships on
every transfer; on the db-import route it additionally ships hostmaster.sql, the
acknowledged scrub list, and the source FQDN record (transfer refuses a db-import
run without a check --accept-scrub-list recorded).
import, per-site route
On the target:
aegir2boa-stage2 import --account o1 --route per-site --all # dry, then --live
# or per site: --site example.com
For the window it sets hosting_platform_automatic_site_import = 0 (restored at
the end) — a platform verify would otherwise auto-import every sites/ dir it
finds, colliding with the explicit imports. Then, per platform: provision-save a
platform_a2b_<name> context, create the platform node, and verify it (the verify
registers the install-profile package — a hard prerequisite for site imports). Per
site: create its DB and user with the site's own credentials (settings.php keeps
working unmodified; refuses to overwrite an existing DB), provision-save the site
context, hosting-import it and drive the import task (inline when re-running
against an already-registered node — a retry quirk the drill caught), map its PHP
version in the account's multi-fpm.info and wait for the pool socket (a D6 site
without its socket is a per-site FAIL; the site must not serve under the account
default), wait the chained verify, probe HTTP against the source baseline, and
re-enable the site's Drupal cron — adopted sites land with cron dark by BOA
design, and the source dispatcher that used to run it is paused, so leaving it
dark means silent job loss. One site's failure never blocks the next; failed sites
are listed and stay un-adopted.
import, db-import route
On the target:
aegir2boa-stage2 import --account o1 --route db-import # dry, then --live
# optional: --welcome-node to recreate a public frontpage
The dry run prints the full numbered plan. The live run opens the account
freeze window: the freeze marker (log/proxied.pid) makes every
nightly/periodic BOA agent skip the account, in-flight nightly passes are drained,
and the account's task dispatcher is held aside for the duration — a raw DB
import must never race task dispatch. If the import fails mid-way the dispatcher
deliberately stays held (a broken panel must not dispatch); only success,
--revert-db-import, or manual repair restore it. The steps, each idempotent
behind its own marker:
- Snapshot: dump the fresh panel DB to
undo/a2b-pre-import.sql(the revert point) and capture the enabled-module set intoundo/a2b-enabled-baseline.txt(the reconciliation source of truth). The import pre-checks roughly 2× the transferred hostmaster dump + 200 MB free under the account root for this snapshot — an extra headroom gate on top of the transfer one. - Drop, then load with sandbox strip: the target panel DB is dropped (an overlay import would leave orphaned tables) and the transferred dump streamed in minus the MariaDB ≥ 10.5.25 sandbox header line, which the Percona client rejects.
- Queue hygiene + neutering: stale/pending imported tasks are failed-out so nothing dispatches from the source's queue, and client welcome emails are switched off — the imported DB holds real client addresses.
- Module scrub (acknowledged rows only): enabled modules whose code is absent
from the panel platform are removed from
{system}— but only modules on thecheck-time acknowledged list; anything else aborts the import for review. On a standard vanilla estate this list is empty or tiny (BOA's panel platform carries code for everything a stock 3.x enables). - Reconcile: platform paths are re-pointed at the landed trees, the BOA module
set is re-enabled from the snapshot baseline,
updatedbruns (a no-op at schema high-water), features are reverted to code state, and per-site Drupal cron intervals are seeded hourly — vanilla's cron table is schema-less, so imported sites would otherwise have cron silently off forever. - Service restore: the Let's Encrypt service row (absent from a vanilla server
node) is re-attached; optionally a welcome frontpage node (
--welcome-node) — by default the imported admin-only frontpage stands. - Per-site content DBs are created and loaded with each site's own credentials.
- Identity surgery — targeted UPDATEs by nid, never a blanket rename. The
imported server node's title becomes the target FQDN (Ægir regenerates
remote_hostfrom that title on every server verify — a stale value would silently re-point the account at the old box over ssh); the panel site node adopts the account's own panel domain (<oN>.<target-fqdn>, the identity already on disk); the imported panel platform is re-pointed at the real one; the importedserver_localhostnode gets the target account's own DB credentials restored from the snapshot (the imported node carries the source's — its verify would otherwise poison the account alias with a dead DSN; this failure is FATAL). History rows keep old FQDN references by design and are reported, not rewritten. - Inline verify ladder, while the dispatcher stays held: server verifies (with
an assert that
remote_hostregenerated to the target FQDN), platform verifies, the panel site verify (FATAL on failure), then per site: PHP mapping, verify, and an HTTP probe against the source baseline. FPM socket waits are deferred until after the freeze lifts — the pool agent skips frozen accounts by design. - Close: panel bootstrap probe (FATAL if the panel does not bootstrap), stray cascade-queued tasks neutered, dispatcher restored, freeze lifted, deferred FPM waits + re-probes, done-marker written.
proxy — cut each site over
On the source:
aegir2boa-stage2 proxy --target <target-ip> --all # dry, then --live
# per site: --site example.com; accept a changed-but-2xx/3xx answer:
# --accept-http-diff
Per site, gated on the target actually answering for that site (an HTTP probe
against the manifest baseline; a differing 2xx/3xx needs --accept-http-diff,
anything else is a SKIP): the real vhost is moved aside to the dotfile .<domain>
(the revert artifact) and a proxy vhost pointing at the target is written in
its place; for https sites a per-site proxy cert store is seeded from the site's
existing cert files and an https proxy vhost added. nginx -t-gated: on failure
the swap is undone and the 503 stub keeps pausing the site. On success the stub is
removed — the site now serves live from the target through the source proxy.
proxy --all also puts the old panel domain behind a permanent 503 (both
routes): the panel identity moved to <oN>.<target-fqdn>, so the old panel URL
must not proxy anywhere. Tell the client the new URL.
The proxy templates are embedded, vanilla-adapted equivalents of BOA's own proxy
vhosts (BOA's originals need the BOA nginx build and Octopus cert paths, so they
cannot be dropped onto a distro nginx verbatim). The catch-all location also
forwards /.well-known/acme-challenge/ — so the target can mint and renew
real Let's Encrypt certs for domains whose DNS still points at the source, for the
whole proxy window.
cert-sync — keep the proxy's certs fresh (https estates)
On the source:
aegir2boa-stage2 cert-sync --target <target-ip> --account o1 --install-cron # dry
aegir2boa-stage2 cert-sync --target <target-ip> --account o1 --install-cron --live
For each proxied https site it pulls the target account's live LE cert pair into
the source's proxy cert store and reloads nginx (rollback to the previous pair if
nginx -t fails). --install-cron writes /etc/cron.weekly/a2b-cert-sync so a
months-long proxy window never serves an expired certificate — nobody renews on
the proxy box; it mirrors the box that does. Remove that cron at decommission
(stage 3). Remember this whole path is implemented but undrilled — see
Validation status.
Reverts — the way back, until DNS moves
Everything here is drilled. The one rule that matters: a reverted site must be re-exported before any retry — the moment it serves locally again its transferred dump is stale, and a cutover from a stale dump silently loses writes. The tools enforce this by clearing the export and transfer markers on revert.
Single site, on the source:
aegir2boa-stage2 revert --site example.com # dry, then --live
It restores the original vhost (the dotfile back over the proxy vhost), removes the https proxy vhost and the 503 stub, and reloads nginx. The site serves locally again as if nothing happened. It does not touch the target copy — reset that before any retry (below).
Whole estate: revert --all --live, then resume --live (restores the crontab
dispatch line and the hosting-queued daemon exactly as recorded, and clears the
source panel's maintenance mode). resume refuses while any site is still paused
or proxied.
On the target:
aegir2boa-stage2 import --account o1 --reset-sites --live # per-site route
aegir2boa-stage2 import --account o1 --revert-db-import --live # db-import route
--reset-sites drops exactly the site DBs this tool loaded (marker-fenced — it can
never touch a DB it did not create). --revert-db-import additionally restores the
panel DB from the pre-import snapshot and returns the held dispatcher. Both print
the same reminder: a retry needs a fresh export + transfer.
Monitoring a migration
aegir2boa-stage2 status # on source: route, markers, per-site [P E T X] + live HTTP
aegir2boa-stage2 target-status --account o1 # on target: route, freeze state, step markers
Source state lives under /var/aegir/log/a2b/ (state, manifests, per-site
markers), target state under /data/disk/<oN>/log/a2b/; dumps under src/a2b/ on
both sides; the pre-import snapshot under /data/disk/<oN>/undo/. A crashed run's
stale lock (/var/run/aegir2boa-stage2.*.lock) is taken over automatically once
its recorded pid is dead. Per-site dumps use mysqldump (not mydumper) with each
site's own credentials, and every remote action is plain root ssh + rsync. Locks
and markers are scoped per account+site, so one source can migrate into two target
accounts sequentially — but never run two acting verbs on the same scope
concurrently.
Stage 3 — DNS cutover and source decommission
The proxy window ends when DNS points every domain at the target and the old box is retired. Until DNS moves, the per-site dotfile revert exists; after it, the target's own backups are the safety net. Take this deliberately:
- Pre-lower TTLs. The preflight recorded the estate's DNS TTLs; get every A/AAAA record's TTL down (300–3600 s) at least one old-TTL period before the cutover, so the switch propagates fast.
- Repoint DNS for every migrated domain to the target IP. Site-by-site or estate-wide — the proxy keeps covering stragglers either way, so this is not a downtime event.
- Mail and network identity. A new IP invalidates any SPF
ip4:record pinned to the old box and needs PTR/rDNS set up at the new provider; if the old box received mail (MX) or ran authoritative DNS (BIND/hosting_dns), move those roles explicitly — the migration tools do not. - Re-home the jobs that watched the old box: monitoring/alerting, external backup jobs, anything in root's crontab an operator added. The source's Ægir automation is already paused; leave it paused.
- Verify traffic has left the proxy. Watch the source's nginx access logs go
quiet per domain (residual hits mean stale DNS caches or overlooked records), and
spot-check
dig +short <domain>against the target IP. - Retire the proxy. Host policy decides the window (days to months). When it
closes: remove
/etc/cron.weekly/a2b-cert-sync, archive what the record needs (the preflight.txt/.env,/var/aegir/log/a2b/, the migration log), snapshot the box if the provider makes that cheap, then power it off. - Clean the target: remove the source's CSF allow/ignore lines (
csf -raafter), drop the migration key from/root/.ssh/authorized_keys, and optionally the tool copy and the landedsrc/a2b/artifacts once the estate has run clean past a backup cycle.
Troubleshooting quick reference
| Symptom | Meaning / action |
|---|---|
check dies: no preflight report / wrong host |
run aegir2boa-preflight on this box now (reports are per-host and die with /tmp) |
check dies: stage-2 verdict FAIL |
resolve the named reasons; re-run the preflight |
check dies: current nginx config fails nginx -t |
fix the box first — the tool refuses to build on a broken config |
--live refused: no prior clean dry run |
run the dry form of the same verb+scope first (every failed live consumes the token) |
create waits forever |
another Octopus operation on the target; it times out at 30 min with a warning — verify quiescence manually before import |
export skips a site |
the printed reason (missing vhost/alias, multi-host DB, bad creds, missing cert files, headroom); fix or accept, re-run |
| dump fails mid-export | the 503 stub STAYS up (consistency over uptime); revert --site <dom> to unpause |
import (db-import) fails mid-run |
the dispatcher stays held on purpose; fix and re-run import (steps are idempotent), or --revert-db-import |
| site fails its verify/probe on the target | it is listed and skipped; proxy will refuse it (probe gate) — fix, re-import that site |
proxy skips a site: HTTP diff |
the target answers differently than the baseline; re-check the site on the target, or --accept-http-diff if the change is expected |
| FPM socket wait times out | the pool agent needs a pass (runs every few minutes; frozen accounts are skipped until the freeze lifts); D6 sites must not be proxied without their pool |
| lock held | another run of the same verb+scope is live; stale locks self-clear once the pid is dead |
| anything else | read /var/log/aegir2boa-stage2.log, then the named hostmaster task log (node/<nid> on the panel) |
Related
- Cross-host migration — xoct, xcopy, xmass — the BOA-to-BOA move this path is modelled on; reach for it when both boxes are already BOA.
- Firewall blocked (CSF/lfd) — clearing a port-22 temp block, relevant to the pre-key-exchange CSF whitelist.
- Security & isolation — CSF,
ip_access, and the realip layer a migration proxy interacts with. - See the Reference appendix for the consolidated variable, command, and control-file tables.