Migrator — context-aware destination + global sources
Synced read-only from
/home/synnet/mirrors/synvirt-product/docs/superpowers/specs/2026-06-22-migrator-context-aware-destination-design.md. Edit at the source, not here.
Migrator — context-aware destination + global sources
Section titled “Migrator — context-aware destination + global sources”Date: 2026-06-22 Status: approved-as-goal (autonomous execution authorized by Tony) Scope: make the migrator behave according to where it is launched — standalone (migrate to the local host, unchanged) vs cluster/cloud (migrate to any destination host) — and make registered sources global across the federation when launched from cloud/federation.
Goal (operator words)
Section titled “Goal (operator words)”“haz que migrator sea según de dónde sea lanzado standalone o cluster wide, cualquier host destino … cuando sea lanzado desde cloud/federación, que los sources sean globales, que con que estén registrados una sola vez estén disponibles para todos.”
Key finding — the cross-host primitive already exists
Section titled “Key finding — the cross-host primitive already exists”The daemon already exposes a transparent cross-host reverse proxy:
ANY /api/v1/hosts/{fingerprint}/proxy/*rest → https://{peer_ip}:443/{rest} (Basic Auth + body passthrough)crates/daemon/src/api/hosts.rs::host_proxy. The frontend already wraps
paths onto it via hostScopedPath(path, fp) in src/api/remoteRouting.ts,
and useStoragePools(fp) already lists a remote host’s pools through it.
Validated live (2026-06-22): GET .12/api/v1/hosts/{fp_.11}/proxy/api/v1/migrator/capabilities
returned .11’s {preparer_available:true} (vs .12 local false),
proving the migrator API proxies cross-host with auth.
So “any destination host” is almost entirely a frontend routing
problem: route the migrator’s API conversation to the chosen host. The
migration engine (orchestrator, ProfileDomainStarter, ZFS sink) is
untouched — the destination host’s own migratord runs the job locally,
so disks land in its pool and the VM is defined on its libvirt.
Design
Section titled “Design”Active host (destination)
Section titled “Active host (destination)”stores/migrator.ts gains activeHostFingerprint ('' = local). Its
useMigrator(() => activeHostFingerprint) routes every migrator call
through hostScopedPath. setActiveHost(fp) switches host and re-pulls
sources/jobs/capabilities for that host.
MigratorView shows a destination-host selector only when
useClusterStore().mode !== 'standalone'. Options = this host ('',
the entry/founder) + managedHosts[] (federated peers). Default = local.
The New Migration pool picker follows the selected host
(useStoragePools(() => activeHostFingerprint)), so the operator can only
pick a pool that exists on the destination. The plan is submitted through
the active-host api → the job runs on that host.
Global sources (cloud/federation)
Section titled “Global sources (cloud/federation)”In cluster/cloud mode, Add Source registers + verifies on the active
host (existing probe → create → test → rollback flow), then best-effort
fans the registration out to every other member host ('' + each
managedHosts fingerprint). Result: a source registered once is present
on every host, so it is usable as the source for a migration to any
destination. Per-host replication result is surfaced via toast; a partial
failure leaves the source working where it landed (reversible by delete).
Standalone — provably unchanged
Section titled “Standalone — provably unchanged”hostScopedPath(path, '') returns path verbatim. The selector does not
render in standalone, activeHostFingerprint stays '', the drawer’s
host prop is undefined, the SSE stream stays local, and Add Source uses
the single-host flow. Every standalone code path is byte-identical.
src/composables/useMigrator.ts— optionalhostFingerprint?: () => string|undefined; wrap paths viahostScopedPath.src/stores/migrator.ts—activeHostFingerprint+setActiveHost+createSourceOnHost; bindapito it; SSE pollution guard.src/views/MigratorView.vue— destination-host selector (cluster mode), fan-outcommitSource, pass host to drawer, gate local SSE.src/components/migrator/NewMigrationDrawer.vue—hostFingerprintprop →useStoragePools(() => props.hostFingerprint).
KNOWN / deferred
Section titled “KNOWN / deferred”- Durable global sources belong in the cluster Raft state machine
(
ClusterCommand::UpsertConfig) so a host that joins later auto-syncs; the fan-out is the pragmatic first cut (no backend change, reversible). - Guided wizard (
MigratorWizardModal+Step6Storage) submission already inherits the active host (shared store); its pool picker host- scoping is a follow-up. - Live SSE for a remote host is not proxied (buffered passthrough); remote-host jobs update via the 5 s poll. Local SSE is suppressed while a peer is active to avoid mixing the local host’s jobs into the view.
- Windows preparer is per-host (
preparer_available): a Windows migration only runs where virt-v2v/virtio-win are provisioned (today only.11). The destination picker surfaces the chosen host’s capabilities so this is visible. siem.synnet.mxis not present on the currently-registered source (ESXi172.16.11.15); cross-host routing is validated by dry-run, the live cutover waits on the correct source/VM + a powered-off guest.