Account migration between nodes (TIGER-310)
Move a hosting account from one fleet node to another — files, databases, DNS, and the control-plane binding — idempotently and safely. The source stays live and authoritative until a green verify authorizes the irreversible teardown, so the whole flow can run unattended (the primitive a nightly fleet optimizer uses to consolidate under-used nodes and rebalance hot ones).
- Orchestrator:
Hosting_Lib_Migrate(control plane). - Node verbs:
Server_Service_Account::migrate*(TigerServer), reached as rolebilling. - CLI:
bin/tigerhosting-migrate.php. - State: one resumable
hosting_migrationrow per attempt.
Why dedicated verbs (not account create/delete)
A custom-domain account's authoritative DNS zone lives on the shared PowerDNS cluster. The generic
account/create/delete create and DELETE that zone — so using them to migrate would let a mid-flight
failure (or the source teardown) destroy the zone and break the source too. Migration instead uses verbs
that only ever repoint the zone's A record:
| Verb | Role in the move |
|---|---|
migrateCreate |
Target skeleton (user/web/db) without the DNS zone or custom cert (both deferred). Validates inputs directly — not the create form, whose availability gates reject an existing domain. |
migrateTest |
Pre-cutover: the target serves the real domain on its own vhost (loopback), before DNS moves. |
migrateDns |
Cutover: repoint apex + www A records to the target (Server_Provision_Dns::setA — a PATCH in the existing zone, never create/delete). Re-issues the deferred cert. |
migrateVerify |
Comprehensive, read-only: authoritative A (read from the pdns primary) points at the target and the site is served at the target's public IP. Returns the node's mailbox count. |
migratePurge |
Source teardown that keeps the shared zone (deprovisions every step except dns). |
mailboxCount |
Per-node mailbox count, for mail-parity gating. |
The data move itself reuses the backup transfer (backup read/write/ingest/restore, sha256-verified);
the restore clears the node's provisioning placeholder where it lays down a real app.
State machine
backing_up → creating_target → transferring → restoring → direct_url → verifying → cutover → confirming → verified → (tearing_down → done)
- verifying — target serves (loopback) + mail parity. Fail ⇒ rollback (purge target; DNS never moved).
- cutover — repoint the A record to the target (binding NOT flipped yet).
- confirming — the commit, gated on
migrateVerifygreen + mail parity: flip the CP binding (node_id/account_ref/direct_url) and recompute both nodes'used. Fail ⇒ rollback (DNS back to source, purge target). - verified — moved + confirmed; source still intact, awaiting teardown.
- teardown →
migratePurgethe source (keeps the zone) → done.
The verify gate (automated)
A green verify is the only thing that authorizes teardown; teardown() re-runs it live and refuses if not
green. Green = all of:
- DNS — authoritative A (apex + www), read from the PowerDNS primary, equals the target's public IP.
- Site — the real domain is served at the target's public IP (2xx/3xx, not the bare placeholder).
- Email — opt-in. No mailboxes (domain-less / email not set up) ⇒ N/A (pass). If the source has mailboxes, the target must have at least as many. v1 does not migrate mail, so a mail-bearing account fails verify and is not torn down.
CLI
CLI="sudo -u apache env APPLICATION_ENV=<env> php .../modules/hosting/bin/tigerhosting-migrate.php"
$CLI plan --account=<account_id> [--to=<node>] # DRY RUN — always first; mutates nothing
$CLI start --account=<account_id> [--to=<node>] [--teardown] # move → verified (--teardown = auto on green)
$CLI advance --migration=<id> # resume
$CLI verify --migration=<id> # read-only comprehensive verify
$CLI status --migration=<id>
$CLI teardown --migration=<id> # removes the SOURCE (self-gates on green verify)
$CLI list
- Operator:
plan→start(stops atverified) →verify/spot-check →teardown. - Unattended:
start --to=<node> --teardown(teardown fires automatically once verify is green; a non-green verify leaves the source live).
Preconditions
The target node must carry the fleet DNS config and its own temp base in its config tier
(tiger.server.dns.{api_url,server_id,nameservers,public_ip,api_key_enc}, tiger.server.temp_url_base).
Without them the migration fails at creating_target or direct_url.
v1 limitations
- Mail is not migrated (box-level dovecot/postfix state); mail-bearing accounts are blocked by verify.
- Limits are not re-synced — the target uses node defaults; re-apply the account's package post-move.
- External-DNS domains —
migrateDnsreportsmanaged=false; the A record must be pointed at the target manually, and verify stays red until it is.