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:

  1. Create .providers.json in the repository root.
  2. Pass the environment-specific runtime values with --tc=....
  3. Start with --collect-only or --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: addopts does not include -n, so the default is a serial run. Add -n auto yourself when you want pytest-xdist parallelism; --dist=loadscope is 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-file yourself. The repo already points pytest at tests/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.example is not valid JSON as-is. It contains comments and placeholder values. Your real .providers.json must be valid JSON, because the loader uses json.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.json contains credentials. Keep it local, chmod 600 it, 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-rhel8 and start with the cold sanity plan. If your lab uses different VM names, update the matching entry in tests/tests_config/config.py before 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.json
  • storage_class: required for real execution; destination OpenShift storage class
  • cluster_host, cluster_username, cluster_password: useful when you want to pass OpenShift access explicitly on the command line
  • mtv_namespace: optional; defaults to openshift-mtv
  • target_namespace_prefix: optional; defaults to auto
  • insecure_verify_skip: optional; controls OpenShift API TLS verification
  • source_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, tier1
  • remote, warm
  • copyoffload, copyoffload_sanity, copyoffload_snapshots
  • shared_disk, deep_inspection, ca_crt, aap, upgrade
  • ova, vsphere, esxi, rhv, hyperv, openstack, openshift
  • incremental, min_mtv_version

For example, to list the smoke-suite tests:

uv run pytest --collect-only -q -m tier0

Tip: --collect-only is 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_storagemap
  • test_create_networkmap
  • test_create_plan
  • test_migrate_vms
  • test_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, and hyperv providers. 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.json is missing or empty. An empty file raises ProviderEmptyContentError.
  • The value passed in --tc=source_provider:... does not exactly match a top-level key in .providers.json.
  • .providers.json was copied from .providers.json.example without 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.json was not found.
  • You targeted a single method such as ::test_create_plan instead 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.xml and tears down created resources after the run. If you need to inspect what was created after a failure, rerun with --skip-teardown.