Runtime Configuration
mtv-api-tests uses pytest-testconfig for suite settings and plain pytest flags for per-run behavior. In practice, you keep shared defaults in tests/tests_config/config.py,
keep provider definitions in .providers.json, and pass environment-specific values such as source_provider and storage_class with --tc=key:value.
Most users only need to remember three things:
pytest.inialready loads the default runtime config file for you.- A normal test run requires
source_providerandstorage_class. - The repo adds custom pytest flags for cleanup, artifact collection, logging, provider-file selection, and AI analysis.
How Configuration Is Loaded
The baseline pytest behavior is defined in pytest.ini:
[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
What this means in day-to-day use:
- Tests are collected from
tests/. pytest-testconfigautomatically loadstests/tests_config/config.py.- JUnit XML is written by default to
junit-report.xml. - The suite enables strict marker validation and xdist
loadscopedistribution. - Pytest's built-in logging plugin is disabled, and the suite configures its own logger.
Note: JIRA integration is installed but opt-in.
pytest-jirais a project dependency, and it registers--jirawith a default ofFalse.--jirais not part ofaddopts. Add it yourself when you want JIRA-linked markers to drive test behavior;jira.cfg.exampleshows the expectedjira.cfgformat.
Required Runtime Overrides
For a normal run, the suite expects two runtime values even though they are not defined in the default config file:
| Key | Required | Purpose |
|---|---|---|
source_provider |
Yes (or a pool, see below) | Selects the source provider entry from .providers.json |
source_provider_type |
Pool alternative | Identifies the candidate pool for latency-based selection (required for pool mode) |
source_provider_version |
Optional | Narrows the pool to an exact version; omitted, every version of the type competes |
source_provider_feature |
Optional | Feature gate for the pool (copyoffload): copy-offload-capable providers are excluded without it and included with it |
storage_class |
Yes | Sets the target OpenShift storage class for migration |
cluster_host |
Optional | Cluster API URL. Falls back to the CLUSTER_HOST environment variable |
cluster_username |
Optional | Cluster user. Falls back to the CLUSTER_USERNAME environment variable |
cluster_password |
Optional | Cluster password. Falls back to the CLUSTER_PASSWORD environment variable |
If either required key is missing, the session aborts with pytest.exit(...) and return code 1 before any fixture runs.
Cluster credentials resolve from the environment, in this order: the --tc value first, then the environment variable. CLUSTER_VERIFY_SSL takes
precedence over insecure_verify_skip for OpenShift API SSL verification, and its semantics are inverted: CLUSTER_VERIFY_SSL=true means insecure_verify_skip=False.
Note:
cluster_host,cluster_usernameandcluster_passwordhave no built-in default. All three resolved values must be nonblank unless all three keys are absent from both pytest config and the environment. When all three are absent,get_cluster_client()authenticates from the ambient kubeconfig — the path a directpytestrun takes when a kubeconfig is available to the test process.mtv-api-tests runtakes that path only when no OpenShift provider is selected from.providers.json: the CLI checks for an existingoctoken (oc whoami -t) only then, and with an OpenShift provider entry present — as in the example configuration — it instead uses that provider'shost,username, andpassword. When any of the three keys is provided but resolves to an absent, empty, or whitespace-only value, the run fails fast with aValueErrornaming each such value, for exampleValueError("Missing cluster credentials: set 'cluster_username' in pytest config or CLUSTER_USERNAME"). That happens as theocp_admin_clientfixture requests the client, before any migration work, and the message says which variable to set.
The repository's OpenShift Job template in README.md uses --tc= overrides like this:
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]
Warning:
source_provideris not the provider type. It must match a top-level key in.providers.jsonexactly.
Latency-Based Provider Selection
source_provider can be resolved by latency instead of being pinned explicitly. Pass a pool instead of a key:
--tc=source_provider_type:vsphere --tc=source_provider_version:8.0.1 # exact version
--tc=source_provider_type:vsphere # every version of the type
A feature key (--tc=source_provider_feature: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. The session-scoped selected_source_provider fixture — requested first by the
autouse fixture, before anything consumes the provider — derives the candidate pool from .providers.json
(entries matching the type, and the version when passed, gated by the feature key — copyoffload providers only
with the key, excluded without it). OpenShift pools are never probed: create_source_provider rewrites
OpenShift sources to the local cluster host, so their configured endpoints are unused — the lowest provider key
is selected deterministically. 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 and
the lowest provider key wins. Otherwise the fixture launches one probe pod on the cluster and measures TCP-connect
RTT to every unique candidate endpoint (providers sharing a host:port are measured once and share the
measurement). Each endpoint's host comes from fqdn when set, else from api_url (a URL authority or an NFS
export path — OVA entries carry an empty fqdn and an NFS api_url), else from the OpenShift host field; the
port comes from an explicit port in api_url when it carries one (e.g. OpenStack 13000), otherwise the type's API
port — OVA NFS 2049, Hyper-V 5986, everything else 443. OVA endpoints are probed on the NFS port (2049) because
that is the data path Forklift's populator mounts. Selection is the lowest RTT with the lowest provider key as the
deterministic tiebreak — median of three connects.
In a single-process run the pool is probed inline; under xdist the first worker to hold a run-scoped file lock
(PYTEST_XDIST_TESTRUNUID-keyed, so parallel runs never share it) probes and writes the selection while every other
worker reads the same file. The chosen provider and its RTT are logged.
Precedence and failure modes:
- 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, as before.
- An empty pool (no type match, or no provider passes the feature gate) aborts the session — never a skip.
-
An entry with no resolvable endpoint host (empty
fqdn,api_url, andhost) raises aValueErrornaming the type — it is never probed against an empty host (OpenShift entries are exempt: they are never probed at all). -
Unreachable candidates are excluded from selection; 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 item names carry the type instead of the provider key.
For copy-offload (XCOPY) plans the bulk disk copy runs 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 only, so selection stays valid for copy-offload pools but the runtime win applies to cold/warm (NBD)
migrations. Pool members are expected to be true clones of each other (same VM inventory, same capabilities); the
feature key enforces copy-offload capability, VM inventory equivalence is the operator's responsibility.
Global pytest-testconfig Values
The default shared values live at the top of tests/tests_config/config.py:
global config
insecure_verify_skip: str = "true" # SSL verification for OCP API connections
source_provider_insecure_skip_verify: str = "false" # SSL verification for source provider (VMware, RHV, etc.)
number_of_vms: int = 1
check_vms_signals: bool = True
target_namespace_prefix: str = "auto"
mtv_namespace: str = "openshift-mtv"
vm_name_search_pattern: str = ""
remote_ocp_cluster: str = ""
snapshots_interval: int = 2
mins_before_cutover: int = 5
plan_wait_timeout: int = 3600
Actively used global values
| Key | Default | What it controls |
|---|---|---|
insecure_verify_skip |
"true" |
OpenShift API SSL verification. The cluster client is created with verify_ssl=not insecure_verify_skip. |
source_provider_insecure_skip_verify |
"false" |
Source-provider SSL verification and the Provider secret's insecureSkipVerify value. |
target_namespace_prefix |
"auto" |
Base text used when generating the target namespace name for migrated resources. |
mtv_namespace |
"openshift-mtv" |
Namespace used for MTV resources, pod health checks, and must-gather collection. |
remote_ocp_cluster |
"" |
Enables tests marked remote when set and validates the current cluster host against that value. |
snapshots_interval |
2 |
Updates the forklift-controller precopy interval for warm migration tests. |
mins_before_cutover |
5 |
Number of minutes added when calculating warm-migration cutover time. |
plan_wait_timeout |
3600 |
Timeout used while waiting for migration plans to complete. |
Defined in the default file but not currently used elsewhere
A repository-wide search shows these keys are defined in tests/tests_config/config.py but are not referenced by the rest of the codebase today:
| Key | Default |
|---|---|
number_of_vms |
1 |
check_vms_signals |
True |
vm_name_search_pattern |
"" |
Warning: For boolean-style CLI overrides such as
insecure_verify_skipandsource_provider_insecure_skip_verify, use lowercase string values liketrueandfalse. The default config file stores them as strings, and some code paths handle them that way.
Named Test Plans In tests_params
The same config file also contains tests_params, which is the catalog of named migration scenarios used by the test classes. These are not suite-wide defaults; they are
per-scenario plan definitions.
A minimal real entry from tests/tests_config/config.py:
"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,
},
Those named plans are referenced directly by the tests through class_plan_config with indirect=True:
@pytest.mark.parametrize(
"class_plan_config",
[
pytest.param(
py_config["tests_params"]["test_warm_migration_comprehensive"],
)
],
indirect=True,
ids=["comprehensive-warm"],
)
Test Plan Configuration documents every per-VM key and plan-level flag the code supports, with the real names.
Custom Pytest Options
The repository adds six user-facing pytest options on top of the standard pytest set:
| Option | Group |
|---|---|
--providers-json <path> |
Providers |
--skip-data-collector |
DataCollector |
--data-collector-path <path> |
DataCollector |
--skip-teardown |
Teardown |
--openshift-python-wrapper-log-debug |
Openshift Python Wrapper |
--analyze-with-ai |
Analyze with AI |
Provider file selection
Resolution order when the suite loads provider definitions:
--providers-jsonCLI value.PROVIDERS_JSON_PATHenvironment variable..providers.jsonin the current working directory.
A missing path raises FileNotFoundError, and an empty or non-mapping file fails fast. See Provider Config File.
Data collection
By default, the suite collects runtime artifacts for failed runs.
- The base directory defaults to
.data-collector. - Class setup/call failures queue
oc adm must-gatheruntil the pre-finalizer class-plan boundary, attempting at most one successful gather per class-plan instance per worker. Operational class-plan failures remain pending for session-finish retry; artifacts are not guaranteed if retry fails. Standalone tests attempt collection per failing phase until one succeeds, but failed attempts are not queued for session-finish retry. Subdirectories use percent-encoded node identities, class parameter indices where applicable, and worker IDs. - At session finish, it writes a
resources.jsonfile with tracked resources. - If session teardown fails, it attempts an additional must-gather into the base collector directory.
Use these flags to control that behavior:
--skip-data-collectordisables artifact collection.--data-collector-path <path>changes the output directory.
Warning: The suite deletes and recreates the base data-collector directory at session start. Use a dedicated path if you want to preserve older artifacts.
Note: The current help text for
--skip-data-collectoris misleading. In actual runtime behavior, the flag disables data collection.Tip: When
resources.jsonis available, the repository includestools/clean_cluster.pyto clean resources from that file.
Teardown control
By default, the suite cleans up the resources it created.
That includes:
- Session-level tracked resources such as plans, providers, namespaces, and migrated resources.
- Class-level cleanup of migrated VMs through the
cleanup_migrated_vmsfixture.
Use --skip-teardown when you want to keep resources around for debugging.
The repository's own documentation shows it this way:
uv run pytest -m copyoffload --skip-teardown \
-v \
...
Warning:
--skip-teardownis for investigation and debugging. If you use it, you are responsible for cleaning up leftover resources afterward.
Debug logging
Logging is already enabled by default through pytest.ini:
-skeeps stdout/stderr visible.-o log_cli=trueenables live console logging.- The suite writes logs to
pytest-tests.logunless you set a different--log-file. - The log level comes from pytest's
log_cli_leveloption and falls back toINFO.
There is also one custom debug flag:
--openshift-python-wrapper-log-debugsetsOPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG.
In other words, the main knobs for troubleshooting are:
- Standard pytest logging options such as
log_cli_levelandlog_file. - The custom
--openshift-python-wrapper-log-debugflag for wrapper internals.
Tip: For deeper troubleshooting, raise
log_cli_level, write to a dedicated--log-file, and add--openshift-python-wrapper-log-debug.
AI failure analysis
The suite can enrich failed JUnit XML reports with AI-generated analysis.
Enable it with:
--analyze-with-ai
When enabled, the code does the following:
- Disables itself in dry-run mode (
--collect-only,--setup-plan). - Calls
load_dotenv(), so a local.envfile can supply the settings. - Requires
ROOTCOZ_SERVER_URL; if unset, it logs a warning and turns the feature off. - After a failed run, reads the JUnit XML file and posts the raw XML to
${ROOTCOZ_SERVER_URL}/analyze-failures. - If enrichment succeeds, writes the enriched XML back to the same file.
Environment variables used by this feature:
| Variable | Required | Default | Purpose |
|---|---|---|---|
ROOTCOZ_SERVER_URL |
Yes | None | Base URL of the analysis service |
ROOTCOZ_TIMEOUT |
No | 600 |
Request timeout in seconds. Invalid values fall back to 600 |
ROOTCOZ_AI_PROVIDER |
No | None | AI provider name sent to the service. When unset, the service applies its own default |
ROOTCOZ_AI_MODEL |
No | None | AI model name sent to the service. When unset, the service applies its own default |
Note: pytest does not hard-code a provider or model default. Those defaults live in
.rootcoz/settings.json, which is owned by the rootcoz service.
Request payload:
payload: dict[str, str] = {"raw_xml": raw_xml}
if ai_provider := os.environ.get("ROOTCOZ_AI_PROVIDER"):
payload["ai_provider"] = ai_provider
if ai_model := os.environ.get("ROOTCOZ_AI_MODEL"):
payload["ai_model"] = ai_model
Important behavior to know:
- Successful runs skip AI enrichment.
--collect-onlyand--setup-plandisable AI analysis automatically.- If the JUnit XML path is unset or the file is missing, enrichment is skipped.
- If enrichment fails, the original JUnit XML is preserved.
- The default JUnit XML path is already configured as
junit-report.xmlinpytest.ini.
Warning:
--analyze-with-aisends the raw JUnit XML content to an external HTTP service. Review what your report contains before enabling this in shared or external environments.Note: If enrichment succeeds, the original JUnit XML file is overwritten in place with the enriched version.
Dry-Run Modes
This repository treats --collect-only and --setup-plan as dry-run modes.
That behavior is visible in tox.toml:
[env.pytest-check]
commands = [
[
"uv",
"run",
"pytest",
"--setup-plan",
],
[
"uv",
"run",
"pytest",
"--collect-only",
],
]
And the container image defaults to collection-only mode:
CMD ["uv", "run", "pytest", "--collect-only"]
In dry-run mode:
- Required runtime checks for
source_providerandstorage_classare skipped. - AI analysis is disabled.
- Session-finish teardown and JUnit enrichment do not run.
Tip: If you start the published container image without overriding its command, it only performs test collection. To run real tests, supply your own
uv run pytest ...command.