run

migrate and data plan stay plan-only: they write an Ansible role and attested runbooks, and a human applies them. run is a separate, explicitly-invoked command for an operator or pipeline that wants that same chain executed unattended instead: plan → apply the role over real SSH → carry the promised config → optional workload-data copy → verify → optional Kubernetes/OpenShift cutover. A full run records the same billable-eligible migration usage event as migrate; the following verify does not meter.

vrtmv run --root centos7-mnt --target rocky9 --target-ssh root@10.0.1.20 -o out/
vrtmv run --image centos7.vmdk --target rhel9 --target-ssh admin@10.0.1.30 \
  --copy /srv/www --copy /var/lib/mysql --notify-webhook https://hooks.example.com/vrtmv
vrtmv run-status -o out/
FlagPurpose
--root / --image / --sshThe source (see Sources). With --ssh, --copy runs directly on the source host — see below.
--target <spec>Target OS (default rocky9). See Supported targets.
--target-ssh <user@host>Where to apply the migration. A real host run connects to and changes.
--target-ssh-port, --target-ssh-key, --target-jump, --target-sudoTarget SSH connection options, mirroring the source's.
-o, --out <dir>Output directory for the role, attestation, verify report, and run-state file (default vrtmv-out).
--format <list>Must include ansible (default) — that's what run applies.
--copy <path>A guest data path to copy onto the target automatically (repeatable). Omit to carry OS/config only, as migrate alone promises.
--resumeSkip phases already recorded complete under --out instead of restarting.
--target-reposPlan with the target's repositories that are not in its default set (EPEL, a vendor's), as migrate --target-repos does. See below.
--retries <n>Attempts per external step — ssh, ansible-playbook, API calls (default 3; 1 disables retry).
--not-before <epoch seconds>Wait until this time before touching the target; planning still runs immediately.
--notify-webhook <url>Best-effort JSON POST on the terminal state (done/blocked/failed). Never fails the run.
--require-health, --health-key-file, --health-max-age-mins, --health-strictThe same vrtmn HealthReport gate verify --require-health offers, checked before run can report done.
--job-id <id>Stamp an external id into the run-state file and any webhook payload. Generated if omitted.
--cutoverAfter a passing verify (and health gate, if requested), APPLY the cutover manifest instead of only emitting it. Requires --deploy-image or --deploy-disk-url.
--rollback-cutoverDelete the Kubernetes object a prior --cutover applied. Reads <out>/deploy/vrtmv-cutover.json (namespace/name/kind). Does not reverse apply or data copy.
--traffic-cutoverDrain → forward → health from operator-authored <out>/deploy/vrtmv-traffic.json (kind: command argv only). Does not guess a VIP. Health fail rolls back completed traffic steps. Also runs as a run phase after verify/--cutover when that json is present; absence is silence.
--rollback-trafficReverse completed traffic steps. Independent of --rollback-cutover. Unread vrtmv-traffic-state.json refuses rather than looking like nothing completed.
--deploy-image / --deploy-disk-url, --deploy-disk-size, --deploy-storage-classThe cutover VM's disk source — a containerDisk image, or an imported disk via CDI DataVolume. Same shape as deploy.
--deploy-namespace, --deploy-nameNamespace and name for the cutover manifest.
--deploy-server, --deploy-token, --deploy-insecure-skip-tls-verify, --deploy-kubectlCluster connection for --cutover. Omit --deploy-server/--deploy-token to use the current oc/kubectl login.

vrtmv run-status -o <dir> prints the last recorded phase/verdict without running anything — poll this from a pipeline instead of tailing output.

Phases and the run-state file

Progress is written to <out>/.vrtmv-run-state.json after each phase (plan, apply, config_carry, data_copy, verify, cutover). --resume re-reads it and skips whatever already completed. run always re-runs planning even on resume — it's cheap, and it's what holds the source root open for the phases after it.

Packages the target has only in another repository

Without --target-repos, run plans as migrate does without it: a package the target carries only in a repository outside its default set is not installed, the report names it, and a service of that software is not enabled. With --target-repos the role run applies enables the repository on the target and installs those packages. That is a change to the target's repository policy, and passing the flag is what authorises it.

It decides more than the package. A service is planned only when its software is in the install list. Rocky Linux 9 has openvpn in EPEL alone, so a Gentoo source's openvpn-1194 is mapped onto openvpn-server@openvpn-1194.service only in a run with --target-repos; see A package's OpenRC script. In that run the plan lists the instance's file and the carried files it names beside itself as moving to /etc/openvpn/server/, and the config carry copies each to that path as well as to the path it had. The carry comes after the role, so while the role runs the file is not yet at the unit's path, and the play ends with a failure: the role's last task names the file, unless an earlier report has already stopped the play (a package that did not install under its own name, see below). run prints the warning and goes on, as it does for any failure the role reports. The role enables the unit and does not start it. A private key is never carried: you bring server.key, and the unit does not start without it. The target's openvpn is the version EPEL has, which may be older than the source's; whether the instance's file suits it is yours to check.

Pass --target-repos again with --resume. The plan is built again on every invocation, and a resumed run without the flag plans without those packages.

What it refuses to automate

Database export/import. If the data plan includes a database, run stops and names it as a blocking manual step rather than running an export/import command with no verified credentials to run it with. Follow <out>/data/vrtmv-db-runbook.md by hand, then --resume.

Workload data from an --ssh source runs on the source host itself. SSH collection only pulls the paths config analysis needs, not arbitrary application data, so the locally collected root has nothing to copy from. run instead discovers, hashes, and copies --copy paths directly on the source over its own SSH connection — database classification is the same path match data plan uses, so a database is still refused. The copy needs the orchestrator's ssh-agent forwarded to the source so it can authenticate to the target without the target's key ever being copied there: use an agent that already has the target's key loaded, or pass --target-ssh-key to load one into a throwaway agent.

A partial package-install does not abort the run

The generated role installs packages individually and reports whenever any package can't install under its own name — including the common case where the target's base already provides that capability under a different name (curl-minimal providing curl on an EL9-family target). That makes ansible-playbook itself exit non-zero, but run doesn't treat it as fatal: it logs the failure and continues to config-carry and verify, because verify's provides-aware check is the real judge of parity, not one Ansible task's exit code.

Exit codes

Only a Done outcome with verdict equivalent exits zero. Blocked (a translation blocker, a refused unattended step, or a non-equivalent verify verdict) and Failed (a genuine infrastructure error — couldn't connect, couldn't run ansible-playbook) both exit non-zero, so a pipeline can't go green on an incomplete migration.