Skip to content

Powered by Grav

xoct & xcopy — single-account move and copy

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.

SH
# 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.ftpo2.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 createcreate 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:

  1. re-import of the full hostmaster DB dump,
  2. a couple of frontend variable-set calls via drush (site_frontpage, plus hosting_client_send_welcome in the fix path — Ægir API, not direct SQL),
  3. 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 with drushrc.php + settings.php on 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
  4. 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 sed of hostmaster drush aliases plus hardcoded-nid drush sqlq REPLACE/UPDATE/DELETE against hosting_context/hosting_package/node/hosting_site/hosting_platform/users_roles, guarded by a one-shot post-merge-fix.pid), and
  • the ghost/empty-platform delete loop, whose missing-sites/all test 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 xoct steps 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 example exported.pid, transferred.pid, imported.pid or proxied.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_site tables,
  • the greedy sed "Pre-Fix" rewrites of hostmaster.alias.drushrc.php,
  • the nightly one-shot UPDATE hosting_context SET name=hostmaster SQL, and
  • the nightly _fix_site_readonlymode reset.

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 xoct works correctly when driven by xmass.

If muscle-memory still types xboa, an operator-created compatibility symlink is the documented workaround (BOA does not ship one):

SH
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.

SH
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.

© 2026 BOA Documentation. All rights reserved.