xoct & xcopy — single-account move and copy
xoct moves one Octopus account between BOA hosts and decommissions the source to a
migration proxy; xcopy shares its dispatcher shape but is a non-destructive copy —
the source stays live. This page is the full runbook for both. For where these sit
against the whole-server xmass tool, and for the machinery all three share — the
storage-aware DRY/--live gate, the http-off write freeze, and the migration proxy —
see the Cross-host migration overview.
xoct — single-account move
xoct is a multi-step dispatcher, not a two-verb prep/import model. Almost every
step runs on the source with the target IP as the argument — including create,
which SSHes to the target to provision the new Octopus account there (boa in-octopus).
One prerequisite before anything: both servers should run the same BOA
release. Unlike xmass (whose release gate is an enforced refusal), xoct
carries this as a soft expectation — minor version differences are usually
fine, but a major platform gap between the ends is asking for trouble; bring
the older box current first.
Only import and post-mig are run on the target; pre-mig is run on both. The
optional fourth argument renames the account on the target.
pre-mig (run on the source first, then the target) freezes the task queue for the move:
it parks the BOA background runners — runner.sh, owl.sh, usage.sh, graceful.sh,
manage_ltd_users.sh — as .off and kills any running copy, then exchanges root SSH keys
so root can SSH freely between the two hosts. post-mig restores those runners afterwards.
# Phase 0 — pre-mig, run on BOTH source then target
xoct pre-mig <source-fqdn>
# On SOURCE — provision target shared infra + first-pass transfer.
# transfer/pretransfer default to a read-only DRY run: review the
# [DRY-PLAN] output, then re-run the same command with --live.
xoct transfer shared <target-ip> # DRY plan
xoct transfer shared <target-ip> --live
xoct create o1 <target-ip> [o2] [--fix-php]
xoct pretransfer o1 <target-ip> [o2] # DRY plan
xoct pretransfer o1 <target-ip> [o2] --live
# On SOURCE — export (writes http-off 503 + dumps) then transfer
xoct export o1 <target-ip>
xoct transfer o1 <target-ip> [o2] # DRY plan
xoct transfer o1 <target-ip> [o2] --live
# On TARGET — import, then post-mig (restores runner scripts on target)
xoct import o1 <target-ip> [o2]
xoct post-mig <source-fqdn>
# On SOURCE — convert old host to proxy, then post-mig
xoct proxy o1 <target-ip> [o2] [--proxy-mode=temporary|permanent|ha-switch] [--deadline=YYYY-MM-DD|+Nd]
xoct post-mig <source-fqdn>
# Then update DNS A records to <target-ip>; the proxy hop drops away
# once DNS propagates.
The dispatch verbs are: export, create, import, pretransfer, transfer,
proxy, proxy-mode, proxy-retire, reset-state, pre-mig, post-mig, and
ssl-gen (re-issue proxy SSL, gated on /root/.ssl.proxy.cnf; it creates the nginx
pre.d directory if the master is missing it, refuses a domain by name when it cannot,
and reports a proxy config as created only when the file is really on disk).
Only one state-mutating xoct verb runs on a box at a time: every verb above except
proxy-mode takes a box-wide owner-PID lock, and a second run refuses loudly and
non-zero, naming the live owner's pid. proxy-mode stays unlocked on purpose — it
writes a policy pin, not data, so policy can be inspected or pinned during a run. The
guard is liveness-based: a run killed outright (or a reboot) wedges nothing and there is
no stale lock to clear by hand.
reset-state o1 clears the per-account migration state — the src/*.sql dumps
including prev_hostmaster.sql, the exported/transferred/imported/proxied
pid stamps, the export- and import-failure latches (log/export_failed.pid,
log/import_failed.pid), and the recorded import panel database (log/panel_db.txt).
That is what a chained migration needs: a box that was once a target
keeps a stale dump the export step will not overwrite, an imported.pid that blocks
the account from ever being an import target again, and a panel-database pointer that
belongs to the previous move. It never touches serving state.
transfer shared provisions the account-independent shared infrastructure on the
target — the shared codebases (/data/all, /data/disk/all), archives and SQL dumps
(/data/disk/arch), the Solr cores (/opt/solr4, /var/solr7/data, /var/solr9/data),
/var/www/static, /etc/bind, and the usage logs under /var/log/boa/usage. A Solr
index tree that does not fit on the target is a hard stop, not a skip: each Solr
home is space-checked before its rsync and a failure refuses the run non-zero
("refusing to migrate without its search indices") — free target space and re-run,
rather than discovering an index-less account later. The
per-account verbs take the Octopus username (o1, o2, …) as their second argument and
the target IP as their third.
When the optional fourth argument differs from the source username, xoct enters
rename mode on import. renameaegirhost rewrites every reference from the source to
the target account name via its --old-account/--new-account axis: the control-panel
identity becomes o2.<target-fqdn> (adopting the fresh install's panel site dir and its
valid credentials), and /data/disk/o1 paths in platform, git, backup and package records
are rewritten to /data/disk/o2 — otherwise the first DB-driven regeneration would
re-stamp the stale source paths over the transfer-time fixes. On the account axis,
customer-site URIs are never touched: a site on the account subdomain such as
shop.o1.<fqdn> keeps its name. (The host axis is different: a site whose URI embeds
the box hostname follows a hostname rename on every surface — see the rename step on
the xmass page.)
The rewrite also covers the per-site FPM $user_socket account token, so the renamed site
is served by its own account's pool rather than the target's install-time account that
still carries the old name, and the shell/FTP login name (o1.ftp → o2.ftp) on the
credential lines of the account's stored welcome mail, so the on-disk credentials copy —
the file you are pointed at when a send fails — is consistent with the new account.
A mistyped or unknown --flag on create, transfer, pretransfer, import or
proxy is a hard error rather than falling through to the positional arguments —
precisely because on those verbs the next free positional slot is this rename value, so
a silently consumed flag would have become a rename instruction.
The target account o2 must not already exist before create — create provisions
it fresh. After a rename, verify the site returns a real 200 direct to the target
(not a proxy or catch-all): a site-wide 403 immediately after a rename historically meant
a socket token still pointed at the wrong account's pool.
What create seeds
create does more than provision: it carries the account's identity, which a fresh
install would otherwise replace with the target's defaults.
Before installing it verifies the target has every PHP version this account pins —
from static/control/fpm.info, cli.info and the per-site column of multi-fpm.info.
A missing version is fatal, because BOA's own fallback ladder would silently downgrade
the pin and serve those sites on a different interpreter; --fix-php instead appends the
missing versions to the target's _PHP_MULTI_INSTALL and drives one
barracuda up-<tree> system pass there to build them. It installs from the target's
BOA tree rather than this box's, so a cross-tree migration is not sent through the
target's licence gate, and uses boa in-oct when the source account is ProxySQL-wired.
After the install settles it merges the portable values of
/root/.<o1>.octopus.cnf into the target's copy — the FPM tuning knobs, the _CLIENT_*
plan identity, the ghost-cleanup flags, _RESERVED_RAM — while leaving the host-specific
lines for the target to re-derive. It force-copies the PHP pin files, and carries the
client's shell credentials: the <o1>.ftp password hash paired with log/pass.txt, the
sub-account password store and each sub-user's hash and SSH keys. Sub-users that do not
exist on the target yet converge when manage_ltd_users.sh creates them there from
clients/, adopting the carried password and the staged keys. The install's own welcome
email is suppressed for the run, since the credentials it would announce are replaced
moments later. Finally it verifies the pins actually took.
The client keeps their credentials: Ægir panel login, SSH/SFTP password and SQL password all carry over, as do the main account's and every sub-account's SSH keys.
Re-running create against an already-installed account skips the install and re-seeds
only, so it is safe to use to converge a target that was prepared by hand.
The transfer/pretransfer verbs are gated by the DRY/--live model — see
Storage-aware transfers and the DRY/--live gate
on the overview.
create first waits for a quiet target — no barracuda or octopus pass, no install
lock, and no armed run-upgrade.pid on any account (an armed pid is a pass the runner
starts on its next tick; a lock-free box with one armed is not quiet), held on two
consecutive polls because chained passes leave lock-free gaps of a few seconds — before
it installs (the launcher refuses on a lock, and the account must never be reported
created when it was not), proves the install afterwards, and arms the enforced
post-install upgrade only when the seeded control files actually changed. Driven by
xmass prep-target, the full target-silence gate runs before every create on top of
this wait.
The export dump
export dumps the entire hostmaster database (${_HDB}, derived from the
hostmaster site's drushrc.php) into /data/disk/<oN>/src/prev_hostmaster.sql,
using the same mysqldump option set renameaegirhost uses (--single-transaction --quick --no-autocommit --skip-add-locks --no-tablespaces --hex-blob).
The previous hand-curated table allowlist — tied to the removed direct-SQL import
surgery below — silently dropped any hostmaster table not on the list, so newly added
module/field schema tables simply vanished from the migrated instance; the full dump
gives the import target a faithful copy for renameaegirhost's rewrite and 5-pass
queue.
The same applies to xcopy export; xmass is unaffected (full-server replication, no
hostmaster mysqldump).
exported.pid means "this account's export is restorable", not "a dump was
attempted". The stamp lands only after the per-site dump loop with zero counted
failures: every per-site mydumper run must exit clean AND leave its final metadata
marker, and the hostmaster dump must exit clean and be non-empty. Any failure instead
writes the refusal latch log/export_failed.pid (parsed, never sourced), prints an
INCOMPLETE verdict and exits non-zero. While that latch exists, transfer and import
refuse the account — and the latch travels with the account transfer, so the target
refuses a forced-through partial export too. --force bypasses only that one refusal,
loudly; after a fully forced-through partial migration xoct proxy needs --force as
well, on the same explicit axis. The recovery is simply re-running the same export
after fixing the cause: site dumps are redone in place, the hostmaster dump is kept, and
the client start notice is not re-mailed. A database carrying any non-transactional
table is dumped with mydumper's transactional-only mode turned off, selected
automatically per database, so a stray MyISAM table neither fails the export nor
withholds the stamp (see Dumps with mydumper for
the underlying behaviour).
Source disk space: export writes the per-site mydumper output and the hostmaster
dump under /data/disk/<oct>/src/, so keep at least 1× the account's total database
size free there before you start.
Chained migrations — a former target moving onward: a completed xoct import
starts a fresh migration lifecycle on the new box — the source-role latches that
travelled inside the account's log/ tree (exported.pid, transferred.pid,
export_failed.pid) are cleared in the same breath as stamping imported.pid, so a
box that has just received an account can be a source for the next leg without
hand-clearing them. What is deliberately left: a box that was itself a migration
target earlier still holds src/prev_hostmaster.sql and log/imported.pid from that
transfer. xoct export will not write a fresh dump while prev_hostmaster.sql is
present (it protects a dump a pending import may still need), so a chained move would ship
the stale dump; imported.pid also blocks the account from being an import target again.
Before migrating a former target onward, run xoct reset-state o1 — the full clear
list is above, and it is what the tool itself prints. (log/sourcefqdn.txt
is rewritten with every dump and needs no cleanup.)
On-target import
Before importing, on the target: pause the BOA task runner so no background job fires
mid-import — import quiesces the box itself: it stops cron (init script plus
pkill, the Devuan-safe form), relaxes /data/all/cpuinfo, and waits for running
BOA jobs with a bounded poll. Nothing to do by hand there. But mind the other
half of that quiesce: cron stays stopped until xoct post-mig restarts it. A
box left without cron silently stops receiving fleet updates altogether, so if
the run gets interrupted between import and post-mig, check pgrep -x cron
before you walk away — same callout as on the xmass whole-server path. Run the import under
websh: point /bin/sh and /usr/bin/sh at it first — ln -sfn $(which websh) /bin/sh; ln -sfn $(which websh) /usr/bin/sh — and confirm with ls -la /bin/sh.
On the target, import reduces to:
- re-import of the full hostmaster DB dump,
- a couple of frontend
variable-setcalls via drush (site_frontpage, plushosting_client_send_welcomein the fix path — Ægir API, not direct SQL), - panel platform reconciliation — the imported database names the SOURCE's
hostmaster platform path, and the target's live panel usually sits on a
different
aegir/distro/NNN(panel platforms are rebuilt on PHP-pin changes, and retired trees keep their code, so code presence at the imported number proves nothing). The import locates the target's unique live panel (site dir withdrushrc.php+settings.phpon a code-bearing platform) and repoints the hostmaster platform row before the rename, so the rename queue's DB-derived regeneration lands on a platform that really serves; an unresolvable panel aborts the import with recovery steps instead of completing with a dead control panel, then renameaegirhost --aegir-root.
Per-site database loads are truthful. A transferred dump directory lacking
mydumper's final metadata marker is SKIPPED rather than loaded — a hollow dump
silently restoring an incomplete database is the failure being guarded against. A
failed myloader run is counted, and so is a site whose db credentials cannot be parsed;
a site with a counted failure does not get its verify scheduled, while clean sites
import and verify normally. Any counted failure writes log/import_failed.pid, prints
an INCOMPLETE verdict naming the databases and exits non-zero; the printed recovery is
to remove imported.pid and re-run the full import from the same transferred dumps.
One exception keeps mixed accounts importable: a site with no transferred dump whose
database on this box is already populated is a target-native site (the account's
own pre-existing dedicated site is the common shape) and is left as-is, not counted —
only a site with neither a dump nor a populated local database counts as a failure.
import also rebuilds the account's pinned PHP pools on the new host. The account
arrives carrying the source's per-release FPM markers; both boxes run the same release,
so without clearing them the target reads the pool set as already built, never creates
pools for versions that exist only here, and never regenerates the per-site socket
includes — every pinned site is then served by the account's DEFAULT pool indefinitely
(a 7.4-pinned site silently running on 8.4). The import clears the markers, runs the
normal sweep in two passes, then verifies every pin has a live pool socket and alerts
by name for any that does not. Treat such an alert as a stop: a site answering 200 with
correct content can still be on the wrong interpreter.
renameaegirhost handles the in-place hostname rename of the moved account — a full
5-pass Ægir task queue that rewrites Drush aliases, nginx vhost files under
config/server_master/, and the Ægir DB content as its atomic rename step.
This supersedes the legacy "migrated/merged instance" fixup layers, which were removed
from import:
- the direct-SQL surgery (in-place
sedof hostmaster drush aliases plus hardcoded-niddrush sqlqREPLACE/UPDATE/DELETE againsthosting_context/hosting_package/node/hosting_site/hosting_platform/users_roles, guarded by a one-shotpost-merge-fix.pid), and - the ghost/empty-platform delete loop, whose missing-
sites/alltest was true for every valid Composer D8+ platform and so deleted valid platforms from Ægir during import.
xcopy import follows the same model; the intended remaining xoct/xcopy divergence
is that xcopy does not enable the migration proxy.
Operational notes
- Re-running after a failure — most
xoctsteps are pid-gated and safe to repeat after a failed run. To force a step to run again, remove its marker under/data/disk/o1/log/(for exampleexported.pid,transferred.pid,imported.pidorproxied.pid). - Drupal 6 IP blocking — Drupal 6 sites that block by IP can lock out the migration.
Before migrating such a site, whitelist the source IP at
/admin/user/rules(a Host rule of type Allow), or flush the site's{access}table via Chive afterwards.
No automatic fixups on upgrade or nightly runs
Migrated/merged instances get no automatic surgery elsewhere either. The legacy fixups that used to rewrite imported instances on upgrade and nightly runs were all removed:
- the hardcoded-node-ID SQL rewrites against hostmaster's
hosting_context/node/hosting_sitetables, - the greedy
sed"Pre-Fix" rewrites ofhostmaster.alias.drushrc.php, - the nightly one-shot
UPDATE hosting_context SET name=hostmasterSQL, and - the nightly
_fix_site_readonlymodereset.
They predate renameaegirhost and, on long-lived instances whose node IDs no longer
matched the hardcoded assumptions, caused hostmaster drift rather than fixing it.
After a cross-host migration, hostname/topology fixups are exclusively the job of
renameaegirhost and the normal hosting machinery — do not expect BOA to auto-rewrite
hostmaster contexts or aliases on the next upgrade or nightly run.
The retired internal markers (post-merge-fix.pid, hmpathfix.pid,
hosting_context.pid, readonlymode_fix.info) need no operator action.
DNS proxy conversion
DNS-proxy setup is not automatic: it is the explicit xoct proxy step run on the
source. It resolves the account's proxy mode (explicit flag, else the account's own
source-role record set with xoct proxy-mode, else the box default, else
temporary with a loud warning — an inbound target-role record left by the migration
that brought the account onto this box is never resolved as this box's proxy mode, so a
chained move follows the operator's flag or the box default rather than the previous
migration's mode and deadline), writes the policy record on both ends, wires the
target-side realip/CSF
trust, converts the account's nginx vhosts to proxy templates forwarding to the target
IP, removes the migration http-off, and emails the account owner a mode-selected
notice. A failed conversion mails nothing, stamps nothing and exits non-zero. An account
that holds nothing but its control panel has nothing to convert and is converted by
definition: it is stamped, its gate is released and the owner is told that the panel
moved (a whole-server move once left such an account 503-gated on the source for a
"0 sites converted"). The notices are one paragraph per line, so mail clients wrap them
for the screen.
The account's control panel is never proxied. It is identified by what it is — the
site_path of the hostmaster alias — not by which alias files exist, keeps its local
vhost, and is put into Drupal's own maintenance mode once the conversion is in, with
its page cache flushed in the same step (a variable change alone leaves the cached
anonymous front page in place, so the hold would be invisible until the next cache
clear): it stays online on the old box as a monitoring canary, an admin can still log
in, and nobody else can queue tasks against a database that now lives on the target. The
target does not serve a source-named panel, so a proxied panel would only ever answer
the new box's catch-all page. A panel an earlier conversion had proxied is restored
from its saved dot-file copy on the next proxy run. The legacy
<panel-fqdn>.alias.drushrc.php symlink to the hostmaster alias, which used to make
the panel look like a site to this tool, is purged by the
ltd-users pass on every box; an account that
still depends on it is repaired before it migrates.
In the same breath as stamping proxied.pid, the conversion parks the account's
Ægir dispatcher: a single dispatcher tick would run the account's queue, whose
pending Verify regenerates the site vhosts straight from the database and silently
un-converts the proxy — the old box then resumes serving its own stale copy.
xoct post-mig re-parks any account carrying proxied.pid instead of handing the
dispatcher back; the same rule governs an xtrim restore, see
PX0 proxies & shrinking.
An already-proxied account is reopened with --repair (rebuild the vhosts; mails only
if the promised arrangement changed) and repointed with --repair --retarget (the
abandoned target's trust is released). xoct proxy-mode --all prints the per-account
policy table — mode, deadline, scope, peer, and what the client was last told — and
xoct proxy-retire marks an account withdrawn and sends the retirement notice. After
DNS is repointed the proxy hop is no longer used.
The closing nginx reload is configtest-gated. If nginx -t fails, a first conversion
reverts both halves of every vhost it rewrote — the plain HTTP vhost from its
dot-backup (.<domain>) and the site's real HTTPS server block, which the conversion
saves as .https.<domain> before overwriting it (first copy only, so a repair pass
cannot destroy the original) — so "reverted" means TLS is back too, not just that nginx
parses. A --repair
refuses instead and says why restoring the saved copy would be wrong — on a repair that
copy is the pre-migration original, so putting it back would serve the account
against a database that now lives on the target. Either way nginx is never reloaded into
a broken config. An HTTPS proxy vhost is written only when all three files its
template references exist — the account's ssl.d/<domain>/openssl.key and
openssl_chain.crt, plus the Let's Encrypt tools/le/certs/<domain>/chain.pem —
because nginx refuses to load a server block whose certificate file is missing and the
proxy render is box-wide, so one partial certificate directory would fail the
configtest for the whole conversion. A site missing any of the three is counted and
named with the missing files rather than skipped silently; re-run with --repair
once the material exists.
Both proxy templates forward /.well-known/acme-challenge to the target explicitly. The
target is the sole certificate issuer for names still resolving to the proxy, and the
certificate mirror pulls each renewed pair back — so that forwarding is the property the
whole Let's Encrypt policy rests on, and it is stated in the templates rather than left
to fall through the catch-all location.
Downtime and cross-version safety
Downtime per account: minutes to a few hours depending on site sizes. Sequential — one
account at a time. xoct uses mydumper/myloader for the DB phase and is
cross-version safe — it works between hosts running different Percona versions.
xboa is gone
xboa was the predecessor name. It is fully removed — there is no xboa binary and
no xboa fetch entry (BOA fetches xoct, xcopy, and xmass; nothing under the
old name).
xoct is its functional successor with two improvements:
- the Ægir DB hostname replacement and post-import task queue are delegated to
renameaegirhost(more thorough, 5-pass), and - the hardcoded internal-account email exclusion was removed so
xoctworks correctly when driven byxmass.
If muscle-memory still types xboa, an operator-created compatibility symlink is the
documented workaround (BOA does not ship one):
ln -sfn /opt/local/bin/xoct /opt/local/bin/xboa
The symlink target must be the real binary at /opt/local/bin/xoct (xoct is fetched
to /opt/local/bin only — it is not mirrored into /usr/local/bin); both directories
are on PATH, so a bare xboa then resolves.
xcopy — duplicate an account, source stays live
xcopy shares xoct's dispatcher shape but is a non-destructive copy: it does
not write http-off on the source, and it does not convert the source to a
proxy. Use it to stand up a duplicate of an account on a second host (staging clone of a
whole instance, pre-cutover rehearsal) while the original keeps serving.
xcopy pre-mig <source-fqdn> # on SOURCE + TARGET
xcopy transfer shared <target-ip> # on SOURCE — DRY plan
xcopy transfer shared <target-ip> --live # on SOURCE
xcopy create o1 <target-ip> [o2] # on SOURCE
xcopy pretransfer o1 <target-ip> [o2] # on SOURCE — DRY plan
xcopy pretransfer o1 <target-ip> [o2] --live # on SOURCE
xcopy export o1 <target-ip> # on SOURCE (dumps, no http-off)
xcopy transfer o1 <target-ip> [o2] # on SOURCE — DRY plan
xcopy transfer o1 <target-ip> [o2] --live # on SOURCE
xcopy import o1 <target-ip> [o2] # on TARGET
xcopy noproxy o1 <target-ip> [o2] # on SOURCE (skip proxy conversion)
xcopy post-mig <source-fqdn> # on SOURCE + TARGET
xcopy ssl-gen # on TARGET (re-issue certs)
Dispatch verbs: pre-mig, export, create, import, pretransfer, transfer,
noproxy, post-mig, ssl-gen.
The noproxy verb is the deliberate counterpart to xoct proxy — it finalises the copy
without proxying the source.
ssl-gen re-issues Encryption on the target for the (now duplicate) account, which is
required because the copied account, like any name-changed account, lands with
Encryption disabled.
Because the source is never frozen, an xcopy produces a point-in-time duplicate that
diverges from the still-live source the moment it completes — it is a copy, not a
synchronised replica.
Related
- Cross-host migration overview — which tool when, and the
shared storage gate,
http-offfreeze and migration-proxy machinery these tools stand on. - xmass — whole-server move — the whole-host tool that drives
xoct proxyper account during cutover. - Site cloning & in-host migration — the within-host Clone and Migrate tasks for a single site.
- Aliases & redirects — the alias/redirect model, relevant when a moved or renamed account changes its canonical hostname.
- Database (MySQL/Percona) —
mydumper/myloaderinternals behind the DB phase. - See the Reference appendix for the consolidated variable, command, and control-file tables.