Test suite for validating VM migrations to OpenShift from VMware vSphere, RHV, OpenStack, and OVA using Migration Toolkit for Virtualization (MTV).
Full Documentation | Copy-Offload Guide
The documentation site in docs/ is generated with
pi-docsite: edit docs/*.md, keep the sidebar
order in docs/nav.json, then rebuild the generated files with
uvx pi-docsite --docs-dir docs \
--tagline "Pytest-based integration suite for validating Migration Toolkit for Virtualization migrations into OpenShift Virtualization."Never edit the generated *.html, assets/, search-index.json, llms.txt or
llms-full.txt by hand. See AGENTS.md.
- OpenShift cluster with MTV operator installed
- Podman or Docker - To run the test container
- Linux/macOS: Podman or Docker
- Windows: Docker Desktop or Podman Desktop
You need a base VM/template in your source provider:
| Provider | Resource Type | Requirements |
|---|---|---|
| VMware vSphere | VM | Powered off, QEMU guest agent installed |
| RHV/oVirt | Template | Min 1536 MiB memory |
| OpenStack | Instance | ACTIVE/SHUTOFF state, QEMU guest agent installed |
| OVA | OVA file | NFS-accessible OVA files |
Note: Copy-offload tests have additional prerequisites. See the Copy-Offload Testing Guide for details.
podman --version # or: docker --versionOptional - If you have oc CLI installed, you can verify your cluster:
oc whoami # Check cluster access
oc get csv -n openshift-mtv | grep mtv # Verify MTV operatorImportant: A pre-built public image is available at ghcr.io/redhatqe/mtv-api-tests:latest. You can use it directly
or build and push your own custom image.
Option A: Use the public image (recommended):
Use ghcr.io/redhatqe/mtv-api-tests:latest directly in the commands below.
Option B: Build your own custom image:
# Clone the repository
git clone https://github.com/RedHatQE/mtv-api-tests.git
cd mtv-api-tests
# Build the image (use 'docker' if you prefer Docker)
podman build -t <YOUR-REGISTRY>/mtv-tests:latest .
# Push to your registry
podman push <YOUR-REGISTRY>/mtv-tests:latestReplace <YOUR-REGISTRY> with your registry (e.g., quay.io/youruser, docker.io/youruser).
Ensure your OpenShift user has permissions to create MTV resources and VMs.
For Test/Development Clusters Only:
oc adm policy add-cluster-role-to-user cluster-admin $(oc whoami)For Production/Shared Environments (Recommended - Least Privilege):
Instead of granting cluster-admin, create a dedicated role (e.g., mtv-operator-role) that grants only the
required permissions for MTV testing. Your cluster admin should:
-
Create a custom Role with specific verbs and resources:
- Verbs:
create,delete,get,list,watch,update,patch - MTV Resources (API group
forklift.konveyor.io):virtualmachines,plans,providers,storagemaps,networkmaps,migrations,hooks
- Core Resources (in
mtv-testsnamespace):secrets,configmaps,persistentvolumeclaims,pods,services
- Read-only access (cluster-scoped):
storageclasses,namespaces
- Verbs:
-
Bind the role to your test user:
# Example: Bind the custom role to the test user oc adm policy add-role-to-user mtv-operator-role $(oc whoami) -n mtv-tests oc adm policy add-role-to-user mtv-operator-role $(oc whoami) -n openshift-mtv
This least-privilege approach limits the blast radius and follows security best practices for production environments.
What is .providers.json? A configuration file that tells the tests how to connect to your source
virtualization platform.
Why do you need it? The tests need to:
- Connect to your source provider (vSphere, RHV, OpenStack, OVA, or Hyper-V)
- Find the base VM to clone for testing
- Create test VMs and perform migrations
What should it include?
- Connection details (hostname, credentials)
- Location information (datacenter, cluster)
- Base VM/template name to use for testing
Protect your credentials file:
.providers.json file contains sensitive credentials. Follow these security practices:
- Set restrictive permissions:
chmod 600 .providers.json(owner read/write only) - Never commit to Git: Add
.providers.jsonto your.gitignorefile - Rotate secrets regularly: Update passwords and credentials on a regular schedule
- Use secret management: For OpenShift deployments, use Kubernetes secrets
- Delete when done: Remove the file from local systems when no longer needed
About # pragma: allowlist secret comments:
⚠️ The JSON examples below contain# pragma: allowlist secretcomments - these are REQUIRED for this repository's pre-commit hooks but are NOT valid JSON. Do NOT copy these comments to your actual.providers.jsonfile. They exist only for documentation tooling, not security.
By default, the file is loaded from .providers.json in the current directory. You can override this with:
--providers-json /path/to/file.jsonpytest CLI argumentPROVIDERS_JSON_PATH=/path/to/file.jsonenvironment variable
Priority: CLI arg > environment variable > default .providers.json
Create a providers JSON file with your provider's details:
- default location:
.providers.jsonin the current directory - custom location: any path passed via
--providers-jsonorPROVIDERS_JSON_PATH
VMware vSphere Example:
{
"vsphere-8.0.1": {
"type": "vsphere",
"version": "8.0.1",
"fqdn": "vcenter.example.com",
"api_url": "https://vcenter.example.com/sdk",
"username": "administrator@vsphere.local",
"password": "your-password", # pragma: allowlist secret
"guest_vm_linux_user": "root",
"guest_vm_linux_password": "your-vm-password" # pragma: allowlist secret
}
}Key requirements:
- Configuration key must follow pattern:
{type}-{version}(e.g.,"vsphere-8.0.1"for vSphere 8.0.1) - This key is used directly with
--tc=source_provider:vsphere-8.0.1 - Replace
8.0.1with your actual vSphere version - both the key andversionfield must match - All fields shown above are required
- Replace placeholder values with your actual credentials and endpoints
For other providers (RHV, OpenStack, OVA, Hyper-V, or copy-offload configuration):
# Use the example file as a template
cp .providers.json.example .providers.json
# Edit with your actual valuesSee .providers.json.example for complete templates of all supported providers.
Hyper-V management_type: Optional. "cluster" for a Failover Cluster, "standalone" for a single host.
The value is passed through to spec.settings.managementType on the MTV Provider CR. If the key is omitted,
the suite does not set the field and MTV uses standalone.
Hyper-V two-phase transfer: TestColdMigrationComprehensive passes Plan pre_copy_source_disks (spec
preCopySourceDisks) when the source provider is Hyper-V. The Hyper-V Provider CR is created with
smb_max_channels from tests/tests_config/config.py (default 4, spec settings.smbMaxChannels) when any
plan config sets pre_copy_source_disks: True. Other Hyper-V tests keep MTV defaults (preCopySourceDisks
unset; smbMaxChannels omitted unless that plan flag is present).
Hyper-V preserve_static_ips: Before creating the Plan, the suite waits until Forklift inventory has
guestNetworks entries matching every static IPv4 address reported in the saved source VM data
(host KVP is not sufficient on cluster). Any required gateway must appear on the matching entry, not on
an unrelated NIC. The existing IPv4/gateway readiness check also remains: a gateway is required when the
source reports one or its network metadata is incomplete. The gateway requirement is waived only when
every source NIC has IP entries that explicitly report no gateway; missing metadata is not treated as
gatewayless. Timeout scales with HYPERV_REFRESH_INTERVAL (default ~700s).
Schema & IDE support:
providers_schema.jsondocuments which fields are required vs optional per provider type. Add"$schema"as the first key in your.providers.jsonfor IDE autocomplete:{ "$schema": "https://raw.githubusercontent.com/RedHatQE/mtv-api-tests/main/providers_schema.json", "my-vsphere": { ... }, "my-ovirt": { ... } }
Check which storage classes are available in your OpenShift cluster:
oc get storageclassPick one that supports block storage (e.g., ocs-storagecluster-ceph-rbd, ontap-san-block).
You'll use this name in the next step.
Execute tier0 tests (smoke tests) using the containerized test suite:
# Set cluster password in environment variable (avoids shell history exposure)
export CLUSTER_PASSWORD='your-cluster-password' # pragma: allowlist secret
podman run --rm \
-v $(pwd)/.providers.json:/app/.providers.json:ro \
-e CLUSTER_PASSWORD \
ghcr.io/redhatqe/mtv-api-tests:latest \
uv run pytest -m tier0 -v \
--tc=cluster_host:https://api.your-cluster.com:6443 \
--tc=cluster_username:kubeadmin \
--tc=cluster_password:${CLUSTER_PASSWORD} \
--tc=source_provider:vsphere-8.0.1 \
--tc=storage_class:YOUR-STORAGE-CLASSSecurity Note: Use environment variables to avoid shell-history exposure. Note the expanded value can still appear in process listings inside the container; prefer OpenShift Secrets or other secret injection where possible.
Note: On RHEL/Fedora with SELinux, add
,zto volume mounts:-v $(pwd)/.providers.json:/app/.providers.json:ro,z. You can usedockerinstead ofpodmanif preferred.Windows Users: Replace
$(pwd)with${PWD}in PowerShell or use absolute paths likeC:\path\to\.providers.json:/app/.providers.json:ro. Requires Docker Desktop or Podman Desktop.Non-root Container: The container runs as UID 1001. Read-only mounts (
:ro) are unaffected, but writable bind mounts must be accessible to that UID. See the Test Results and Reports section for details.
Replace:
https://api.your-cluster.com:6443→ Your OpenShift API URLkubeadmin→ Your cluster usernameyour-cluster-password→ Your cluster passwordYOUR-STORAGE-CLASS→ Your storage class from step 4vsphere-8.0.1→ Provider key from your.providers.json(e.g.,vsphere-8.0.1,ovirt-4.4.9)
Cluster credentials: each of cluster_host, cluster_username, and cluster_password is resolved from the
pytest config value first (--tc=cluster_host:...) and then from the matching CLUSTER_HOST, CLUSTER_USERNAME,
or CLUSTER_PASSWORD environment variable. Runs that provide any of the three must provide all three as nonblank
values; partial, empty, or whitespace-only sets fail fast with a named error such as
Missing cluster credentials: set 'cluster_username' in pytest config or CLUSTER_USERNAME. Omitting all three is
valid only for ambient authentication from a kubeconfig available to the test process. mtv-api-tests run reaches that
path only when no OpenShift provider is selected from .providers.json — the CLI checks for an existing oc token only
then and does not pass a standalone token to the client. See
runtime configuration for details.
VM naming: the OpenShift version segment of generated source VM names is read from the target cluster
automatically — do not pass --tc=target_ocp_version.
Set MTV_REMOTE_PDB=1 to enable a remote pdb debugger reachable over a socket,
for interactive debugging of test runs by humans or AI agents. Without the variable, debugging is completely disabled
and the helpers below are no-ops.
Run with -s so the listening message is not swallowed by pytest output capture, and use a single worker (-n 0)
when debugging — parallel xdist workers listen on distinct ports (4444 + worker index), but a debugger session per
worker is rarely what you want.
Temporarily insert a call where you want to stop (remove it when done):
from utilities.debug import remote_breakpoint
remote_breakpoint()Run the test; the process blocks at the call and waits for a debugger connection.
With MTV_REMOTE_PDB=1 set, any test failure automatically opens a remote post-mortem session on the failing
traceback (no --pdb needed). If you pass --pdb, the local terminal debugger takes precedence.
# host and port come from the run's "RemotePdb session open at <host>:<port>" line
# (defaults: 127.0.0.1 4444; see the REMOTE_PDB_HOST / REMOTE_PDB_PORT overrides below)
nc <host> <port> # or: rlwrap nc <host> <port> / telnet <host> <port>Send c to continue the run. q — or dropping the connection — quits the debugger: at a code
breakpoint the current test fails with BdbQuit and the run continues; at a failure post-mortem the whole
test session exits (matching pytest's native --pdb quit behavior).
The debugger binds to 127.0.0.1 only — it is never exposed on the cluster network. To debug a run inside an
OpenShift Job pod, the pod must run with MTV_REMOTE_PDB=1 — the Job manifest generated by the CLI (see
Running as OpenShift Job) does not set it, so add it to the container env
(or prefix the pytest command in the manifest with MTV_REMOTE_PDB=1) before creating the Job. Then forward
the port:
oc port-forward must stay alive while the client connects — run it in the background (or a
separate terminal) as shown, and stop it when the session ends:
port=4444 # replace with the numeric listener port from the run's "RemotePdb session open at" line
oc port-forward -n "$job_namespace" "pod/$pod_name" "$port:$port" &
forward_pid=$!
nc localhost "$port"
kill "$forward_pid"$job_namespace is the generated UUID-suffixed namespace shown by the CLI at manifest generation
(and present in the manifest) — oc port-forward without -n searches only your current namespace
and will not find the pod.
Host and port can be overridden with REMOTE_PDB_HOST / REMOTE_PDB_PORT. The host must resolve to a
loopback address — non-loopback values are rejected with a ValueError, so the debugger can never be
exposed on the cluster network.
The repo ships an agent skill for driving debugger sessions — see skills/. It is not
auto-discovered: symlink it into your agent harness's skills directory manually (see
skills/README.md), and restart the session so the harness picks it up.
Note: while a debugger session is attached, the test process is frozen but the cluster keeps moving — migrations and reconciliation continue server-side. Use the debugger for inspection; avoid long pauses inside migration polling loops.
The Quick Start runs tier0 tests (smoke tests). You can run other test categories by changing the -m marker:
| Marker | What It Tests | When to Use |
|---|---|---|
tier0 |
Smoke tests - critical paths | First run, quick validation |
tier1 |
Extended functionality tests | Broader coverage beyond smoke, including skip guest conversion |
copyoffload |
Fast migrations via shared storage | Testing storage arrays |
copyoffload_sanity |
Copy-offload sanity subset (see below) | Quick copy-offload validation |
warm |
Warm migrations (VMs stay running) | Specific scenario testing |
shared_disk |
Shared disk migration tests | Testing shared disk between VMs |
ca_crt |
CA certificate field (ca.crt) in provider secrets | Testing ca.crt secret field support |
vsphere |
VMware vSphere provider-specific tests | Tests specific to vSphere provider |
hyperv |
Hyper-V provider-specific tests | Tests specific to Hyper-V provider |
upgrade |
Migration across MTV operator upgrades | Validating upgrade compatibility |
The existing TestSanityWarmMtvMigration also verifies qcow2 overlay lifecycle logs for vSphere
sources (MTV-5644) after migration completes, using the same VM, Plan, and conversion pods.
It checks in-place conversion, matching created/committed overlay filenames, and no discarded
overlays before the usual migrated-VM checks. Conversion logs are captured during migration,
alongside Deep Inspection capture, and retained by migration, Plan VM ID, and conversion pod UID
for verification after success. A complete capture requires logs read after the conversion container terminates;
after Plan success, the test waits up to 60 seconds for Kubernetes to report termination and
capture those logs. Missing or incomplete captures and missing lifecycle messages fail the sanity
test. The capture must include one completed conversion pod for each configured Plan VM ID. Pods
that disappear before complete capture can still prevent verification. RHV runs do not perform this check.
tests/warm/test_resume_conversion_warm_migration.py covers vSphere warm
migration recovery after conversion failure and rejection of a resume request
without a prior migration failure. These tests require an MTV version supporting
resumeConversion and openshift-python-wrapper version 11.0.148 or later.
The copyoffload_sanity marker selects a curated subset of copy-offload tests that cover the core
migration scenarios. Use this marker for quick validation of copy-offload functionality without
running the full suite. The sanity set covers:
| Test ID | Test Class | What It Covers |
|---|---|---|
| MTV-562 | TestCopyoffloadRdmVirtualDiskMigration |
RDM (Raw Device Mapping) virtual disk migration |
| MTV-564 | TestCopyoffloadMultiDatastoreMigration |
VMs with disks spread across multiple datastores |
| MTV-565 | TestCopyoffloadMixedDatastoreMigration |
Mixed environment - XCOPY-capable and non-XCOPY datastores with fallback |
| MTV-572 | TestCopyoffloadScaleMigration |
Scale migration (5 VMs concurrently) |
| MTV-573 | TestCopyoffload10MixedDisksMigration |
VM with 10 mixed disks (thin and thick provisioned) |
| MTV-577 | TestCopyoffloadWarmMigration |
Warm (live) migration with copy-offload |
Together these tests validate: different disk types (thin, thick, RDM), multi-disk and multi-datastore layouts, mixed XCOPY/non-XCOPY fallback behavior, concurrent scale migration, and warm migration -- providing broad coverage of copy-offload functionality in a single run.
# Run only the sanity subset
podman run ... uv run pytest -m copyoffload_sanity -v --tc=source_provider:vsphere-8.0.1 ...Examples - Change -m tier0 to run different tests:
Note: In the examples below, replace
podman run ...with the fullpodman runcommand shown in the Quick Start section (step 5), including the image name and volume mounts.
# Warm migration tests
podman run ... uv run pytest -m warm -v --tc=source_provider:vsphere-8.0.1 ...
# Copy-offload tests (full suite)
podman run ... uv run pytest -m copyoffload -v --tc=source_provider:vsphere-8.0.1 ...
# Copy-offload sanity tests (quick validation)
podman run ... uv run pytest -m copyoffload_sanity -v --tc=source_provider:vsphere-8.0.1 ...
# Combine markers
podman run ... uv run pytest -m "tier0 or warm" -v --tc=source_provider:vsphere-8.0.1 ...For long-running test suites or automated CI/CD pipelines, you can run tests as OpenShift Jobs:
Store your .providers.json file and optionally cluster credentials as an OpenShift secret:
Option A: Providers configuration only (credentials passed as command args in Job):
oc create namespace mtv-tests
oc create secret generic mtv-test-config \
--from-file=providers.json=.providers.json \
-n mtv-testsOption B: Include cluster credentials in Secret (recommended - avoids exposing secrets in Job YAML):
oc create namespace mtv-tests
oc create secret generic mtv-test-config \
--from-file=providers.json=.providers.json \
--from-literal=cluster_host=https://api.your-cluster.com:6443 \
--from-literal=cluster_username=kubeadmin \
--from-literal=cluster_password=your-cluster-password \
-n mtv-testsReplace the cluster values with your actual OpenShift API endpoint and credentials. This approach keeps sensitive
data out of the Job definition and prevents credential exposure in oc get job -o yaml output.
Use this template to run tests. Customize the placeholders:
[JOB_NAME]- Unique job name (e.g.,mtv-tier0-tests,mtv-warm-tests,mtv-copyoffload-tests)[TEST_MARKERS]- Pytest marker(s) (e.g.,tier0,warm,copyoffload)[TEST_FILTER]- Optional: specific test name for-kflag (omit lines for all tests)
Note: This Job template is also used in
guides/copyoffload/how-to-run-copyoffload-tests.md. If updating, ensure both files remain in sync for the following fields:apiVersion,kind,metadata.name,spec.template, containerimage,command, andvolumeMounts/volumesconfiguration.
Template:
cat <<EOF | oc apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: [JOB_NAME]
namespace: mtv-tests
spec:
template:
spec:
restartPolicy: Never
containers:
- name: tests
image: ghcr.io/redhatqe/mtv-api-tests:latest # Or use your custom image
env:
- name: CLUSTER_HOST
valueFrom:
secretKeyRef:
name: mtv-test-config
key: cluster_host
optional: true
- name: CLUSTER_USERNAME
valueFrom:
secretKeyRef:
name: mtv-test-config
key: cluster_username
optional: true
- name: CLUSTER_PASSWORD
valueFrom:
secretKeyRef:
name: mtv-test-config
key: cluster_password
optional: true
command:
- /bin/bash
- -c
- |
uv run pytest -m [TEST_MARKERS] \
-v \
${CLUSTER_HOST:+--tc=cluster_host:${CLUSTER_HOST}} \
${CLUSTER_USERNAME:+--tc=cluster_username:${CLUSTER_USERNAME}} \
${CLUSTER_PASSWORD:+--tc=cluster_password:${CLUSTER_PASSWORD}} \
--tc=source_provider:[SOURCE_PROVIDER] \
--tc=storage_class:[STORAGE_CLASS]
# Replace [SOURCE_PROVIDER] with the key from your .providers.json (e.g., vsphere-8.0.3.00400)
# Replace [STORAGE_CLASS] with your OpenShift storage class name
# Optional: To run a specific test, add: -k [TEST_FILTER]
volumeMounts:
- name: config
mountPath: /app/.providers.json
subPath: providers.json
volumes:
- name: config
secret:
secretName: mtv-test-config
EOFReplace placeholders:
[JOB_NAME]→mtv-tier0-tests[TEST_MARKERS]→tier0[SOURCE_PROVIDER]→vsphere-8.0.3.00400(key from your.providers.json)[STORAGE_CLASS]→rhosqe-ontap-san-block(your OpenShift storage class)- Remove the commented
-kand[TEST_FILTER]lines
Replace cluster configuration:
ghcr.io/redhatqe/mtv-api-tests:latest- Use this public image, or substitute with your custom image from Quick Start Step 1 (e.g.,<YOUR-REGISTRY>/mtv-tests:latest)- If you used Option A (credentials in Job): Replace the command with explicit
--tcflags as shown below - If you used Option B (credentials in Secret): The Job template above automatically reads from the Secret
vsphere-8.0.3.00400- Your source provider key (must match key in.providers.json)your-storage-class- Your OpenShift storage class name
For Option A (credentials in Job YAML) - NOT RECOMMENDED:
🚨 Security Warning: This approach is strongly discouraged because:
- Credentials are visible in plaintext via
oc get job -o yaml - Passwords appear in CI logs, cluster audit logs, and etcd backups
- Anyone with read access to the Job resource can see passwords
- Use Option B with Kubernetes Secrets instead (recommended above)
If you must use Option A (only for isolated test environments), replace the command section with:
command:
- uv
- run
- pytest
- -m
- [TEST_MARKERS]
- -v
- --tc=cluster_host:https://api.your-cluster.com:6443
- --tc=cluster_username:kubeadmin
- --tc=cluster_password:your-cluster-password # pragma: allowlist secret
- --tc=source_provider:vsphere-8.0.3.00400
- --tc=storage_class:your-storage-classFollow test logs in real-time:
# Replace [JOB_NAME] with your actual job name (e.g., mtv-tier0-tests)
oc logs -n mtv-tests job/[JOB_NAME] -fCheck Job status:
oc get jobs -n mtv-tests
# Look for "COMPLETIONS" showing 1/1 = success, 0/1 = still runningRetrieve test results:
# Copy JUnit XML report from completed pod (replace [JOB_NAME] with your actual job name)
# Note: pytest writes to /app/junit-report.xml (WORKDIR /app, pytest.ini: --junit-xml=junit-report.xml)
POD_NAME=$(oc get pods -n mtv-tests -l job-name=[JOB_NAME] -o jsonpath='{.items[0].metadata.name}')
oc cp mtv-tests/$POD_NAME:/app/junit-report.xml ./junit-report.xmlClean up after tests:
# Replace [JOB_NAME] with your actual job name
oc delete job [JOB_NAME] -n mtv-testsThe mtv-api-tests CLI tool automates configuration and test execution. It discovers
vSphere and OpenShift resources via their APIs, generates .providers.json and a self-contained
mtv-api-tests-manifests.yaml (Namespace + Secret + Job), and can run tests locally or as an OpenShift Job.
uv run mtv-api-tests generateThe wizard will:
- Prompt for vSphere credentials (or read from
VSPHERE_HOST,VSPHERE_USERNAME,VSPHERE_PASSWORD) - Connect to vSphere and let you select datastores, a test VM, and an ESXi host
- Prompt for storage vendor and credentials
- Connect to OpenShift and let you select a storage class
- Ask which test category to run (
all,copyoffload,copyoffload_sanity,tier0,tier1,warm,remote) - Write
.providers.jsonandmtv-api-tests-manifests.yaml
Security Note: The generated
mtv-api-tests-manifests.yamlcontains embedded credentials (cluster username/password and provider secrets encoded in base64). Do not commit this file to version control or share it. The file is created with0600permissions and is listed in.dockerignore.
# Run locally
uv run mtv-api-tests run --mode local
# Run as OpenShift Job (uses the generated mtv-api-tests-manifests.yaml)
uv run mtv-api-tests run --mode jobWhen --category or --source-provider are omitted, the tool prompts interactively.
All interactive prompts can be bypassed with CLI flags, making the tool suitable for CI pipelines and repeatable lab scripts:
# Generate configuration with a specific category and custom image
uv run mtv-api-tests generate --category copyoffload --image quay.io/myorg/mtv-api-tests:v2.8.3
# Run locally with all options pre-set (no prompts)
uv run mtv-api-tests run --mode local \
--category tier0 \
--source-provider vsphere-8.0.3.00400 \
--storage-class ontap-san-block \
-k "thin or thick"
# Run as OpenShift Job with a custom manifest path
uv run mtv-api-tests run --mode job --job-yaml /path/to/mtv-api-tests-manifests.yamlUse uv run mtv-api-tests generate --help or uv run mtv-api-tests run --help to see all available flags.
uv run mtv-api-tests coverage <keywords> answers "is scenario X covered by a test class?"
by searching test-class plan docstrings (see test_plan_schema.md). It parses the docstrings
on demand with AST — no test modules are imported and no index is stored — so the docstrings
are the single source of truth.
When a scenario or regression comes in, the workflow is: search what covers it → copy the pytest node ID from the output → check the CI status of that test instead of running it manually.
# Search keywords (case-insensitive, partial match; quoting and multiple
# arguments are equivalent - all words are ANDed)
uv run mtv-api-tests coverage "static IP"
# Multiple keywords are ANDed
uv run mtv-api-tests coverage warm resume
# List classes whose docstring is missing or does not follow the schema yet
uv run mtv-api-tests coverage --missing-plans
# JSON output for scripting
uv run mtv-api-tests coverage --json "shared disk"Each match shows the class name, one-sentence scenario summary, pytest markers, file:line,
the pytest node ID that runs exactly that class, and the matching docstring line. Keywords match across
the whole docstring, the class name, and the markers (shared disk matches the shared_disk
marker); matches in classes whose docstring does not follow the schema yet are still listed,
marked with a note. No matches exits 0 with a "not covered" message, so scripts can chain
on output rather than on exit codes.
Instead of pinning --tc=source_provider:<key>, a run can identify a candidate pool by type —
version optional — and let the suite pick the fastest member from the cluster's point of view:
--tc=source_provider_type:vsphere --tc=source_provider_version:8.0.1 # exact version
--tc=source_provider_type:vsphere # every version of the typeA feature key (copyoffload) flips pool membership: copy-offload-capable providers — entries
with a copyoffload section — are excluded from the pool by default and included only when
the key is passed:
--tc=source_provider_type:vsphere --tc=source_provider_version:8.0.1 --tc=source_provider_feature:copyoffloadSelection happens once per run, in the session-scoped selected_source_provider fixture
(requested first by the autouse fixture, before anything consumes the provider). A pool with a single
member — or whose members all share one probe endpoint (e.g. two OVA entries on the same NFS server)
— is selected instantly without probing; no probe pod is created. Otherwise, in a single-process
run the pool is probed inline; under xdist the first worker to hold a run-scoped file lock probes and
writes the selection while every other worker reads the same file, so all workers run against the
same provider. Probing measures every unique endpoint once (providers sharing a host:port
share the measurement); each endpoint's host comes from fqdn, else from api_url (URL or NFS
export path), else from the OpenShift host field, and the port from api_url when it carries one,
else the type's API port (OVA NFS 2049, Hyper-V 5986, otherwise 443). The chosen provider and its
measured RTT are logged.
Rules:
- Explicit
--tc=source_provider:<key>always wins: nothing is probed and nothing changes. - The pool form requires the type key; version is optional and
source_provider_versionwithoutsource_provider_typeaborts the session withpytest.exit(...). - Neither an explicit provider nor a pool aborts with the missing-config error, exactly as before.
- Candidates are the
.providers.jsonentries matching the requested type (and version, when passed), gated by the feature key (copyoffloadproviders only with the key, excluded without it); an empty pool aborts the session with a clear error — never a skip. - A single-candidate or single-endpoint pool is selected without probing; otherwise the winner is the lowest-RTT endpoint with the lowest provider key as the deterministic tiebreak.
- OpenShift pools are never probed:
create_source_providerrewrites OpenShift sources to the local cluster host, so their configured endpoints are unused and the lowest provider key wins deterministically. - Unreachable candidates are excluded; a fully unreachable pool aborts the session.
- In pool mode the provider is unknown at collection time, so collection-time provider gating uses the
pool's
source_provider_typeand test item names carry the type instead of the provider key.
Note: for copy-offload (XCOPY) plans the bulk disk copy executes array-side (storage array / ESXi hosts, including
dedicated_migration_hosts) and does not traverse the cluster-to-provider path. The probe measures the vCenter API path, which copy-offload runs still use for inventory and controller operations — selection stays valid for copy-offload pools, but the runtime win applies to cold/warm (NBD) migrations only.
Warning: pool selection assumes every member of the pool is a true clone — same VM inventory (names, guest agent, disk layouts) and same capabilities. The feature key enforces copy-offload capability; VM inventory equivalence is the operator's responsibility because it is not machine-checkable. Test configs pin VM names, so a pool of non-interchangeable providers breaks tests.
Copy-offload is an MTV feature that uses the storage backend to directly copy VM disks from vSphere datastores to OpenShift PVCs using XCOPY operations, bypassing the traditional v2v transfer path. This requires shared storage infrastructure between vSphere and OpenShift, VAAI (vSphere APIs for Array Integration) enabled on ESXi hosts, and a configured StorageMap with offload plugin settings.
For detailed instructions on running copy-offload tests, including prerequisites, configuration, and troubleshooting, see:
Note:
TestCopyoffloadPopulatorThrottlingMigration(MTV-696) andTestCopyoffloadDedicatedMigrationHostset both the ForkliftControllercontroller_max_populator_inflightand populator deploymentMAX_POPULATOR_INFLIGHTto 2.TestCopyoffloadDedicatedHostSharedPopulatorLimit(MTV-4494) sets both to 1 and sets the VM in-flight limit to 2. The fixtures restore the prior values afterward. Do not run them alongside other MTV migrations or copy-offload tests on the same cluster. The original dedicated-host test pins both source VMs away from configured dedicated hosts and retains multi-host coverage. The shared-limit scenario requires exactly one configured dedicated ESXi host and at least one other ESXi host; it places one source VM on each and checks that their active populate pods stay within the shared runtime-host populator limit. The assertion checks the peak observed Pending/Running-pod count during migration; it does not require a specific count ofPopulatorThrottledevents. It requires one migration poll to observe a Pending PVC with a structuredPopulatorThrottledevent, alongside a Running worker from the other source host on the sole configured dedicated host. The Event's involved PVC is matched by name and UID; the PVC'ssourceHostlabel identifies its source, and the worker's runtime metadata identifies the dedicated host. The Event's latest timestamp must be at or after that worker started. The poll reuses its populate-Pod snapshot for concurrency counting and only lists PVCs when throttling Events exist. Runtime-host grouping prefersthrottleHostwhen present and otherwise uses the worker's--migration-hostargument. Current Forklift builds may reuse the populate Pod'ssourceHostlabel for the runtime host.
For array-specific Secret keys not covered by built-in vendor fields, use storage_secret_extra in the
copyoffload section of .providers.json (or COPYOFFLOAD_STORAGE_SECRET_EXTRA); see the guide.
For technical implementation details, see the vsphere-xcopy-volume-populator documentation.
This test suite verifies MTV's ability to migrate LUKS-encrypted VMs. It validates both successful cold migration when the correct decryption passphrase is provided, and expected failure at the ImageConversion phase when an incorrect LUKS decryption passphrase is used.
Add the luks_passphrase field to your .providers.json provider configuration:
{
"vsphere-8.0.1": {
"type": "vsphere",
"luks_passphrase": "your-luks-passphrase"
}
}The passphrase can be configured at two levels:
- Provider-level (in
.providers.json): Used as the default for all LUKS VMs - Per-VM override (in test config): Set
luks_passphraseon individual VM entries to override the provider default
# Run LUKS encryption migration tests
podman run ... uv run pytest -m tier1 -k luks -v --tc=source_provider:vsphere-8.0.1 ...See .providers.json.example for the complete provider template including the luks_passphrase field.
The disk-exclusion test verifies MTV's ability to exclude individual source disks from a migration: the excluded disks are neither copied nor attached to the migrated VM, and no PVC is created for them in the dedicated VM namespace.
Set exclude_disks on a VM entry in the test configuration with the vSphere bus addresses
of the disks to exclude:
"virtual_machines": [
{
"name": "your-source-vm",
"exclude_disks": ["scsi0:1"], # vSphere bus addresses of disks to exclude
},
]The suite renames this key to the Plan spec.vms[].excludeDisks field before plan creation.
# Run the disk-exclusion migration test
podman run ... uv run pytest -k exclude_disks -v --tc=source_provider:vsphere-8.0.1 ...See test-plan configuration
for the full exclude_disks reference and
cold migrations for the
exclude-disks test pattern.
Control where and how migrated VMs are scheduled in the target OpenShift cluster.
Schedule VMs to specific labeled nodes.
"target_node_selector": {
"migration-workload": None, # Exactly one label; None = auto-generated key & value with session_uuid
}Apply custom labels to migrated VMs.
"target_labels": {
"app": "my-application",
"migrated-from": "vsphere",
"test-id": None, # Auto-generated
}Configure pod affinity/anti-affinity and node affinity rules.
"target_affinity": {
"podAffinity": {
"preferredDuringSchedulingIgnoredDuringExecution": [{
"podAffinityTerm": {
"labelSelector": {"matchLabels": {"app": "database"}},
"topologyKey": "kubernetes.io/hostname"
},
"weight": 100
}]
},
"nodeAffinity": {
"requiredDuringSchedulingIgnoredDuringExecution": {
"nodeSelectorTerms": [{
"matchExpressions": [{
"key": "node.kubernetes.io/instance-type",
"operator": "In",
"values": ["m5.2xlarge"]
}]
}]
}
}
}Version requirement: MTV 2.10.0+. Mark tests with @pytest.mark.min_mtv_version("2.10.0") and @pytest.mark.usefixtures("mtv_version_checker").
Auto-generation: Setting a value to None replaces it with the session UUID (e.g., "test-id": None becomes
"test-id": "mtv-api-tests-abc123"). This ensures uniqueness for parallel test execution and prevents conflicts.
Upgrade tests validate that migration resources created on a pre-upgrade MTV version survive the operator upgrade and that migrations execute successfully afterward. The test flow is:
- Create migration resources (StorageMap, NetworkMap, Plan) on the current MTV version
- Upgrade the MTV operator to the target version using an external upgrade script
- Verify the upgraded operator version and that existing Plan CRs remain in Ready state
- Execute the migration on the upgraded operator
- Validate the migrated VMs
Upgrade tests require additional --tc parameters beyond the standard cluster and provider options:
| Parameter | Required | Description |
|---|---|---|
upgrade_repo_url |
Yes | URL of the Git repository containing the upgrade script |
upgrade_repo_ref |
Yes | Git ref (branch or tag) to checkout |
upgrade_script_path |
Yes | Relative path to the upgrade script within the repository |
mtv_upgrade_to_version |
Yes | Target MTV version to upgrade to (e.g., 2.7.0) |
mtv_upgrade_to_source |
Yes | MTV source identifier (e.g., brew, released) |
mtv_upgrade_image_index |
No | Image index override (pass empty string "" to skip) |
The suite waits for the external upgrade script with an outer budget that exists only to catch a wedged
process — the script owns its own internal waits (the MCP rollout, its oc wait timeouts). Configure it
with either source, highest priority first:
| Source | Example |
|---|---|
| pytest option | --mtv-upgrade-timeout 7200 |
| environment variable | MTV_UPGRADE_TIMEOUT_SECONDS=7200 |
| default | 5400 seconds |
Keep the outer budget above whatever the script waits for internally: when it expires first the script is
SIGTERMed and never gets to report its own timeout. The resolved budget is logged at the start of the run,
and the script's stdout/stderr are persisted under the data-collector directory (redacted, with the last
200 lines of each also carried in the failure message). With --skip-data-collector no file is written.
uv run pytest -m upgrade -v \
--tc=cluster_host:https://api.your-cluster.com:6443 \
--tc=cluster_username:kubeadmin \
--tc=cluster_password:${CLUSTER_PASSWORD} \
--tc=source_provider:vsphere-8.0.1 \
--tc=storage_class:YOUR-STORAGE-CLASS \
--tc=upgrade_repo_url:https://github.com/org/mtv-autodeploy.git \
--tc=upgrade_repo_ref:main \
--tc=upgrade_script_path:scripts/upgrade.sh \
--tc=mtv_upgrade_to_version:2.7.0 \
--tc=mtv_upgrade_to_source:released \
--tc=mtv_upgrade_image_index:""Add these flags to your test runs for debugging:
For containerized runs (Podman/Docker) - use uv run pytest:
# Enable verbose output
uv run pytest -v # Verbose test names
# Enable debug logging
uv run pytest -s -vv # Very verbose with output capture disabled
# Set MTV/OpenShift debug level (add as environment variable)
podman run -e OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG ...
# Keep resources after test for inspection
uv run pytest --skip-teardown # Don't delete VMs, plans, etc. after tests
# Skip data collector (faster, but no resource tracking)
uv run pytest --skip-data-collector # Don't track created resources
# Change data collector output location
uv run pytest --data-collector-path /tmp/my-logs
# Run a specific test from a marker/suite
uv run pytest -k test_name # Run only tests matching pattern
uv run pytest -m copyoffload -k test_copyoffload_thin_migration # Run only thin testFor local developer mode (after running uv sync) - use uv run pytest or just pytest:
# Same flags work in local mode
pytest -v # Verbose test names
pytest -s -vv # Very verbose with output capture disabled
pytest --skip-teardown # Don't delete VMs, plans, etc. after tests
pytest -k test_name # Run only tests matching patternExample - Run tier0 with debug mode and keep resources:
podman run --rm \
-v $(pwd)/.providers.json:/app/.providers.json:ro \
-e OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG \
-e CLUSTER_PASSWORD \
ghcr.io/redhatqe/mtv-api-tests:latest \
uv run pytest -s -vv -m tier0 --skip-teardown \
--tc=cluster_host:https://api.your-cluster.com:6443 \
--tc=cluster_username:kubeadmin \
--tc=cluster_password:${CLUSTER_PASSWORD} \
--tc=source_provider:vsphere-8.0.1 \
--tc=storage_class:YOUR-STORAGE-CLASSSecurity Note: Use environment variables to avoid shell-history exposure. Note the expanded value can still appear in process listings inside the container; prefer OpenShift Secrets or other secret injection where possible. Set
export CLUSTER_PASSWORD='your-password'before running. # pragma: allowlist secret
When to use these flags:
--skip-teardown- Test failed and you want to inspect the created VMs/plans--skip-data-collector- Running many quick tests and don't need resource tracking-s -vv- Test is failing and you need detailed output to diagnoseOPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG- Need to see all API calls to OpenShift-k- Run only specific tests by name pattern (useful for debugging or running individual tests)
Note: All examples in this section show the full command. When using containers, always prefix with
podman run ... ghcr.io/redhatqe/mtv-api-tests:latestbefore theuv run pytestcommand.
The -k flag allows you to run specific tests by matching their names:
# Run only the thin migration test from copyoffload
podman run ... uv run pytest -k test_copyoffload_thin_migration -v \
--tc=source_provider:vsphere-8.0.1 --tc=storage_class:ontap-san-block
# Run multiple tests with pattern matching
podman run ... uv run pytest -k "test_copyoffload_multi_disk" -v ... # Matches both multi-disk tests
podman run ... uv run pytest -k "thin or thick" -v ... # Matches thin and thick testsList all available test names:
# In container
podman run --rm ghcr.io/redhatqe/mtv-api-tests:latest uv run pytest --collect-only -q
# In local developer mode
uv run pytest --collect-only -qTests automatically generate a JUnit XML report (junit-report.xml) containing:
- Test results (passed/failed/skipped)
- Execution times
- Error messages and stack traces
- Test metadata
Accessing the report:
From local Podman/Docker run:
# Default path: /app/junit-report.xml (WORKDIR /app, pytest.ini: --junit-xml=junit-report.xml)
# Override with --junit-xml to write to a mounted volume for persistence:
# Note: source_provider must match a key from your .providers.json (e.g., vsphere-8.0.1)
podman run --rm \
-v $(pwd)/.providers.json:/app/.providers.json:ro \
-v $(pwd)/results:/app/results \
-e CLUSTER_PASSWORD \
ghcr.io/redhatqe/mtv-api-tests:latest \
uv run pytest -m tier0 -v \
--junit-xml=/app/results/junit-report.xml \
--tc=cluster_host:https://api.your-cluster.com:6443 \
--tc=cluster_username:kubeadmin \
--tc=cluster_password:${CLUSTER_PASSWORD} \
--tc=source_provider:vsphere-8.0.1 \
--tc=storage_class:YOUR-STORAGE-CLASS
# Report will be saved to ./results/junit-report.xmlNon-root Container: The container runs as UID 1001, so the host
results/directory must be writable. With podman rootless this usually works automatically (user namespace mapping). For podman/docker rootful, runmkdir -p results && chmod 777 resultsor usepodman run --userns=keep-id. On OpenShift, the Dockerfile's group-permission pattern (chgrp -R 0 && chmod -R g=u) handles this automatically.Security Note: Use environment variables to avoid shell-history exposure. Note the expanded value can still appear in process listings inside the container; prefer OpenShift Secrets or other secret injection where possible. Set
export CLUSTER_PASSWORD='your-password'before running. # pragma: allowlist secret
From OpenShift Job:
# Copy report from completed pod (default path: /app/junit-report.xml from pytest.ini)
# Replace [JOB_NAME] with your job name, e.g., mtv-tier0-tests
POD_NAME=$(oc get pods -n mtv-tests -l job-name=[JOB_NAME] -o jsonpath='{.items[0].metadata.name}')
oc cp mtv-tests/$POD_NAME:/app/junit-report.xml ./junit-report.xmlView report in CI/CD tools: Most CI/CD platforms (Jenkins, GitLab CI, GitHub Actions) can parse JUnit XML for test result dashboards.
The --analyze-with-ai flag enriches JUnit XML reports with AI-powered failure analysis. After tests complete,
failed test cases are sent to a rootcoz server,
which classifies failures and suggests fixes. The analysis results are injected back into the JUnit XML as
structured properties (classification, details, suggested code fixes, bug reports) and human-readable summaries.
Prerequisites:
- A running rootcoz server
ROOTCOZ_SERVER_URLenvironment variable set to the server URL
Environment variables (pytest client):
| Variable | Required | Default | Description |
|---|---|---|---|
ROOTCOZ_SERVER_URL |
Yes | - | rootcoz server URL |
ROOTCOZ_AI_PROVIDER |
No | from .rootcoz/settings.json via rootcoz |
Optional override for AI provider |
ROOTCOZ_AI_MODEL |
No | from .rootcoz/settings.json via rootcoz |
Optional override for AI model |
ROOTCOZ_TIMEOUT |
No | 600 |
Request timeout in seconds |
Environment variables can be set via a .env file in the project root (loaded automatically when the flag is used).
Pytest does not read .rootcoz/settings.json. When ROOTCOZ_AI_PROVIDER /
ROOTCOZ_AI_MODEL are unset, the request omits them and
rootcoz applies defaults from that file.
Repo settings for rootcoz (.rootcoz/settings.json):
This file is for the rootcoz server/CLI when it analyzes this repository
(provider/model defaults, peer configs, timeouts, additional_repos, and related options).
rootcoz reads and applies it; pytest --analyze-with-ai only passes optional ROOTCOZ_* overrides when set.
Usage:
uv run pytest -m tier0 -v --analyze-with-ai \
--tc=source_provider:vsphere-8.0.1 \
--tc=storage_class:YOUR-STORAGE-CLASSNotes:
- Requires
--junit-xml(enabled by default inpytest.ini) - Skipped automatically during
--collectonlyand--setupplan - If
ROOTCOZ_SERVER_URLis not set, the feature is disabled with a warning - If
ROOTCOZ_AI_PROVIDER/ROOTCOZ_AI_MODELare unset, rootcoz uses.rootcoz/settings.json - Original JUnit XML is preserved if enrichment fails
The preserve_static_ips feature relies on Hyper-V KVP DHCPEnabled to distinguish static from DHCP-assigned IPs.
This field is unreliable on RHEL 9 guests using NetworkManager keyfiles (the default configuration).
The Linux hv_kvp_daemon determines DHCP status by checking /etc/sysconfig/network-scripts/ifcfg-* files —
on RHEL 9+ (which uses keyfiles in /etc/NetworkManager/system-connections/), no ifcfg file is found,
causing all NICs to report DHCPEnabled = False regardless of actual DHCP state.
Impact: DHCP-assigned IPs are incorrectly treated as static, causing preserve_static_ips post-migration verification to fail.
Workarounds:
- Use RHEL 10 guests (patched
hv_kvp_daemonqueries NetworkManager directly) - Use RHEL 9 guests with ifcfg-style network configuration
- Ensure Hyper-V test VMs use only static IPs on NICs being verified
Make sure you're using uv run pytest (not just pytest):
# ✅ Correct
podman run ... uv run pytest -m tier0 ...
# ❌ Wrong
podman run ... pytest -m tier0 ...oc whoami
oc auth can-i create virtualmachines# Test connectivity from cluster
oc run test-curl --rm -it --image=curlimages/curl -- curl -k https://vcenter.example.com
# List available provider keys
cat .providers.json | jq 'keys'
# Verify credentials for a specific provider (replace with your actual key)
cat .providers.json | jq '."vsphere-8.0.1"'oc get storageclass # Use actual storage class name# Check MTV operator logs
oc logs -n openshift-mtv deployment/forklift-controller -f
# Check plan status
oc get plans -A
oc describe plan <plan-name> -n openshift-mtvAutomatic failure collection attempts at most one successful must-gather per class-plan instance per worker,
using a percent-encoded class node ID, class parameter indices and worker ID under .data-collector/.
It binds the current Plan or explicit None at the class-plan boundary before default fixture finalization,
including early exits, and retains that context for retries. Standalone tests gather immediately.
Known operational class-plan collection failures remain pending for the session-finish retry; artifacts are not guaranteed if it fails.
Standalone tests gather immediately on the first failing phase; an operational failure is retried only when another
phase of the same test also fails. Collector exceptions during setup or call are stashed on the item, cleared at teardown
and not retried there. Standalone failures are not queued for session-finish retry.
Unexpected standalone setup/call collector errors are retained on the item until pytest fixture finalization.
The session-finish hook runs the pending-gather flush, resource inventory writing and configured cleanup before the inner
session hooks yield, so xdist worker shutdown cannot interrupt them; the summary and enabled AI enrichment run after those
hooks, and errors are re-raised at the end with earlier errors chained.
Invalid image metadata and other configuration/programming errors propagate instead of returning False. A pending class-plan gather
remains queued and may raise again when retried at a later boundary or session finish.
Inventory, configured teardown, temporary-directory removal, summary and enabled AI enrichment are attempted independently;
cleanup errors propagate with earlier errors chained. See
diagnostic collection for timing and teardown limitations.
oc adm must-gather --image=quay.io/kubev2v/forklift-must-gather:latest --dest-dir=/tmp/mtv-logsIf tests fail or you used --skip-teardown, clean up manually.
About the data collector: The test suite includes a data collector feature that tracks all created
resources in .data-collector/resources.json. This file is created automatically during test runs unless
you pass --skip-data-collector. When available, use tools/clean_cluster.py with the resources.json
file for automated cleanup. If the file doesn't exist (e.g., you used --skip-data-collector), fall back
to manual oc delete commands.
# Using resource tracker (if data collector was enabled)
uv run tools/clean_cluster.py .data-collector/resources.json
# Or manually delete resources
oc delete vm --all -n <test-namespace>
oc delete plan --all -n openshift-mtv
oc delete provider <provider-name> -n openshift-mtvQ: Do I need Python/pytest/uv on my machine? A: Not for container-based runs — you only need Podman or Docker. For local runs, you need uv (which manages Python automatically). See Running Locally Without Container.
Q: How long do tests take? A: Test duration varies. tier0 tests are fastest (smoke tests), warm migration tests include warm migration scenarios, and copy-offload tests are optimized for speed with shared storage.
Q: Can I run on SNO (Single Node OpenShift)? A: SNO has been validated with copy-offload tests. Other test types may work but have not been specifically validated on SNO.
Q: Do tests generate reports?
A: Yes. Tests automatically generate a JUnit XML report (junit-report.xml) with test results, execution times,
and error details. See the "Test Results and Reports" section for how to access it.
Q: How do I debug a failing test?
A: Use --skip-teardown to keep resources after test, and -s -vv for verbose output.
Set OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG for API call logs. See the "Useful Test Options" section for details.
For test developers who want to run tests directly on their machine (requires manual setup).
System packages:
Note: uv automatically downloads and manages Python versions—no system Python installation needed. However, the packages below are system-level compilation dependencies required by Python extensions used by the test suite
# RHEL/Fedora
sudo dnf install gcc clang libxml2-devel libcurl-devel openssl-devel
# Ubuntu/Debian
sudo apt install gcc clang libxml2-dev libcurl4-openssl-dev libssl-dev
# macOS
brew install gcc libxml2 curl opensslRequired tools:
- uv package manager (manages Python automatically)
- oc CLI
- virtctl (optional — the suite downloads a cluster-matched binary; a
virtctlinstalled onPATHis ignored so the version always matches the cluster)
Note: uv will automatically download and manage the appropriate Python version. If you encounter HTTPS failures with Python 3.13, the issue is urllib3 2.4.0+ (released April 2025) rejecting non-RFC-compliant certificates missing AKID. Fix: Regenerate certificates with AKID, pin
urllib3<2.4.0, or use Python 3.12 for legacy certs.
# 1. Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# 2. Clone repository and install dependencies
git clone https://github.com/RedHatQE/mtv-api-tests.git
cd mtv-api-tests
uv sync # uv will automatically handle Python version
# 3. Set cluster password (avoids shell history; still visible in process listings)
export CLUSTER_PASSWORD='your-cluster-password' # pragma: allowlist secret
# 4. Run tests
uv run pytest -v \
--tc=cluster_host:https://api.cluster.com:6443 \
--tc=cluster_username:kubeadmin \
--tc=cluster_password:${CLUSTER_PASSWORD} \
--tc=source_provider:vsphere-8.0.1 \
--tc=storage_class:standard-csi
# For debug options (--skip-teardown, -s -vv, etc.), see "Useful Test Options" section above