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 vrtmv subcommand.
  • 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:

  1. Probe / source — detect the image format, or open a mounted root or SSH source.
  2. Inventory — read the distro (os-release) and installed packages (dpkg / rpm) locally.
  3. Resolve — map native package names to canonical slugs via the API.
  4. Translate — fetch the target-distro packages, config-path relocations, and conditionals for those canonicals.
  5. 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

DataLeaves the host?
Image contents, /etc, accounts, keysNo
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:

ConfidenceMeaning
highVendor-documented or upstream-verified.
mediumHolds consistently across a family or rebuild relationship, but not yet Vrtmv-validated.
lowAn educated guess.
untestedThe 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 --ssh collection.
  • For --image (mounting a cold disk image), Vrtmv shells out to standard system tools and needs root plus:
    • losetup for raw images;
    • qemu-nbd and the nbd kernel module for qcow2/vmdk;
    • mount, and lvm tooling 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:

VariablePurpose
VRTMV_API_KEYBearer token.
VRTMV_API_URLAPI 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:

FormatNotes
rawattached via losetup
qcow2attached via qemu-nbd
vmdkattached via qemu-nbd
OVAa tar bundle of a VMDK — extract first
ploopneeds 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:

FlagPurpose
--ssh-port <n>Non-default SSH port.
--ssh-key <file>Identity file (otherwise agent / ssh_config).
--jump user@bastionProxyJump / bastion.
--sudoWrap 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

See Tagging VM migrations.

Commands

vrtmv is a single binary with these subcommands.

CommandAccount?What it does
assessnoDetect a disk image's source format from magic bytes.
inventoryoptionalExtract distro + installed packages from a source.
driftnoClassify /etc into carry / caveat / default / skipped; derive service accounts.
resolveyesMap native package names to canonical slugs.
translateyesFetch target-distro packages for canonicals.
config-pathsyesFetch config-path relocations for canonicals.
migrateyesFull pipeline → Ansible role + attestation.
readinessyesFleet migration-readiness report across many sources.
uiLocal web GUI (loopback only).
authStore / show / clear API credentials.
docs / helpBundled 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 / --out sets 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>
ArgumentPurpose
<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
FlagPurpose
--root / --image / --sshThe source (see Sources).
--ssh-port, --ssh-key, --jump, --sudoSSH collection options.
--resolveAlso 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
FlagPurpose
--distroSource distribution, as the index names it (e.g. CentOS Linux).
--versionSource version (e.g. 7).
--nameA 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
FlagPurpose
--source-distro, --source-versionThe source release.
--target-distro, --target-versionThe target release.
--slugA 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
FlagPurpose
--source-distro, --source-versionThe source release.
--target-distro, --target-versionThe target release.
--slugA 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
FlagPurpose
--root / --image / --sshThe source (see Sources).
--ssh-port, --ssh-key, --jump, --sudoSSH collection options.
--preserve-sshCarry host SSH keys (public and private) instead of holding them back as a caveat. Off by default.
--jsonEmit the full classification as JSON instead of a text summary.

The four lanes

LaneMeaning
carryWorkload configuration to reproduce on the target.
caveatHost identity, accounts, and SSH keys — acknowledged, not blindly carried.
defaultVendor-shipped and unmodified — nothing to do.
skippedSecrets, 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
FlagPurpose
--root / --image / --sshThe source (see Sources).
--ssh-port, --ssh-key, --jump, --sudoSSH collection options.
--target <spec>Target OS (default rocky9). See Supported targets.
-o, --out <dir>Output directory (default vrtmv-out).
--preflightReport 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
FlagPurpose
--manifest <file>A manifest of sources, one per line (each may set its own target).
--image / --root / --sshAd-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-untestedCount untested-confidence translations toward "ready" (by default they force a "review" verdict).
--stop-on-errorStop 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
FlagPurpose
--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 / flagPurpose
<token>The API token to store. Omit to show current status.
--url <url>Also store a custom API base URL.
--clearRemove 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
FlagPurpose
--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 & pathAuthPurpose
GET /v1/healthpublicLiveness check.
POST /v1/resolvebearerNative package names → canonical slugs for a release.
POST /v1/translatebearerCanonicals → target packages (+ confidence, caveats, conditionals).
POST /v1/config-pathsbearerCanonicals → config-path relocations for a release pair.
POST /v1/usagebearerRecord a depersonalized VM usage event (preflight / readiness / migration).
POST /v1/migrationbearerUpsert a VM migration record under an engagement.
GET /v1/accountbearerThe 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:

StatusMeaning
400Malformed request (empty batch, over the 1000-item limit, bad kind/migration_type).
401Missing, invalid, or disabled token.
402Migration quota reached — see Accounts, quota & billing.
404Unknown 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 kind is migration (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:

  1. Free quota — a number of billable VMs included at no charge.
  2. Prepaid credits — an append-only, auditable ledger of granted or purchased credits.
  3. 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 exampleExpands to
rocky9Rocky Linux 9
rhel10Red Hat Enterprise Linux 10
alma9AlmaLinux 9
ubuntu2404Ubuntu 24.04
debian12Debian 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.