Vrtmv
Vrtmv reverse-engineers Linux VMs from disk images and produces Ansible roles that rebuild an equivalent workload on a modern target OS — with a signed parity report as output.
Run vrtmv assess centos7.vmdk and get back a complete picture of what is inside. Run vrtmv migrate centos7.vmdk --target rocky9 and get back an Ansible role that rebuilds it on Rocky 9, plus a report attesting that the result is equivalent to the source.
Vrtmv reverse-engineers a workload from wherever it lives: a cold disk image, a running Linux VM, or a legacy bare-metal server. The source does not need to be live, reachable, or bootable — but if it is running, Vrtmv can collect from it directly over SSH, so physical machines and VMs that can’t be imaged are in scope too.
What Vrtmv is for
Hypervisor exit and distribution end-of-life (CentOS Linux, EOL June 2024) have turned Linux migration into a board-level programme in regulated enterprises. The tools that move virtual machines competently still deliver the same undocumented, drifted, pre-EOL pet VMs onto the new platform — no infrastructure-as-code, no evidence, no modernisation.
Vrtmv is the codification and attestation layer those tools do not provide:
- Any source. Cold disk images (no SSH, nothing booted), running Linux VMs, and legacy bare-metal servers — the last two collected over SSH. A physical box that predates your virtualization estate is reverse-engineered the same way a VMDK is.
- Offline analysis. For cold images, inventory extraction runs entirely on the customer host with nothing booted.
- Cross-distro translation. CentOS 7 in, Rocky 9 out. Package names, config paths, init-system differences, service accounts, and MAC (SELinux/AppArmor) posture — translated from a curated, hosted translation API.
- Parity attestation. Every output is auditable. Each translation carries provenance and a graded confidence; the report is a signed, timestamped artefact suitable as audit evidence.
- Your workload data stays local. Image contents, configuration, and extracted inventory never leave the customer environment. The only thing sent off-host is package identifiers, looked up against the Vrtmv translation API.
How to read these docs
- Concepts explains the client–server architecture, the Translation Index, and what “attestation” means here.
- Guide walks from install and authentication through your first migration.
- Commands is the reference for every
vrtmvsubcommand. - The Vrtmv API documents the authenticated service the client talks to, including accounts, quota, and billing.
Vrtmv is a commercial product. The curated Translation Index is served, never shipped — it is the asset, kept current centrally and licensed per account. See Accounts, quota & billing.
Architecture
Vrtmv is a client–server system with three parts.
The client — vrtmv
A single binary that runs where the customer’s disk images live. It probes a source, extracts an inventory locally, and performs every translation lookup over the network against the authenticated Vrtmv API. It never connects to the translation database directly, and it never sends workload data off-host — only package identifiers.
The client is written in Rust: it parses untrusted disk images, so memory safety is a security property, not a convenience.
The API — vrtmv-api
The only thing that connects to the Translation Index. It is the authentication and account boundary: licensing, metering, and quota all live here. Clients authenticate per account with a bearer token; each lookup is metered. See The Vrtmv API.
The Translation Index
A curated PostgreSQL database mapping packages, services, config paths, service accounts, and MAC policy across distributions, with provenance on every row. It is the product’s moat, and it is served, not shipped — never embedded in the client. See The Translation Index.
The pipeline
A migration flows through five stages:
- Probe / source — detect the image format, or open a mounted root or SSH source.
- Inventory — read the distro (
os-release) and installed packages (dpkg / rpm) locally. - Resolve — map native package names to canonical slugs via the API.
- Translate — fetch the target-distro packages, config-path relocations, and conditionals for those canonicals.
- Render — evaluate conditionals against the local inventory and emit an Ansible role plus a JSON attestation.
Stages 1–2 are entirely local. Stages 3–4 send only identifiers to the API. Stage 5 is local again — which is what makes the attestation specific to this host.
Where your data goes
| Data | Leaves the host? |
|---|---|
Image contents, /etc, accounts, keys | No |
| Extracted inventory (package list) | No |
| Package identifiers (names, canonical slugs, release pair) | Yes — to the API, to look up translations |
| Depersonalized VM fingerprint (a one-way hash) | Yes — for metering only |
| Diagnostics: command, outcome, OS pair, timings, counts, redacted error text | Beta channel only, and only after an account admin opts in — see The beta channel |
A stable build has no code path that sends that last row. The channel is a compile-time constant, so there is no flag or environment variable that turns reporting on; vrtmv diagnostics prints which channel a given binary is and what, if anything, it reports.
The Translation Index
The Translation Index is the curated PostgreSQL database that turns “what is installed on this CentOS 7 box” into “what to install, and how to configure it, on Rocky 9”. It is what the Vrtmv API serves.
What it covers
- Packages. Canonical packages (a distro-neutral identity, e.g.
cpkg:openssh), the native packages that realise them on each release, and per-(source, target)translations. - Name normalisation & sub-packages. Rename and split/merge relationships across families.
- Repository routing. Which repository a package comes from on the target.
- Config paths. Where a package’s configuration moves across a migration (chunk C relocations), including format-incompatible cases surfaced as caveats.
- Service units. systemd unit differences, including instance templates.
- Service accounts. The users and groups a package expects to exist.
- MAC policy. SELinux/AppArmor posture per release.
Conditionals
A translation can carry conditionals: a predicate (from a locked rule vocabulary) plus an effect to apply when the predicate is true on the source. The API resolves each predicate into a full tree and returns it; the client evaluates it locally against the inventory. This is why a translation can adapt to the specific source without the source’s state ever leaving the host.
Predicates that a cold image genuinely cannot answer (for example, whether a kernel module is currently loaded) evaluate to unknown and are surfaced as attestation caveats — never silently assumed.
Served, not shipped
The index is never embedded in the client or copied to the customer. It is kept current centrally and served through the authenticated API. This protects the curated asset and is the commercial control point: translations are licensed per account under contract, not distributed as a copyable, drifting local file.
Parity Attestation
Attestation is what separates Vrtmv from a package-renaming script. Every migration produces evidence that an auditor can rely on.
The attestation report
vrtmv migrate writes, alongside the Ansible role, a vrtmv-attestation.json. It records:
- the source and target releases;
- the depersonalized VM fingerprint and the anchors it was derived from (audit transparency);
- each translated canonical, its target packages, and its graded confidence;
- packages that resolved to no canonical, and canonicals with no translation to the target;
- manual runbook steps and caveats from conditionals that fired or could not be evaluated;
- config-path relocations that need operator attention.
Because conditionals are evaluated locally against the real inventory, the report is specific to the host that produced it — not a generic mapping.
Honesty over completeness
The report distinguishes clearly between what Vrtmv translated, what it could not, and what it could not determine. An unevaluable predicate becomes a caveat, not an assumption. A canonical with no vetted translation is reported as untranslated rather than guessed. Silence — the absence of a row — is treated as a truthful “nothing to report”, not a gap to paper over.
This discipline is deliberate: a fabricated mapping that looks authoritative is worse than an acknowledged gap, because an auditor may rely on it.
Fingerprints
The VM fingerprint is a one-way hash derived from stable host anchors (such as machine-id, fstab, and boot UUIDs). It lets Vrtmv recognise the same VM across runs — for metering and for linking a maintenance re-scan to its initial migration — without carrying any host identity off the machine.
Signing
When the service has signing enabled, vrtmv migrate also writes vrtmv-attestation.sig.json — a cryptographic signature over the report, produced by Vrtmv’s own key rather than a self-signed operator key.
The engine keeps workload data local: it sends only the report’s SHA-256 digest and the OS pair to the API, which signs a compact envelope binding that digest to the authenticated account, the moment, and Vrtmv’s key id. The signature file carries the signature, the exact bytes it covers, and a how_to_verify note. Vrtmv’s public key is published, unauthenticated, at /v1/attest/pubkey.
An audit firm verifies in two steps: recompute the SHA-256 of vrtmv-attestation.json and confirm it matches the digest in the signed envelope; then verify the Ed25519 signature against the published public key. What this attests is provenance — that a Vrtmv-authenticated account produced this exact report at this time, signed by Vrtmv — and it is tamper-evident: any later edit to the report changes its digest and breaks verification.
Provenance & Confidence
Every assertion Vrtmv makes is traceable and graded. These two properties are what make the output audit-defensible.
Provenance
Every row in the Translation Index carries a provenance reference, and every provenance record points to a real, published source — a vendor packaging guideline, an upstream document, an FHS reference, or per-engagement evidence. Bulk imports from packaging guidelines qualify; rows that cannot point at a source do not exist.
Confidence, graded honestly
Translations are graded, and the grade is reported:
| Confidence | Meaning |
|---|---|
high | Vendor-documented or upstream-verified. |
medium | Holds consistently across a family or rebuild relationship, but not yet Vrtmv-validated. |
low | An educated guess. |
untested | The default for any cross-family translation not yet exercised. |
By default the client suppresses untested rows from findings (--include-untested=false). You opt in explicitly when you want to see them.
Why this matters
The alternative — inferred or “this probably works” mappings presented as fact — produces findings worse than no findings, because they carry unearned authority into an audit. Vrtmv’s curation discipline forbids creating a rule without verifiable vendor evidence, and grades everything else honestly so a reader always knows how much weight a row can bear.
Installation
vrtmv is a single self-contained binary. It runs on Linux (x86-64) where your disk images or mounted sources live.
From a release build
Download the vrtmv binary for your platform, make it executable, and place it on your PATH:
chmod +x vrtmv
sudo mv vrtmv /usr/local/bin/
vrtmv --version
From source
The client is a standalone Rust crate. With a recent Rust toolchain (1.82+):
git clone <repository-url>
cd engine
cargo build --release
# binary at target/release/vrtmv (or $CARGO_TARGET_DIR/release/vrtmv)
What you need on the host
- Nothing extra for
--root(an already-mounted filesystem) or--sshcollection. - For
--image(mounting a cold disk image), Vrtmv shells out to standard system tools and needs root plus:losetupfor raw images;qemu-nbdand thenbdkernel module for qcow2/vmdk;mount, andlvmtooling for LVM-backed roots.
The block-layer attach is always read-only, so the source image is never modified.
Next
Set up your account credentials in Authentication, then run the Quickstart.
Authentication
Translation lookups (resolve, translate, config-paths, migrate, inventory --resolve, readiness) authenticate to the Vrtmv API with a per-account bearer token. Local-only commands (assess, drift) need no account.
Storing a token
vrtmv auth <token>
This writes the token to a credentials file with owner-only permissions. Show the current status, or clear it:
vrtmv auth # show masked token + stored URL
vrtmv auth --clear # remove stored credentials
To point at a non-default API endpoint, store a URL alongside the token:
vrtmv auth <token> --url https://api.vrtmv.com
Environment variables
Credentials can also come from the environment, which takes precedence over the stored file:
| Variable | Purpose |
|---|---|
VRTMV_API_KEY | Bearer token. |
VRTMV_API_URL | API base URL. |
Transport security
The API base URL must be https://. The client refuses to send your bearer token over cleartext http:// to a non-loopback host — the token is your licensing credential, and it is attached to every request. A local development server on localhost/loopback is allowed over http://, and an explicit VRTMV_ALLOW_INSECURE=1 overrides the check with a warning.
Managing tokens & usage
An account can hold multiple tokens, and each token’s usage is metered against the account. See Accounts, quota & billing.
Sources: image, root, SSH
Every extraction command (inventory, drift, migrate, and readiness per line) accepts a source three ways. They are mutually exclusive.
--image <file> — a cold disk image
Vrtmv detects the format from magic bytes and mounts it read-only. Supported formats:
| Format | Notes |
|---|---|
| raw | attached via losetup |
| qcow2 | attached via qemu-nbd |
| vmdk | attached via qemu-nbd |
| OVA | a tar bundle of a VMDK — extract first |
| ploop | needs ploop tools — extract first |
Mounting needs root and the relevant tooling (see Installation). LVM-backed roots are activated automatically; btrfs subvolumes and multi-device/RAID are not yet handled. The attach is read-only at the block layer, so a journal replay can never modify the source.
vrtmv assess <image>detects the format from magic bytes alone — no mounting, no root, no VM access.
--root <dir> — an already-mounted filesystem
Point Vrtmv at a directory that is the root of the source filesystem (a loop mount you manage, a snapshot, a sidecar-attached volume). No privileges beyond read access are required.
--ssh user@host — a running Linux VM or bare-metal server
Collect state from a live host over SSH into a local bundle, then analyse it exactly as if it were an image. This is how Vrtmv reverse-engineers workloads that can’t be imaged:
- Running Linux VMs — a live guest you can’t or don’t want to snapshot.
- Legacy bare-metal servers — physical machines that predate the virtualization estate, with no disk image to hand. Vrtmv treats them as first-class migration sources: collect over SSH, translate, and rebuild the workload on a modern VM or target OS.
Options:
| Flag | Purpose |
|---|---|
--ssh-port <n> | Non-default SSH port. |
--ssh-key <file> | Identity file (otherwise agent / ssh_config). |
--jump user@bastion | ProxyJump / bastion. |
--sudo | Wrap the remote read in sudo (needs passwordless sudo). |
Host and jump arguments are validated against argument-injection before any process is spawned.
Which to use
Cold image (--image) is the most audit-defensible: the source is never booted and cannot change during analysis. --root is operationally simplest when you already have the filesystem mounted. --ssh is for running hosts you cannot take offline.
Quickstart
This walks from a cold CentOS 7 image to a migration plan for Rocky 9.
1. Look inside the image
No account or mounting required — this reads magic bytes:
vrtmv assess centos7.vmdk
2. Extract the inventory
vrtmv inventory --image centos7.vmdk
# add --resolve to map packages to canonicals via the API
vrtmv inventory --image centos7.vmdk --resolve
3. Classify the configuration (local, no account)
vrtmv drift --image centos7.vmdk
drift separates /etc into what to carry (real workload config), what to hold back as a caveat (host identity, accounts, SSH keys), what is vendor-default, and what to skip (secrets, generated noise).
4. See feasibility before committing
vrtmv migrate --image centos7.vmdk --target rocky9 --preflight
--preflight reports counts and blocking steps, writes no files, and consumes no usage credit.
5. Produce the migration plan
vrtmv migrate --image centos7.vmdk --target rocky9 -o out/
This writes:
out/roles/vrtmv_migration/tasks/main.yml— the Ansible role that rebuilds the workload on Rocky 9;out/vrtmv-attestation.json— the signed, host-specific parity report.
6. Tag it (optional)
Record the migration under an engagement so it becomes a tracked, auditable record:
vrtmv migrate --image centos7.vmdk --target rocky9 -o out/ \
--engagement acme-2026 --vm-id web01 --migration-type initial
Commands
vrtmv is a single binary with these subcommands.
| Command | Account? | What it does |
|---|---|---|
assess | no | Detect a disk image’s source format from magic bytes. |
inventory | optional | Extract distro + installed packages from a source. |
drift | no | Classify /etc into carry / caveat / default / skipped; derive service accounts. |
resolve | yes | Map native package names to canonical slugs. |
translate | yes | Fetch target-distro packages for canonicals. |
config-paths | yes | Fetch config-path relocations for canonicals. |
migrate | yes | Full pipeline → Ansible role + attestation. Plan-only — never executes. |
run | yes | Unattended migration: plan → apply the role over real SSH → carry config → verify. |
deploy | no | Deploy the workload to OpenShift/Kubernetes (VM or container). |
readiness | yes | Fleet migration-readiness report across many sources. |
ui | — | Local web GUI (loopback only). |
auth | — | Store / show / clear API credentials. |
docs / help | — | Bundled documentation. |
resolve, translate, and config-paths are direct windows onto the API — mainly for scripting and debugging. Most users run assess, drift, migrate, and readiness.
Conventions
- Extraction commands take a source as
--image,--root, or--ssh(see Sources). -o/--outsets an output directory where relevant.- Local-only commands (
assess,drift) never call the API and need no account.
assess
Detect a disk image’s source format from its magic bytes. No mounting, no root, no account.
vrtmv assess <image>
| Argument | Purpose |
|---|---|
<image> | Path to the disk image (VMDK, qcow2, OVA, ploop, raw). |
assess is the fastest way to confirm Vrtmv recognises a source before you invest in mounting or migrating it. It reads only the leading bytes of the file, so it works on very large images without cost.
Recognised formats: vmdk, qcow2, ova, ploop, raw. See Sources for which of these can be mounted directly (--image) versus extracted first.
inventory
Extract the distribution and installed packages from a source. Extraction is entirely local; add --resolve to also map packages to canonicals via the API.
vrtmv inventory --image centos7.vmdk
vrtmv inventory --root /mnt/vm
vrtmv inventory --ssh user@host --sudo
vrtmv inventory --image centos7.vmdk --resolve
| Flag | Purpose |
|---|---|
--root / --image / --ssh | The source (see Sources). |
--ssh-port, --ssh-key, --jump, --sudo | SSH collection options. |
--resolve | Also resolve installed packages to canonical slugs via the API (needs an account). |
What it reads
- The distribution and version from
/etc/os-release(falling back to/usr/lib/os-release). - Installed packages from the dpkg status database (Debian family) or the rpm database (EL family — sqlite, ndb, or BerkeleyDB, auto-detected).
Without --resolve, inventory is a purely local report and needs no account. With --resolve, only package identifiers are sent to the API.
resolve
Map native package names to canonical slugs for a given release, directly against the API. Mainly for scripting and debugging; migrate does this for you as part of the pipeline.
vrtmv resolve --distro "CentOS Linux" --version 7 \
--name openssh-server --name httpd
| Flag | Purpose |
|---|---|
--distro | Source distribution, as the index names it (e.g. CentOS Linux). |
--version | Source version (e.g. 7). |
--name | A native package name. Repeatable; at least one is required. |
Names that resolve are returned with their canonical slug (e.g. cpkg:openssh); names with no mapping are returned as unresolved. Each call is metered against your account.
translate
Fetch the current translation for canonical packages, from a source release to a target release. Direct API access for scripting and debugging.
vrtmv translate \
--source-distro "CentOS Linux" --source-version 7 \
--target-distro "Rocky Linux" --target-version 9 \
--slug cpkg:openssh --slug cpkg:httpd
| Flag | Purpose |
|---|---|
--source-distro, --source-version | The source release. |
--target-distro, --target-version | The target release. |
--slug | A canonical slug (e.g. cpkg:httpd). Repeatable; at least one is required. |
Each translation comes back with its target packages, a graded confidence, any caveats, and any conditionals (predicate trees the client evaluates locally). A canonical with no vetted translation for this pair simply has no result — silence is the correct signal.
config-paths
Fetch config-path relocation rules for canonical packages, source release → target release. Direct API access; migrate applies these automatically.
vrtmv config-paths \
--source-distro "CentOS Linux" --source-version 7 \
--target-distro "Rocky Linux" --target-version 9 \
--slug cpkg:httpd
| Flag | Purpose |
|---|---|
--source-distro, --source-version | The source release. |
--target-distro, --target-version | The target release. |
--slug | A canonical slug. Repeatable; at least one is required. |
Config-path rules describe where a package’s configuration moves across a migration — for example, a default file that relocates, or a directory that changes name. A rule marked format-incompatible is surfaced as a caveat rather than silently applied. Most canonicals return no rule, because most configuration does not move; that absence is expected, not an error.
drift
Classify /etc into the files that actually encode this workload versus vendor-shipped defaults and host-specific noise. Runs entirely locally, calls no API, and needs no account — so it works even without credentials.
vrtmv drift --image centos7.vmdk
vrtmv drift --root /mnt/vm --json
vrtmv drift --ssh user@host --sudo --preserve-ssh
| Flag | Purpose |
|---|---|
--root / --image / --ssh | The source (see Sources). |
--ssh-port, --ssh-key, --jump, --sudo | SSH collection options. |
--preserve-ssh | Carry host SSH keys (public and private) instead of holding them back as a caveat. Off by default. |
--json | Emit the full classification as JSON instead of a text summary. |
The four lanes
| Lane | Meaning |
|---|---|
| carry | Workload configuration to reproduce on the target. |
| caveat | Host identity, accounts, and SSH keys — acknowledged, not blindly carried. |
| default | Vendor-shipped and unmodified — nothing to do. |
| skipped | Secrets, generated files, and noise — deliberately not read or carried. |
Modification is decided by checksum against the package’s own recorded baseline (dpkg .md5sums and conffiles; the rpm backend reconstructs file ownership). Symlinks are never followed, /etc/shadow and secrets are skipped unread, and reads are size-bounded.
Service accounts
drift also derives the service accounts a workload depends on — from file ownership and from systemd User=/Group= directives — and harvests unit enablement, masks, and operator-authored units. Where an owner cannot be resolved, that gap is reported rather than guessed.
data
Migrate the payload — the application data that no package owns and translation therefore cannot carry: uploads, document roots, /opt application trees, database files. Package translation reproduces the operating system; data handles what the workload exists to serve. It runs entirely locally, calls no API, and never moves a byte itself — it emits runbooks the operator runs.
Two phases:
# 1. discover — what non-distro data is on the source, and how to move it
vrtmv data discover --image centos7.vmdk
vrtmv data discover --root /mnt/vm --json
# 2. plan — emit runbooks for the paths you choose
vrtmv data plan --root /mnt/vm \
--copy /srv/www --copy /var/lib/mysql -o vrtmv-data-out
discover
Walks the source’s data roots (/var, /opt, /srv, /home, /usr/local, …; never /etc — that’s drift’s job), asks the packaging backend “does any package own this?” for every file, and groups everything unowned into candidate locations. Volatile and cache trees (/var/cache, /var/log, /tmp, the package DBs themselves) are pruned; symlinks are never followed.
Each location is classified and given a recommendation:
| Recommendation | Meaning |
|---|---|
| byte-copy | Plain data — safe to copy byte-for-byte, and attestable. |
db-export (engine) | A database data directory — its on-disk files must not be raw-copied across versions or engines. Routed to a logical export/import instead. |
LOCATION SIZE FILES RECOMMENDED
/var/lib/mysql 8.0 KiB 1 db-export (mysql)
/home/appuser/uploads 4.0 KiB 1 byte-copy
/srv/www/site 14 B 1 byte-copy
plan
Takes the paths you select with --copy (repeatable) and writes four artifacts to -o <out>:
| Artifact | What it is |
|---|---|
vrtmv-data-copy.sh | The byte-copy runbook — rsync -aHAX --numeric-ids per data path. |
vrtmv-data-manifest.sha256 | Per-file sha256 of the source, sha256sum -c-compatible — the byte-parity attestation. |
vrtmv-db-runbook.md | Step-by-step export/import commands for each detected database, with vendor references. |
vrtmv-data-attestation.json | The machine-readable attestation record (paths, hashes, verify verdicts). |
A path that matches a database signature is always routed to the DB runbook, never to a raw copy — even if you pass it to --copy.
Attestation
The manifest proves the copy is byte-identical. After copying, on the target:
sha256sum -c vrtmv-data-manifest.sha256 # every line must say OK
Optionally, point the tool at a destination tree and it re-hashes it for you, marking each file match / differ / missing in the attestation JSON (it still never performs the copy or the dump):
vrtmv data plan --root /mnt/vm --copy /srv/www --verify-dest /mnt/target/srv/www -o out
Databases
Detected by their vendor-documented on-disk data directory (and, for relocated dirs, a distinctive marker file). The runbook emits the supported dump/restore for each:
| Engine | Export → Import |
|---|---|
| PostgreSQL | pg_dumpall → psql |
| MySQL / MariaDB | mysqldump --all-databases → mysql |
| MongoDB | mongodump → mongorestore |
| Redis | redis-cli SAVE + copy dump.rdb |
| Elasticsearch | snapshot → restore |
| InfluxDB | influx backup → influx restore |
| Cassandra | nodetool snapshot → sstableloader |
| etcd | etcdctl snapshot save → restore |
Every command is a real vendor-documented procedure, cited in the emitted runbook — Vrtmv surfaces the steps; the operator runs them.
| Flag | Purpose |
|---|---|
--root / --image / --ssh | The source (see Sources). |
--copy <path> | A guest path to include (repeatable). |
--verify-dest <dir> | Re-hash this destination tree to attest byte-parity locally. |
-o, --out <dir> | Output directory for the runbooks + manifest. |
--json | Also print the attestation JSON to stdout. |
migrate
The full pipeline: inventory → resolve → translate → evaluate conditionals → emit an Ansible role plus a JSON attestation.
vrtmv migrate --image centos7.vmdk --target rocky9 -o out/
vrtmv migrate --root /mnt/vm --target rhel9 --preflight
| Flag | Purpose |
|---|---|
--root / --image / --ssh | The source (see Sources). |
--ssh-port, --ssh-key, --jump, --sudo | SSH collection options. |
--target <spec> | Target OS (default rocky9). See Supported targets. |
-o, --out <dir> | Output directory (default vrtmv-out). |
--format <list> | Output format(s): any of ansible, terraform (comma-separated; default ansible). |
--preflight | Report counts and blocking steps only; write no files and consume no usage credit. |
--engagement <id>, --vm-id <id>, --migration-type <t> | Tag the run as a VM migration record — see Tagging VM migrations. |
Output
A non-preflight run always writes vrtmv-attestation.json (the host-specific parity report). When the service has signing enabled it also writes vrtmv-attestation.sig.json, a Vrtmv-signed signature over that report (see Attestation → Signing). It then writes the runbook in each requested --format:
ansible(default) →roles/vrtmv_migration/tasks/main.yml— an Ansible role that installs the translated packages and reconciles config.terraform→terraform/cloud-init.yaml+terraform/main.tf— the translated workload packaged as cloud-init, wrapped in acloudinit_configwhose rendered output attaches to a compute resource asuser_data. This is the reusable foundation for the coming cloud targets (EC2 and friends); today it targets a VM you attach the cloud-init to.
Request both with --format ansible,terraform.
Preflight vs full run
--preflight is the feasibility check: it runs the same analysis, prints the translatable/unresolved/untranslated counts and the number of blocking manual steps, and stops. It writes nothing and is not billable. A full run records one depersonalized migration usage event; whether that event is billable depends on your plan and target (see Accounts, quota & billing).
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.
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/
| Flag | Purpose |
|---|---|
--root / --image / --ssh | The 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-sudo | Target 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. |
--resume | Skip phases already recorded complete under --out instead of restarting. |
--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-strict | The 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. |
--engagement, --vm-id, --migration-type | Tag the run — see Tagging VM migrations. |
--cutover | After a passing verify (and health gate, if requested), APPLY the cutover manifest instead of only emitting it. Requires --deploy-image or --deploy-disk-url. |
--deploy-image / --deploy-disk-url, --deploy-disk-size, --deploy-storage-class | The cutover VM’s disk source — a containerDisk image, or an imported disk via CDI DataVolume. Same shape as deploy. |
--deploy-namespace, --deploy-name | Namespace and name for the cutover manifest. |
--deploy-server, --deploy-token, --deploy-insecure-skip-tls-verify, --deploy-kubectl | Cluster 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.
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.
deploy
Deploy a workload to a namespace on an OpenShift or Kubernetes cluster — either a KubeVirt VirtualMachine or a Deployment + Service. Give it the info; it renders an auditable manifest, then applies it with oc (or kubectl).
# A VM from a containerDisk image
vrtmv deploy --kind vm -n prod --name web01 \
--image quay.io/example/rhel9-container-disk:latest --cpu 2 --memory 4Gi
# A VM booting from the migrated disk (imported via CDI)
vrtmv deploy --kind vm -n prod --name web01 \
--disk-url https://mirror.internal/disks/web01.qcow2 --disk-size 40Gi
# A container, exposed with a Service + Route
vrtmv deploy --kind container -n staging --name api \
--image registry.example.com/api:1.4 --replicas 3 --port 8080 --route
| Flag | Purpose |
|---|---|
--kind <vm|container> | What to deploy. |
-n, --namespace <ns> | Target namespace. |
--name <name> | Workload name (DNS-1123). |
--image <ref> | OCI image (container) or containerDisk image (vm). |
--disk-url <url> + --disk-size <q> | VM only: import the migrated disk (CDI DataVolume) instead of a containerDisk. |
--storage-class <name> | VM only: StorageClass for the imported disk. |
--cpu <n>, --memory <q> | vCPU cores and memory (default 1 / 2Gi). |
--replicas <n> | Container only (default 1). |
--port <n>, --route [--host <fqdn>] | Container only: expose via a Service, and optionally an OpenShift Route. |
--server <url>, --token <t> | Cluster API + bearer token. Omit to use your current oc login. |
--insecure-skip-tls-verify | Skip API-server TLS check (labs only). |
-o, --out <path> | Where to write the manifest (default vrtmv-deploy.yaml). |
--dry-run | Render the manifest but don’t apply it. |
--kubectl | Apply with kubectl instead of oc. |
How it works
deploy renders the manifest, keeps it as an auditable artifact (vrtmv-deploy.yaml, on brand with vrtmv’s other outputs), then applies it. Cluster access is your current oc login (recommended) or explicit --server/--token. When you pass --token, vrtmv hands it to oc through a private temporary kubeconfig rather than the command line, so the credential never appears in the process list. --dry-run renders without a cluster.
Inputs are validated before the cluster is touched — names as DNS-1123 labels, memory/disk as Kubernetes quantities, the image as a reference, and --disk-url/--host as quote-free values — so a mistake errors with guidance instead of producing a bad object.
From a migration
vrtmv migrate can emit the deploy manifest alongside the role and attestation when you give it a disk source:
vrtmv migrate --root /mnt --target rocky9 -o out \
--deploy-image quay.io/you/rocky9:latest --deploy-namespace prod --deploy-name web01
# → out/deploy/vrtmv-deploy.yaml (review, then: oc apply -f)
Prerequisites
- The OpenShift CLI
oc(orkubectlwith--kubectl) on yourPATH. - For
--kind vm: OpenShift Virtualization / KubeVirt on the cluster; for--disk-url, the CDI (Containerized Data Importer) operator. - For
--route: an OpenShift cluster (Routes are OpenShift-specific).
readiness
Scan many sources and emit an aggregated fleet migration-readiness report. The batch, non-billable sibling of migrate --preflight: no Ansible role and no per-VM attestation, just the readiness verdict across a fleet.
vrtmv readiness --manifest fleet.txt --target rocky9 -o report/
vrtmv readiness --image a.vmdk --image b.qcow2 --target rhel9
| Flag | Purpose |
|---|---|
--manifest <file> | A manifest of sources, one per line (each may set its own target). |
--image / --root / --ssh | Ad-hoc sources instead of a manifest (each repeatable). |
--target <spec> | Default target for entries that do not specify one (default rocky9). |
-o, --out <dir> | Output directory (default vrtmv-readiness). |
--format <list> | Report formats: any of json, md, csv (default all three). |
--include-untested | Count untested-confidence translations toward “ready” (by default they force a “review” verdict). |
--stop-on-error | Stop at the first VM that fails to scan instead of continuing. |
Verdicts
Each VM is graded ready, review, or blocked, with the gaps that drove the verdict. By default, translations of untested confidence force review rather than ready — you opt into counting them with --include-untested. The report rolls the fleet up per target and highlights the most common blockers.
Run vrtmv docs readiness for the manifest format.
ui
Launch a local web GUI for driving Vrtmv from a browser. It binds to loopback only.
vrtmv ui # http://127.0.0.1:8765
vrtmv ui --port 9000
| Flag | Purpose |
|---|---|
--port <n> | Port to bind on 127.0.0.1 (default 8765). |
The server listens only on the loopback interface and guards against DNS-rebinding. It is intended for interactive use on the analysis host, not as a shared service.
auth
Store, show, or clear the local API credentials so the CLI and GUI authenticate without environment variables.
vrtmv auth <token> # store a token
vrtmv auth <token> --url https://api.vrtmv.com
vrtmv auth # show masked token + stored URL
vrtmv auth --clear # remove stored credentials
| Argument / flag | Purpose |
|---|---|
<token> | The API token to store. Omit to show current status. |
--url <url> | Also store a custom API base URL. |
--clear | Remove the stored credentials. |
The credentials file is written with owner-only permissions, and the token is shown masked. See Authentication for environment-variable precedence and the HTTPS requirement.
Tagging VM migrations
A migration can be recorded as a first-class, auditable VM migration record under an engagement — so a fleet programme has a durable, queryable log of which VM went from which source OS to which target, and whether the run was an initial rebuild or a maintenance re-scan.
Tagging a run
Add the tag flags to migrate:
vrtmv migrate --image web01.vmdk --target rhel9 -o out/ \
--engagement acme-2026 \
--vm-id web01 \
--migration-type initial
| Flag | Purpose |
|---|---|
--engagement <id> | External id of an existing engagement. Requires --vm-id. |
--vm-id <id> | Customer-facing VM identity (hostname, CMDB id, inventory tag). Requires --engagement. |
--migration-type <t> | initial (default) or maintenance. |
Tagging is best-effort: it records the migration after the plan is built and never fails the run. The engagement must already exist (engagements are created through the curator workspace); an unknown engagement is reported and the run still completes.
The record
Each tag upserts one vm_migration row, keyed by (engagement, vm-id, migration-type), capturing:
- the VM identifier and its depersonalized fingerprint;
- the source and destination OS (linked to curated releases where they map);
- the migration type and timestamps.
Re-running the same (engagement, vm-id, migration-type) updates the existing record rather than creating a duplicate.
initial vs maintenance
- initial — the first full cross-distro rebuild: translate, plan, and attest.
- maintenance — a re-scan of an already-migrated VM, oriented toward drift against its attested state.
The type is captured on the record so a programme can distinguish first migrations from ongoing maintenance in its reporting.
The Vrtmv API
The Vrtmv API (vrtmv-api) is the authenticated service the client talks to. It is the only thing that connects to the Translation Index, and it is the account, licensing, and metering boundary.
Authentication
Every protected endpoint requires Authorization: Bearer <token>. Tokens are issued per account and are stored server-side only as a SHA-256 hash — the plaintext is never persisted. An account can hold multiple tokens, and each request is attributed to the token that authenticated it. See Authentication for the client side.
Request shape
- Requests and responses are JSON.
- Batch endpoints (
resolve,translate,config-paths) accept up to 1000 items per call. - Request bodies are size-limited and requests are time-bounded; a burst cannot exhaust the connection pool.
What the client sends
Only identifiers: package names, canonical slugs, a (source, target) release pair, and — for metering — a depersonalized VM fingerprint. Image contents and configuration never reach the API.
See Endpoints for the routes and Accounts, quota & billing for the commercial model.
Endpoints
All routes are under /v1. Every route except health requires a bearer token.
| Method & path | Auth | Purpose |
|---|---|---|
GET /v1/health | public | Liveness check. |
POST /v1/resolve | bearer | Native package names → canonical slugs for a release. |
POST /v1/translate | bearer | Canonicals → target packages (+ confidence, caveats, conditionals). |
POST /v1/config-paths | bearer | Canonicals → config-path relocations for a release pair. |
POST /v1/usage | bearer | Record a depersonalized VM usage event (preflight / readiness / migration). |
POST /v1/migration | bearer | Upsert a VM migration record under an engagement. |
GET /v1/account | bearer | The account’s billing/usage summary. |
Metering & usage
resolve, translate, and config-paths each record a metered request against the account. POST /v1/usage records a per-VM event whose kind is one of:
preflight— a feasibility check; never billable;readiness— a fleet feasibility check; never billable;migration— a real migration; billable-eligible depending on plan and target.
Errors
Errors return a JSON {"error": "..."} with a standard status code:
| Status | Meaning |
|---|---|
400 | Malformed request (empty batch, over the 1000-item limit, bad kind/migration_type). |
401 | Missing, invalid, or disabled token. |
402 | Migration quota reached — see Accounts, quota & billing. |
404 | Unknown engagement on /v1/migration. |
Accounts, quota & billing
vrtmv bills one price per distinct migrated instance, per year. Analysis is generous — only real migrations count, and re-migrating the same VM never bills twice.
On-Demand is $799 per instance a year, Pro $599 (bought 10 at a time) and Fleet $399 (100 at a time). Each price covers the migration, its signed parity attestation, and ongoing monitoring. There is no separate one-time fee.
What is billable
A usage event is billable-eligible only when all hold:
- its
kindismigration(preflight and readiness checks are always free); - the target is not a free target (
api.free_target, which is currently empty — every target bills); - the account is not on an unlimited plan.
Billing counts distinct VMs by fingerprint, so re-migrating the same VM does not bill twice.
The waterfall
Each account’s position is computed as a waterfall:
- Free quota — a number of billable instances included at no charge (10 by default).
- Annual subscription — beyond the free quota, each instance is billed at the account’s tier rate, per year, as a single subscription whose quantity tracks the instance count.
GET /v1/account returns this summary — free quota, billable instances, paid instances, the
per-instance annual rate and the annual total — which is what an account page renders.
Prepaid credits were retired on 2026-08-07 along with the one-time migration fee, since a credit pre-bought a per-migration charge that no longer exists. The
credits_*fields remain in the API response for compatibility and report zero. The ledger is preserved and closed: no new grants can be created. To comp an account, its free quota is raised instead.
Enforcement
- A non-paying account whose free quota is exhausted is refused a new billable migration with 402 Payment Required.
- A paying account soft-overages into invoiced on-demand usage rather than being blocked.
- An absolute hard cap applies to every non-unlimited account: once it reaches the per-account VM limit, further billable migrations are hard-blocked with 402 until the limit is raised. Unlimited accounts (negotiated large estates) are exempt.
Feasibility checks (preflight, readiness) are never gated.
Data boundary
Metering carries only a one-way VM fingerprint and the release pair — never host identity or workload data. See Security posture.
Supported targets
migrate and readiness take a target as a compact spec — a distribution name followed by a major version, with no separator.
rocky9 rhel10 alma9 ubuntu2404 debian12
Vrtmv expands the spec to the distribution and version the Translation Index uses. rocky9 is the default target for both migrate and readiness.
| Spec example | Expands to |
|---|---|
rocky9 | Rocky Linux 9 |
rhel10 | Red Hat Enterprise Linux 10 |
alma9 | AlmaLinux 9 |
ubuntu2404 | Ubuntu 24.04 |
debian12 | Debian 12 |
The set of source→target pairs that carry vetted translations is defined by the Translation Index; a pair with no coverage yields untranslated canonicals rather than guesses. Migrations to Red Hat Enterprise Linux are treated as free targets for billing.
Security posture
Vrtmv is built for regulated environments. The security model follows from one principle: workload data stays on the customer host, and every off-host interaction is minimal and authenticated.
Data locality
Image contents, /etc, accounts, and keys are never transmitted. From a stable-channel build, the only data that leaves the host is package identifiers (for translation) and a one-way VM fingerprint (for metering). See Architecture.
The beta channel is the one exception, and it is opt-in. A beta build additionally reports diagnostics on every run — command, outcome, OS pair, timings, counts, and a redacted error message. It never reports image contents, configuration, host names, addresses, paths or package name lists. An account admin enrols the account explicitly, and that enrolment is the consent; a stable build cannot report diagnostics under any configuration, because the channel is fixed at compile time rather than read at run time. Run vrtmv diagnostics on any binary to see which channel it is and exactly what it sends. See The beta channel.
Transport
The client refuses to send its bearer token over cleartext http:// to a non-loopback host; the API base URL must be https://. A loopback development server is the only exception, plus an explicit opt-out for controlled testing.
Credentials
Tokens are stored client-side in an owner-only file and shown masked. Server-side they are held only as a SHA-256 hash; the plaintext is never persisted. Tokens can be disabled per token or per account.
Untrusted input
The client parses cold, potentially hostile disk images. Package-database reads are size-bounded to prevent memory exhaustion, image parsing is panic-contained so a malformed database is a clean error rather than a crash, and paths supplied by the API are confined to the mounted image root. Block-layer attach is always read-only, so analysis cannot modify the source. Disk mounting shells out to standard, audited system tools rather than reimplementing block-device handling.
Service integrity
The API parameterises every database query, authorises every non-public route against the authenticated account, and bounds request size, duration, and batch size. Metering is attributed per token and recorded to an auditable log.
The beta channel
Vrtmv publishes two release channels. Stable is what an account gets by default. Beta carries features that have not reached a stable release yet, so an account can test them against its own estate before they ship.
Beta builds report diagnostics back to Vrtmv on every run. That is the condition of the channel, not a setting: there is no way to take the builds without it. This page is the full account of what that means, because a data-sharing term nobody can read in detail is not one anybody agreed to.
Beta is early, not unchecked
Beta artifacts go through exactly the same release pipeline as stable ones. Each is built by the same signed workflow, ships a CycloneDX SBOM, and carries a keyless Sigstore bundle binding it to the workflow, tag and run that produced the bytes. The website’s sync refuses to serve any artifact whose sha256 does not match its manifest and whose signature does not verify against the Vrtmv release identity — for beta exactly as for stable.
Verify a beta download the same way you verify a stable one:
cosign verify-blob --bundle vrtmv-<tag>-<target>.tar.gz.bundle \
--certificate-identity-regexp 'https://github.com/vrtmv/vrtmv/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
vrtmv-<tag>-<target>.tar.gz
What “beta” means is that the features are new, and that the binary reports on how they behave.
Exactly what a beta build sends
One report per command, after the command finishes:
| Field | What it is |
|---|---|
engine_version | The binary’s version |
channel | Always beta |
command | The subcommand you ran — migrate, verify, … Never its arguments |
outcome | ok, error or panic |
exit_code | The process exit code |
error_kind | A short classification, on failure |
error_detail | The error message, redacted — see below |
source_distro, source_version | The source OS, on commands that have one |
target_distro, target_version | The target OS |
counts | Sizes only: packages resolved, services seen, config paths carried. Never names |
duration_ms | How long the command took |
features | Which optional code paths ran, from a fixed list |
That table is the whole field set. The API rejects a report carrying any field not named here rather than accepting and discarding it, so a build that started sending something new would fail loudly on its first request.
What it never sends
- Image contents,
/etc, configuration file contents, accounts or keys — the same as stable. - Host names, IP addresses, file paths, or user names.
- Your package list. Package identifiers still go to the API to be translated, exactly as they do on stable; diagnostics carry counts, not names.
- Command arguments. A path or an image name passed on the command line is not collected in the first place, so there is nothing to redact.
Redaction, and what it does not promise
error_detail is the only free-text field. It is scrubbed twice: once inside the
binary before anything is transmitted, and again on the Vrtmv side before it is
stored. The first pass is the one that matters — bytes that never travel cannot leak
— and the second exists because a redactor that runs only in a shipped binary cannot
be fixed without shipping another one.
The scrubber replaces file paths with <path>, host names and URLs with <host>,
IP addresses with <addr>, email addresses with <email>, and long
credential-shaped strings with <id>. It deliberately leaves package names, version
strings and repository names alone, because those are the diagnosis.
This is a filter, not a proof. It recognises the shapes that carry identity in practice. It will not catch a host name that looks like an ordinary word, or a company name written into prose by a third-party tool whose output got wrapped into an error. We would rather say that plainly than describe it as complete.
You can run the scrubber yourself, on your own text, without sending anything:
vrtmv diagnostics --redact 'install failed on prod-db02.corp.example.com at 10.4.2.19 (see /var/log/dnf.log)'
install failed on <host> at <addr> (see <path>)
Checking what a binary does
Any build, on either channel, answers for itself:
vrtmv diagnostics
A stable build says it is stable and reports nothing. This is not a courtesy: the
channel is a const fixed when the binary is compiled, so a stable build has no code
path that sends diagnostics — not a disabled one, an absent one. No flag,
environment variable or configuration file can turn it on. The release workflow runs
this command against each artifact it builds and fails the release if the binary
disagrees with the channel the tag called for.
Enrolling: two acts, two roles
Enrolment is per account, and it takes two deliberate steps by two roles. Both happen in the customer portal under Downloads → Beta.
- A user requests it. Anyone signed in — member or admin — reads the terms above and submits a request. That records their acceptance, under a specific terms version. It does not enrol anything.
- An account admin approves it. The admin is shown the same terms and accepts them on the account’s behalf. That is the act that enrols. Approving covers every member, not just the person who asked, and the portal says so at the point of approval.
Both acceptances are stored separately, each with its own actor and terms version. They can legitimately differ — if the text changes between a request and its decision, the admin is told what the requester originally agreed to and that they are accepting the current text. Flattening the two into one record would describe an agreement that did not happen.
The split exists because the person who runs the beta binary is usually not the person who can bind the organisation. Giving members no way to ask means the feature goes unused; letting them enrol the account means a data-sharing term gets accepted by someone who cannot accept a bill.
An admin may approve their own request, and the record shows both acts by the same person rather than implying a second party. Most accounts have one admin; a rule requiring a second would mean those accounts could never enrol at all, and a safeguard whose only effect is to make the feature unreachable is not one.
Admins are emailed when a request arrives and see a count in the portal. A request can be declined, with a reason shown to the person who asked.
The consent text is versioned; if it changes, an enrolled account keeps its access and the portal asks an admin to read and re-accept the current version.
Leaving
Only an account admin can leave the channel, and leaving needs no fresh agreement — it is the one direction that never requires reading anything.
Leaving the channel takes effect immediately. Enrolment is re-read from the database on every download request and on every diagnostics submission, so there is no session or cached credential that keeps a withdrawn account working. To stop reports from a particular machine at once, replace its binary with a stable build — a beta binary that is already installed will keep trying, and will simply be refused.
What we do with it
Vrtmv engineers read these reports to find and fix what is breaking. Two consequences follow that the collection alone does not imply, so they are stated rather than left to inference:
- We may email you when something your builds reported is fixed, naming the build that fixes it. That is the point of collecting it — a fix nobody hears about helps no one.
- An engineer may open a support ticket on your behalf about an issue your builds reported, so you are not chasing something we already know about. Tickets are opened by a person, not by the console: no automated path can contact you.
Seeing it, and deleting it
Once enrolled, the customer portal lists every report your builds have sent, exactly as we hold it — the same rows an engineer sees, with the same redaction. The consent above enumerates what is collected; this is how you check that claim rather than take it.
Any of it can be deleted, individually or all at once, by any signed-in member of the account. Deletion is real:
- the row is removed — no copy, no tombstone, no anonymised remainder
- our own issue counts and priorities drop with it
- if we later fix something your report was the only evidence of, you will not be told about that fix, because the record connecting you to it is gone
That last point is the honest cost of a delete button that means what it says, and it is yours to weigh. What survives is a note that a deletion happened and how many rows it covered — never what was in them, since a deletion log that kept the content would be the opposite of a deletion.
Withdrawing from the channel stops future reports. It does not delete what was already sent; deleting that is a separate act, and the button is on the same page.
Retention
Diagnostics are kept for 180 days and then deleted, by a scheduled purge rather than by policy alone.
Leaving the beta channel stops future reports. It does not retroactively delete reports already made under a consent that was live at the time — those age out on the same 180-day schedule, or Vrtmv will delete them on request.
Gating
Beta artifacts are served from a separate location that is gated server-side on enrolment, not merely hidden in the portal: an unauthenticated or unenrolled request for a beta tarball is refused before any bytes are sent. Likewise the diagnostics endpoint refuses a report from an unenrolled account outright rather than accepting and discarding it — the consent says the data is not sent, and quietly binning it on arrival would make that false.