Quickstart First Run
mtv-api-tests uses pytest with pytest-testconfig. The shared test catalog is already wired into the repo, so a first run is mostly about three things:
- Create
.providers.jsonin the repository root. - Pass the environment-specific runtime values with
--tc=.... - Start with
--collect-onlyor--setup-plan, then run one small sanity class.
This page assumes you are running from the repository root. If dependencies are not installed yet, do that first:
uv sync --locked
There is also a supported shortcut that does steps 1 and 2 for you:
uv run mtv-api-tests generate
uv run mtv-api-tests run --mode local
The rest of this page covers the manual path, because it is the one you need when you are debugging a specific test.
How Configuration Works
pytest.ini already tells pytest to load tests/tests_config/config.py through pytest-testconfig:
[pytest]
testpaths = tests
addopts =
-s
-o log_cli=true
-p no:logging
--tc-file=tests/tests_config/config.py
--tc-format=python
--junit-xml=junit-report.xml
--show-progress
--strict-markers
--dist=loadscope
Note:
addoptsdoes not include-n, so the default is a serial run. Add-n autoyourself when you wantpytest-xdistparallelism;--dist=loadscopeis already set for that case.
For real test execution, the suite requires two runtime keys:
def pytest_sessionstart(session):
required_config = ("storage_class", "source_provider")
Note: You normally do not need to pass
--tc-fileyourself. The repo already points pytest attests/tests_config/config.py, so the usual workflow is to add runtime overrides with--tc=key:value.
Create .providers.json
The provider loader resolves the path from --providers-json, then PROVIDERS_JSON_PATH, then .providers.json in the current working directory, and parses it as JSON:
def load_source_providers(providers_json_path: str | None = None) -> dict[str, dict[str, Any]]:
resolved_path = resolve_providers_json_path(cli_path=providers_json_path)
with open(resolved_path) as fd:
content = fd.read()
if not content.strip():
raise ProviderEmptyContentError(path=resolved_path)
providers = json.loads(content)
if not isinstance(providers, dict):
raise ValueError(
f"Providers JSON must be a mapping of provider names to configurations, "
f"got {type(providers).__name__}: '{resolved_path}'"
)
return providers
Create .providers.json in the repository root. Use .providers.json.example as the starting template. This is the vSphere block from that file:
"vsphere": {
"type": "vsphere",
"version": "<SERVER VERSION>",
"fqdn": "SERVER FQDN/IP",
"api_url": "<SERVER FQDN/IP>/sdk",
"username": "USERNAME",
"password": "PASSWORD", # pragma: allowlist secret
"guest_vm_linux_user": "LINUX VMS USERNAME",
"guest_vm_linux_password": "LINUX VMS PASSWORD", # pragma: allowlist secret
"guest_vm_win_user": "WINDOWS VMS USERNAME",
"guest_vm_win_password": "WINDOWS VMS PASSWORD", # pragma: allowlist secret
"luks_passphrase": "LUKS DISK ENCRYPTION PASSPHRASE", # pragma: allowlist secret
"vddk_init_image": "<PATH TO VDDK INIT IMAGE>",
"endpoint_type": "vcenter"
}
The same example file also includes templates for ovirt, openstack, openshift, ova, hyperv, and a vSphere direct-ESXi entry.
Warning:
.providers.json.exampleis not valid JSON as-is. It contains comments and placeholder values. Your real.providers.jsonmust be valid JSON, because the loader usesjson.loads(...).Note: The top-level provider key is what you pass later as
--tc=source_provider:<key>. If your file uses"vsphere", then your runtime flag must use--tc=source_provider:vsphere.Warning:
.providers.jsoncontains credentials. Keep it local,chmod 600it, and treat it as sensitive.
Know What the Built-In Plans Expect
The shared defaults and named test plans live in tests/tests_config/config.py. These are the defaults most relevant to a first run:
insecure_verify_skip: str = "true"
source_provider_insecure_skip_verify: str = "false"
target_namespace_prefix: str = "auto"
mtv_namespace: str = "openshift-mtv"
remote_ocp_cluster: str = ""
plan_wait_timeout: int = 3600
For a first targeted migration, the smallest built-in cold plan is:
"test_sanity_cold_mtv_migration": {
"virtual_machines": [
{"name": "mtv-tests-rhel8", "guest_agent": True, "add_nic": True, "add_nic_start_connected": False},
],
"warm_migration": False,
"per_nic_network_map": True,
},
Note that this plan adds a second NIC. It therefore also exercises the Multus network mapping path, so multi-NIC setup is a prerequisite even for the sanity cold plan.
The warm equivalent is:
"test_sanity_warm_mtv_migration": {
"virtual_machines": [
{
"name": "mtv-tests-rhel8",
"source_vm_power": "on",
"guest_agent": True,
},
],
"warm_migration": True,
"preserve_static_ips": True,
},
Tip: If you want the smoothest first run, prepare a source VM named
mtv-tests-rhel8and start with the cold sanity plan. If your lab uses different VM names, update the matching entry intests/tests_config/config.pybefore your first real run.
Pass Runtime Settings With pytest-testconfig
For a first run, these are the settings you will usually care about:
source_provider: required for real execution; must match a top-level key in.providers.jsonstorage_class: required for real execution; destination OpenShift storage classcluster_host,cluster_username,cluster_password: useful when you want to pass OpenShift access explicitly on the command linemtv_namespace: optional; defaults toopenshift-mtvtarget_namespace_prefix: optional; defaults toautoinsecure_verify_skip: optional; controls OpenShift API TLS verificationsource_provider_insecure_skip_verify: optional; controls source-provider TLS verification
A typical first-run command shape is:
uv run pytest -v tests/cold/test_mtv_cold_migration.py::TestSanityColdMtvMigration \
--tc=source_provider:vsphere \
--tc=storage_class:<storage-class> \
--tc=cluster_host:https://api.<cluster>:6443 \
--tc=cluster_username:<username> \
--tc=cluster_password:${CLUSTER_PASSWORD}
If your provider key is not vsphere, replace it with the top-level key you actually used in .providers.json.
If you keep .providers.json outside the repository root, pass its location explicitly:
uv run pytest -v --providers-json /path/to/.providers.json ...
If you need to relax source-provider TLS verification in a lab environment, add:
--tc=source_provider_insecure_skip_verify:true
If your MTV operator is not installed in openshift-mtv, add:
--tc=mtv_namespace:<your-mtv-namespace>
Start With --collect-only Or --setup-plan
This repo treats both --collect-only and --setup-plan as normal dry-run entry points.
In fact, tox.toml uses both as a basic pytest check, and the container image in Dockerfile defaults to uv run pytest --collect-only.
Use --collect-only when you want to confirm what pytest will select:
uv run pytest --collect-only -q tests/cold/test_mtv_cold_migration.py::TestSanityColdMtvMigration
Use --setup-plan when you want to see fixture/setup planning for the same target before a real run:
uv run pytest --setup-plan tests/cold/test_mtv_cold_migration.py::TestSanityColdMtvMigration \
--tc=source_provider:vsphere \
--tc=storage_class:<storage-class>
If you want to browse by marker first, the full list registered in pytest.ini is:
tier0,tier1remote,warmcopyoffload,copyoffload_sanity,copyoffload_snapshotsshared_disk,deep_inspection,ca_crt,aap,upgradeova,vsphere,esxi,rhv,hyperv,openstack,openshiftincremental,min_mtv_version
For example, to list the smoke-suite tests:
uv run pytest --collect-only -q -m tier0
Tip:
--collect-onlyis the safest place to start when you want to confirm test names, markers, and node IDs before wiring in all runtime values. Note that the data collector appends<source_provider>-<storage_class>to every test name, so node IDs differ from the plain function names.
Run The First Targeted Test Class
The cold sanity test is defined in tests/cold/test_mtv_cold_migration.py like this:
@pytest.mark.vsphere
@pytest.mark.rhv
@pytest.mark.openstack
@pytest.mark.openshift
@pytest.mark.esxi
@pytest.mark.hyperv
@pytest.mark.tier0
@pytest.mark.incremental
@pytest.mark.parametrize(
"class_plan_config",
[
pytest.param(
py_config["tests_params"]["test_sanity_cold_mtv_migration"],
)
],
indirect=True,
ids=["rhel8"],
)
@pytest.mark.usefixtures("cleanup_migrated_vms")
class TestSanityColdMtvMigration:
"""Cold migration test - sanity check."""
That class runs the migration as five dependent steps:
test_create_storagemaptest_create_networkmaptest_create_plantest_migrate_vmstest_check_vms
The incremental marker is what chains them: if any earlier method fails, every later method reports as xfailed rather than failing with a confusing follow-on error.
Run the whole class, not a single method:
uv run pytest -v tests/cold/test_mtv_cold_migration.py::TestSanityColdMtvMigration \
--tc=source_provider:vsphere \
--tc=storage_class:<storage-class> \
--tc=cluster_host:https://api.<cluster>:6443 \
--tc=cluster_username:<username> \
--tc=cluster_password:${CLUSTER_PASSWORD}
If you specifically want to try the warm sanity path afterward, use:
uv run pytest -v tests/warm/test_mtv_warm_migration.py::TestSanityWarmMtvMigration \
--tc=source_provider:vsphere \
--tc=storage_class:<storage-class> \
--tc=cluster_host:https://api.<cluster>:6443 \
--tc=cluster_username:<username> \
--tc=cluster_password:${CLUSTER_PASSWORD}
Warning: Warm tests are skipped at collection time for
openstack,openshift,ova, andhypervproviders. For a first run, the cold sanity class is the safer starting point.
Common First-Run Problems
If the run fails early, check these first:
.providers.jsonis missing or empty. An empty file raisesProviderEmptyContentError.- The value passed in
--tc=source_provider:...does not exactly match a top-level key in.providers.json. .providers.jsonwas copied from.providers.json.examplewithout removing comments.- The built-in sanity plan expects a source VM named
mtv-tests-rhel8, but that VM does not exist in your provider. - You started pytest from a directory other than the repository root, so
.providers.jsonwas not found. - You targeted a single method such as
::test_create_planinstead of the full class. - You chose a warm test on a provider type that the suite skips for warm migration.
Tip: By default, the suite writes
junit-report.xmland tears down created resources after the run. If you need to inspect what was created after a failure, rerun with--skip-teardown.