Skip to content

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.

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

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.

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

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 — optional hostFingerprint?: () => string|undefined; wrap paths via hostScopedPath.
  • src/stores/migrator.tsactiveHostFingerprint + setActiveHost + createSourceOnHost; bind api to it; SSE pollution guard.
  • src/views/MigratorView.vue — destination-host selector (cluster mode), fan-out commitSource, pass host to drawer, gate local SSE.
  • src/components/migrator/NewMigrationDrawer.vuehostFingerprint prop → useStoragePools(() => props.hostFingerprint).
  • 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.mx is not present on the currently-registered source (ESXi 172.16.11.15); cross-host routing is validated by dry-run, the live cutover waits on the correct source/VM + a powered-off guest.