Cross-host migration — xoct, xcopy, xmass
Three root-side tools move accounts between BOA hosts. All three are fetched and
auto-updated by BOA.sh.txt via _fetch_versioned serials (plus a chmod 700
sweep), so the binaries live at /opt/local/bin/{xoct,xcopy,xmass} and all three
— xcopy included — self-refresh on every BOA update.
xoct |
xcopy |
xmass |
|
|---|---|---|---|
| Moves | one Octopus account | one Octopus account (duplicate) | the whole host (all accounts + Solr) |
| DB transport | mydumper / myloader |
mydumper / myloader |
xtrabackup snapshot + GTID replication |
| Source after | converted to proxy | stays live | converted to proxy |
| Write-freeze | http-off 503 |
none | http-off 503 |
xoctis a move (the source account is decommissioned to a proxy).xcopyis a copy (the source account keeps serving — nohttp-off, no source proxy conversion).xmassis a whole-server move for evacuating a box.
Account identity is preserved across the hop: BOA does not support cross-account
migration in the general case (o1 lands on o1, o2 on o2). The one rename axis
is xoct/xcopy's optional fourth argument, which renames the moved account on the
target (see the rename mode).
This page is the overview and the shared reference: the routed summaries below point
at the two per-tool runbooks, and the machinery all three tools share — the storage-aware
DRY/--live gate, the http-off write freeze, and the migration proxy — is documented
here once, so the runbook pages link back to it rather than repeat it.
The three tools in brief
xoct — single-account move
xoct moves one Octopus account across hosts and decommissions the source to a
migration proxy. It transports the databases with mydumper/myloader, so it is
cross-version safe across differing Percona versions, and freezes the source
with http-off for the export window.
→ Full runbook: xoct & xcopy — single-account move and copy.
xcopy — duplicate an account (source stays live)
xcopy shares xoct's dispatcher shape but is non-destructive: it writes no
http-off, never proxies the source, and finishes with a noproxy step — leaving a
point-in-time duplicate on the second host while the original keeps serving. Use it for a
staging clone of a whole instance or a pre-cutover rehearsal.
→ Full runbook: xoct & xcopy — single-account move and copy.
xmass — whole-server move
xmass evacuates an entire BOA host — every Octopus account, every Solr index, every
config — using an xtrabackup snapshot + MySQL GTID replication for the database layer,
so it scales to fleets of large databases where per-account dump/restore would be
impractical. It requires identical Percona versions on both ends.
→ Full runbook: xmass — whole-server move.
Storage-aware transfers and the DRY/--live gate
DRY by default, --live to act
xoct/xcopy transfer and pretransfer, and xmass sync and cutover,
default to a read-only DRY run: the tool resolves the target's attached mount,
prints a [DRY-PLAN] line per store, pre-checks disk space, and finishes with
[DRY] CLEAN or [DRY] NOT CLEAN, recording the result in a state file:
/var/log/boa/xoct.migrate.<oct>_<tgt>.state/var/log/boa/xcopy.migrate.<oct>_<tgt>.state/var/log/boa/xmass.migrate.<tgt>.state(xmasskeys per target only: one DRY plans all accounts plus shared)
An explicit --live is required to perform the migration and is accepted only after
a CLEAN dry run for the same account+target (per target for xmass); the clean-dry
token is consumed on use, so one dry run can never arm two live runs.
--dry/--test are explicit-DRY aliases on all three tools. These are flags, not
verbs — only ---prefixed flags are recognised, and an unknown --flag is now a
hard error on the account verbs rather than falling through to the positional
arguments (where the next free slot is the target-account rename, so a mistyped flag
would have become a rename instruction).
The gate covers only the file-transfer verbs — transfer/pretransfer on
xoct/xcopy, sync/cutover on xmass. Every other verb runs live with its
arguments untouched; that rule stays true as the dispatchers grow.
The DB/SQL steps — the mydumper/mysqldump export-import and the xtrabackup/GTID
pipeline — are never gated.
A second, independent gate rides on evidence rather than storage: an account's
export must prove itself before it may travel or be imported. The exported stamp lands
only after a per-site dump loop with zero failures plus a clean, non-empty panel dump;
failures are recorded in a per-account latch that travels with the transfer, and
transfer and import refuse while it is present — an explicit --force accepts a
partial export deliberately and loudly. The import records its own failure latch the
same way, and both tools end an incomplete run non-zero with an INCOMPLETE verdict, so
a harness can key on the exit status. Details on the
xoct & xcopy page.
All three tools are also single-flight per box: every state-mutating verb takes an
owner-PID lock (/run/<tool>.verb.pid) and a concurrent run refuses non-zero naming
the owning process, while the read-only verbs stay unlocked so a migration can always
be inspected. The guard checks liveness, not the file — a killed run wedges nothing.
And xmass refuses outright to migrate between boxes on different BOA releases:
prep-target compares both release stamps at first target contact and offers no
override (the fix is a full barracuda + octopus run on the older box).
xmass cutover in DRY mode does the storage-plan pass only, for all accounts plus
shared, and stops before any destructive step — no MySQL lock, no downtime.
Storage-aware placement
Store placement follows the target's disk reality, not the source's
(_xoct_handle_store, with twins _xcopy_handle_store / _xmass_handle_store):
- Target has a single attached
/mntmount → a store's contents are mirrored onto it (static/files→<mount>/files/<account>/static/files— using the destination account name, so a rename lands correctly;arch→<mount>/files/system/arch) and the on-target path re-pointed as a symlink — even if root has room. - No target mount → the store is de-referenced into a real directory on the target root.
- A real-dir source stays a real dir on the target root, using the target mount only as a space fallback.
A catch-all sweep applies the same rule to any other /mnt-anchored symlink in the
account tree, so none is left dangling on the target.
Three conditions are a DENY — the dry run reports NOT CLEAN and --live is
refused:
- a dangling source symlink (attached disk not mounted),
- a target with multiple
/mntmounts, or - a store that fits nowhere.
Site-level sites/*/files and sites/*/private symlinks resolve either way — they
point at the account-level static/files path, which on the target is a real directory
or a symlink onto the mount.
Relocated arch (migratefs)
Once migratefs has relocated
/data/disk/arch onto attached storage it is a symlink, and all three tools
transfer its contents: the shared sync routes arch through the storage-aware
store handler, which resolves the symlink (readlink -f, scoped to this store —
not a blanket rsync --copy-links) and materialises it on the target per the rule
above — mirrored onto the target's mount at <mount>/files/system/arch with
/data/disk/arch re-pointed, or de-referenced into a real /data/disk/arch on the
target root.
In LIVE mode an arch that cannot be placed aborts the shared sync with an error
rather than proceeding without backups.
Previously xoct/xcopy rsynced arch as a bare path and delivered a dangling
symlink, while xmass skipped it entirely — silently transferring zero SQL dumps and
cluster backups.
A real (non-relocated) arch keeps the existing behaviour; a dangling arch symlink
(attached disk unmounted at migration time) is a DENY in the dry run instead of a
silent failure.
The shared sync also refuses an empty target: an empty ssh host resolves to
localhost, so the remote mkdir/rm could have run against the source box and
deleted its live arch symlink.
The target mount is now resolved up-front, and the resolver exits non-zero on an empty
target, an unreachable target, or a multi-mount target (xmass already died on an
empty target).
Before migrating a host whose arch/static-files was relocated by migratefs,
the tools must be current on both hosts — and they now arrange that themselves:
xmass pre-mig (which runs on both hosts) and prep-target (for the target) drop the
per-tool control markers for the whole migration tool set and run the housekeeping
fetcher synchronously, logging each tool's version, so a migration prepared days
earlier is never run on stale tooling. The tool executing the command refreshes on its
next verb rather than mid-run.
Solr indices and the space gate
Solr home data (/opt/solr4, /var/solr7/data, /var/solr9/data) — often tens of
GB — is folded into the migration disk-space gate. Each tool runs a Solr space
check before each Solr transfer: du -sk of the source vs available space at the
target destination dir, measured by df on the nearest existing ancestor on the
target — so it follows the target's own /var/solrN symlink onto its mount, or
lands on root.
If it does not fit, the tool prints DENY: <label>: <N>K does not fit at target <dest> — free space and retry. On xoct/xcopy that DENY is a hard
stop: the run exits non-zero naming the account ("refusing to migrate/copy without
its search indices"), because migrating an account without its search indices while
reporting success is the worse outcome. On xmass the DENY is recorded and the sync
or cutover aborts on it before any destructive step. Unknown size or unknown target
availability does not block — the measurement fails open.
Solr placement is unchanged: a data-only rsync following the target's own Solr setup. Solr is deliberately not routed through the storage-aware store handler, which moves whole directories and would clobber the target's fresh Solr home.
Previously Solr was only DRY-wrapped, not space-checked — a dry run would not warn, and a live run would pour the index onto a full target.
How web traffic is frozen — http-off
To stop writes on the source while databases are exported or drained, xoct and
xmass write a per-account static/control/http-off.pid. The file's contents are a TTL
in seconds — 3600 when set by xoct export, 7200 when set by xmass cutover.
global.inc reads the marker and short-circuits every site in that account with a
PHP-level 503.
This replaces the old readonlymode mechanism, which is unavailable via the system
Drush 8 on Drupal 8+ and is bypassed by some commerce/API code paths.
http-off is the per-account freeze, and for xoct it is the whole story — the
box-wide global-extra.inc include must not be borrowed for one account's export
window, because it would freeze every other account on the source too. The
whole-server cutover goes further itself: xmass cutover appends a durable
read-only flag (config_readonly / site_readonly) to
/data/conf/global/global-extra.inc, keeping the previous file as .bak beside it,
and refuses to cut over if the freeze does not take — a session-scoped database
read lock cannot hold anything once its client disconnects, so the durable flag is
what stops writes for the promotion. After promotion the flag is lifted on the new
host, never on the source. Aborts handle the freeze themselves: a pre-promotion
abort thaws the source, and a failed promotion is judged by reading the target's
replica state back rather than by an exit code — a target still reporting itself a
replica thaws and aborts, while a promotion that committed parks the cutover
resumably and keeps the source frozen on purpose (the resume re-asserts the 503
gate and the freeze). Only a target whose state cannot be read back leaves the freeze
standing unresolved; the printed recovery includes the thaw line — restoring the
include from its .bak — and says when it is safe to use.
At each http-off toggle the nginx fastcgi speed cache is purged box-wide so already
cached 200 (or 503) responses don't mask the change; the microcache repopulates
within seconds on live sites. The pid is removed when the account is converted to a
proxy at xoct proxy / xmass cutover time, so the now-proxied sites return to 200.
xcopy never writes http-off — the source is never frozen.
The static/files transfer uses a symlink-safe two-pass helper
(_xoct_transfer_static_symlink_safe, mirrored in xcopy as
_xcopy_transfer_static_symlink_safe):
- pass 1 syncs everything under
static/with symlinks preserved (excludingstatic/files); - pass 2 routes
static/filesthrough the storage-aware store handler (see Storage-aware transfers and the DRY/--livegate above), so on the targetstatic/filesmay legitimately be a real directory on root or a symlink onto the target mount — either way neither the account-levelstatic/filesnor the per-sitesites/*/filessymlinks are left dereferenced into broken layouts.
Migration proxy — real-IP recovery and CSF trust
After cutover the source server is converted to a reverse proxy that relays traffic to
the new host (xoct proxy per account, driven by xmass cutover), so visitors keep
working until DNS is repointed.
Two gates precede the conversion. Peer reachability is arranged by the tools, not
the operator: the source learns the target's SSH host key and opens its own firewall
for the target during prep-target, and the target opens for the source during
pre-mig run on the target — the one pass that executes there and knows the source
identity. Every peer opening writes both csf.allow and csf.ignore before
reloading, because an allow alone leaves the peer exposed to an automatic temp-deny
mid-migration — which fails init after the target's data directory has already
been replaced. Both appends are idempotent. (This pre-migration peer opening is
distinct from the post-cutover proxy whitelist below.) And the target must prove
itself before the source is pointed at it: the cutover runs the nginx config test on
the target and requires a real answer on its port 80 — either check failing parks the
cutover resumably with the source still on 503, printing a recovery recipe led by the
resume instruction. The proof runs at the head of the cutover tail, so every resume
re-proves the target's web layer and starts its nginx itself — a proxy is never
pointed at a dead target.
For HTTPS relays, a domain whose certificate material is incomplete is skipped for HTTPS proxying and named in the conversion output together with the missing files — a vhost referencing a missing certificate file fails the box-wide configuration test, so one partial certificate directory would otherwise take the whole conversion down. Check that output for domains that will not relay on 443.
The critical wiring is on the target: xmass runs
_xmass_setup_migration_proxy_trust before relaying starts. It scps
migration_proxy_trust.sh + migration_proxy_realip.sh from the source's
/var/xdrago/ to the target's /var/xdrago/, chmod 0755s them, and runs
migration_proxy_trust.sh trust <source-ips> [--permanent] on the target. That tool:
- Writes
set_real_ip_from <proxy-ip>;for the nginx realip layer (viamigration_proxy_realip.sh), so the new host recovers the real client IP instead of seeing every request come from the proxy. - Hard-whitelists the proxy's egress IP in CSF —
csf.allowon ports 80 and 443 pluscsf.ignore(tagged# migration proxy).
Without this, every relayed request would appear to come from the proxy IP, so every
per-client control on the new host (scan_nginx, geo bans, ip_access, the AI guards,
CSF/lfd) would key on the proxy and a single ban would blackhole all migrated sites.
The trusted source IPs are derived from hostname -I on the source, filtered to routable
IPv4 (loopback 127.* dropped, IPv6 dropped, private IPs kept since the
source→target hop may run over a private network).
Both validators reject the all-zeros host and any /0 prefix: trusting 0.0.0.0/0 as a
realip source would honour the spoofable CF-Connecting-IP header from every peer,
collapsing the realip trust boundary (nginx ignores host bits, so 1.2.3.4/0 is
0.0.0.0/0).
The path is IPv4-only by design — CSF's whitelist is IPv4 and proxy_pass targets an
IPv4 target IP, so an IPv6 proxy-peer line would be stripped and flap, and is dropped
deliberately.
The proxy also refreshes its own Cloudflare realip (/var/xdrago/cloudflare_realip.sh)
so that for CF-fronted sites it forwards the real visitor — not the CF edge IP — in the
CF-Connecting-IP header it hands the new host.
xoct proxy wires the same target-side trust itself (records, scp of the tools,
trust with --permanent for permanent/ha-switch accounts), so a standalone
xoct move recovers real client IPs from the very first relayed request without any
separate wiring.
Teardown is record-driven: migration_proxy_trust.sh reconcile (run by both tools'
post-mig and after every policy change) recomputes the kept peer set from the
per-account policy records (/data/disk/oN/log/migproxy.cnf). The records carry a
role, and only the outbound (source-role) record decides this box's own proxy
mode — an inbound record left behind by the migration that populated this box is data
about the peer, not a mode declaration — so a box that was itself a migration target
honours the mode you give it when it later becomes a source. Peers of accounts
resolved temporary are dropped once traffic flows directly, permanent/ha-switch
peers stay trusted, accounts stamped proxied with no valid record are never guessed at
(their trust and the permanent marker are left as found and reported), and trust
entries that predate the record system are preserved until an explicit
teardown --force. On a host with no records at all, reconcile behaves exactly as
the old unconditional teardown (the permanent marker is honoured).
The certificate mirror keeps a long-lived proxy's HTTPS alive. A proxied account
stops renewing by design (the target is the sole issuer), so the proxy would present
its cutover-day certificate until it expired 69–90 days later.
migration_proxy_certs.sh runs daily from cron (a quiet no-op on boxes with no
proxied account): for every HTTPS proxy vhost it locates the same domain's live
store on the target — whatever the account is called there — and when the target's
certificate is strictly newer, pulls the whole set staged and verified (expiry plus
a key/certificate match), preserving ownership, modes and the ssl.d/openssl.*
symlink topology, with one configtest-gated nginx reload and automatic rollback if
the test fails. The target IP comes from the vhost's own proxy_pass, cross-checked
against the policy record; a disagreement refuses and names the
--repair --retarget fix. When the earliest proxy certificate falls inside
_MIGRATION_PROXY_CERT_WARN_DAYS (default 21) and could not be refreshed, the admin
is mailed — the only expiry warning a proxied box has. An unreachable target can
never break TLS on the proxy: its certificates depend only on files already on its
own disk.
Which tool when
One site/account, same host → Aegir Migrate/Clone task (see 04.site-cloning)
One account across hosts (move) → xoct
One account across hosts (copy) → xcopy (source stays live)
Whole BOA host → xmass
xoct, xcopy, and xmass are the mature BOA cross-host tools; prefer them over any
legacy manual import. The historical single-site manual remote_import /
hosting_remote_import flow is no longer a functional BOA module and is treated as
retired — see Discontinued features.
Related
- xoct & xcopy — single-account move and copy — the full
single-account move runbook, plus the non-destructive
xcopycopy. - xmass — whole-server move — the whole-host xtrabackup + GTID replication runbook and cutover sequence.
- Site cloning & in-host migration — the within-host Clone and Migrate tasks and the FastTrack/MyQuick accelerators.
- Aliases & redirects — the alias/redirect model, relevant when a moved or renamed account changes its canonical hostname.
- Database (MySQL/Percona) —
mydumper/myloaderinternals and the Percona-version constraints that gatexmass. - Security & isolation — CSF,
ip_access, and the realip layer the migration-proxy trust feeds into. - Abuse Guard —
scan_nginxand the geo/AI bans that key on the recovered client IP after a migration proxy is trusted. - See the Reference appendix for the consolidated variable, command, and control-file tables.