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 |
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.
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. |
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.
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). |
--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 writes:
roles/vrtmv_migration/tasks/main.yml— the Ansible role that rebuilds the workload on the target;vrtmv-attestation.json— the host-specific parity report.
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).
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 meters and bills on distinct migrated VMs. Analysis is generous; only real migrations to non-free targets count.
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 (migrations to Red Hat Enterprise Linux are free);
- 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 VMs included at no charge.
- Prepaid credits — an append-only, auditable ledger of granted or purchased credits.
- On-demand — beyond free quota and credits, additional VMs are invoiced per VM.
GET /v1/account returns this summary — free quota, billable VMs, credits total/used/remaining, on-demand VMs, and amount due — which is what an account page renders.
Enforcement
- A non-paying account whose free quota and credits are 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) and free-target migrations 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. The only data that leaves the host is package identifiers (for translation) and a one-way VM fingerprint (for metering). See Architecture.
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.