Test Plan Configuration
tests_params is the catalog of named migration plans used by this repository. Each entry defines which source VMs to use, which MTV plan features to enable, and which extra
test behaviors should run before or after migration.
The important thing to know is that the value you write in tests/tests_config/config.py is the raw plan. The test fixtures then turn that raw plan into a runtime
prepared_plan before creating the MTV Plan custom resource.
Where Plans Live
The suite loads tests/tests_config/config.py as a Python config file, not as YAML or JSON:
addopts =
-s
-o log_cli=true
-p no:logging
--tc-file=tests/tests_config/config.py
--tc-format=python
Each test class then picks one named entry from py_config["tests_params"]:
@pytest.mark.parametrize(
"class_plan_config",
[
pytest.param(
py_config["tests_params"]["test_sanity_cold_mtv_migration"],
)
],
indirect=True,
ids=["rhel8"],
)
Note:
tests/tests_config/config.pycontains both shared test-suite settings and thetests_paramsdictionary. Only entries insidetests_paramsare individual plan definitions.
Basic Structure
A minimal plan can be very small:
"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,
},
A more advanced plan adds destination placement, PVC naming, labels, and affinity:
"test_warm_migration_comprehensive": {
"virtual_machines": [
{
"name": "mtv-win2022-ip-3disks",
"source_vm_power": "on",
"guest_agent": True,
},
],
"warm_migration": True,
"target_power_state": "on",
"preserve_static_ips": True,
"enable_nested_virtualization": False,
"vm_target_namespace": f"mtv-vms-warm-comprehensive-{uuid.uuid4().hex[:4]}",
"multus_namespace": "default", # Cross-namespace NAD access
# Keys must be Provider.ProviderType string values (e.g. "vsphere") or "default".
# vsphere uses .FileName; default truncates VmName to stay within 63 chars.
"pvc_name_template": {
"vsphere": '{{ .FileName | trimSuffix ".vmdk" | replace "_" "-" }}-{{.DiskIndex}}',
"default": '{{ .VmName | trunc 32 | trimSuffix "-" }}-{{ .VmName | trunc -4 }}-disk-{{.DiskIndex}}',
},
"pvc_name_template_use_generate_name": True,
"target_labels": {
"mtv-comprehensive-test": None, # None = auto-generate with session_uuid
"static-label": "static-value",
},
"target_affinity": {
"podAffinity": {
"preferredDuringSchedulingIgnoredDuringExecution": [
{
"podAffinityTerm": {
"labelSelector": {"matchLabels": {"app": "comprehensive-test"}},
"topologyKey": "kubernetes.io/hostname",
},
"weight": 75,
}
]
}
},
"guest_agent_timeout": 600,
},
In every entry:
virtual_machinesis the per-VM section.- Everything else is plan-level behavior or test behavior.
Note: This file is plain Python. Use Python values such as
True,False, andNone, not JSON values liketrue,false, ornull.
Per-VM Settings
Each item in virtual_machines describes one source VM or template.
| Key | Required | Values or meaning |
|---|---|---|
name |
Yes | Source VM or template name. During preparation this may be rewritten to the actual cloned runtime name. |
source_vm_power |
No | "on" starts the VM, "off" stops it. Omitting it leaves the current state unchanged. |
guest_agent |
No | True when the guest agent is expected to run after migration. |
clone |
No | True marks the VM as a placeholder to migrate: on copy-offload plans the name is replaced by copyoffload.default_vm_name. It does not switch the cloning phase off — prepared_plan clones every VM unless the plan sets skip_clone: True. |
clone_name |
No | Overrides the default clone base name. |
preserve_name_format |
No | True keeps uppercase letters and underscores in clone_name instead of sanitizing them. |
disk_type |
No | thin, thick-lazy, or thick-eager. |
add_disks |
No | List of extra disks to attach to the cloned VM before migration. |
snapshots |
No | Number of source snapshots to create before migration. |
target_datastore_id |
No | VMware target datastore for the cloned VM. |
target_esxi_host |
No | Pin the clone to a specific ESXi host. Set automatically by clone_to_same_host and pin_to_non_dedicated_host unless you set it yourself. |
luks |
No | True when the VM has a LUKS-encrypted disk. Consumed by the LUKS fixture and replaced with a Secret reference. |
luks_passphrase |
No | Per-VM LUKS passphrase. Overrides the provider-level luks_passphrase. |
migrate_shared_disks |
No | True for the owner VM in a shared-disk pair. Overrides the plan-level value for that VM. |
add_nic |
No | True to add an extra NIC. vSphere only. |
add_nic_start_connected |
Yes* | Required when add_nic is True. Must be a bool. |
win_os |
No | Hyper-V guest OS override. Must be a bool when present. |
Validation is strict for the rows marked with an asterisk:
add_nic: Truewithoutadd_nic_start_connectedraises aValueError.- A non-
booladd_nic_start_connectedraises aValueError. add_nicon a non-vSphere provider skips the test.win_oswith a non-boolvalue raises aTypeErrorin the Hyper-V provider.
Disk types
DISK_TYPE_MAP = {
"thin": ("sparse", "Setting disk provisioning to 'thin' (sparse)."),
"thick-lazy": ("flat", "Setting disk provisioning to 'thick-lazy' (flat)."),
"thick-eager": ("flat", "Setting disk provisioning to 'thick-eager' (flat + eagerlyScrub)."),
}
thick-eager maps to a flat disk with eagerlyScrub set, not to eagerZeroedThick.
add_disks entries
Each item in add_disks supports these keys:
| Key | Required | Meaning |
|---|---|---|
size_gb |
Yes for a normal added disk | Disk size in gigabytes. |
provision_type |
No | thin, thick-lazy, or thick-eager. Defaults to thin. |
disk_mode |
No | vSphere disk mode. Defaults to persistent. independent_persistent is used in tests_params. |
datastore_id |
No | Places this disk on a specific datastore instead of the VM's default. |
datastore_path |
No | Directory path to create on the datastore, for datastore-path tests. |
rdm_type |
No | virtual or physical. Creates an RDM disk from copyoffload.rdm_lun_uuid. |
A real multi-disk example:
"test_copyoffload_multi_disk_migration": {
"virtual_machines": [
{
"name": "xcopy-template-test",
"guest_agent": True,
"clone": True,
"add_disks": [
{"size_gb": 30, "disk_mode": "persistent", "provision_type": "thick-lazy"},
],
},
],
"warm_migration": False,
"copyoffload": True,
},
For special naming tests, the config can override the clone name entirely:
"test_copyoffload_nonconforming_name_migration": {
"virtual_machines": [
{
"name": "xcopy-template-test",
"clone_name": "XCopy_Test_VM_CAPS", # Non-conforming name for cloned VM
"preserve_name_format": True, # Don't sanitize the name (keep capitals and underscores)
"guest_agent": True,
"clone": True,
"disk_type": "thin",
},
],
"warm_migration": False,
"copyoffload": True,
},
Snapshot-Driven Tests
snapshots affects test preparation, not just MTV plan creation. Snapshot tests create the snapshots first, then store the pre-migration snapshot list back into the prepared
plan for later validation:
vm_cfg = prepared_plan["virtual_machines"][0]
provider_vm_api = prepared_plan["source_vms_data"][vm_cfg["name"]]["provider_vm_api"]
source_provider.start_vm(provider_vm_api)
source_provider.wait_for_vmware_guest_info(provider_vm_api, timeout=60)
snapshots_to_create = int(vm_cfg["snapshots"])
snapshot_prefix = f"{vm_cfg['name']}-{fixture_store['session_uuid']}-snapshot"
for idx in range(1, snapshots_to_create + 1):
source_provider.create_snapshot(
vm=provider_vm_api,
name=f"{snapshot_prefix}-{idx}",
description="mtv-api-tests copy-offload snapshots migration test",
memory=False,
quiesce=False,
wait_timeout=60 * 10,
)
vm_cfg["snapshots_before_migration"] = source_provider.vm_dict(provider_vm_api=provider_vm_api)[
"snapshots_data"
]
Note: In the class-based flow used by this repository, external-provider VMs are resolved with
clone_vm=Trueduring preparation. That means runtime VM names usually become per-test clone names even when the raw plan entry does not setclone: true.
Plan-Level Settings
Migration behavior
| Key | Required | Values or meaning |
|---|---|---|
warm_migration |
No | True for warm migration. Also adds the CTK flag to the clone options. |
copyoffload |
No | True to enable copy-offload (XCOPY) migration. |
target_power_state |
No | on or off. Expected power state of the migrated VM after completion. |
preserve_static_ips |
No | True to preserve static IP addresses. Requires source_vm_power: "on" for every VM. |
guest_agent_timeout |
No | Guest-info wait timeout in seconds. Defaults to 120 where used. |
migrate_shared_disks |
No | True to enable shared disk migration at plan level. Individual VMs can override. |
xfs_compatibility |
No | True to request XFS v4 filesystem compatibility on the Plan CR. |
rdm_as_lun |
No | True to map RDM disks as LUN devices with a SCSI bus instead of the default virtio. |
enable_nested_virtualization |
No | False disables the vmx and svm CPU features. |
run_preflight_inspection |
No | False disables the Plan CR preflight inspection step. |
preserve_static_ips is validated up front. Every VM must declare source_vm_power: "on", because guest tools must be running to collect static IP and NIC name data.
If you omit target_power_state, post-migration validation falls back to the source VM power state:
def check_vms_power_state(
source_vm: dict[str, Any],
destination_vm: dict[str, Any],
source_power_before_migration: str | None,
target_power_state: str | None = None,
) -> None:
# If targetPowerState is specified, check that the destination VM matches it
if target_power_state:
actual_power_state = destination_vm["power_state"]
LOGGER.info(f"Checking target power state: expected={target_power_state}, actual={actual_power_state}")
assert actual_power_state == target_power_state, (
f"VM power state mismatch: expected {target_power_state}, got {actual_power_state}"
)
LOGGER.info(f"Target power state verification passed: {actual_power_state}")
elif source_power_before_migration:
if source_power_before_migration not in ("on", "off"):
raise ValueError(f"Invalid source_vm_power '{source_power_before_migration}'. Must be 'on' or 'off'")
# Default behavior: destination VM should match source power state before migration
assert destination_vm["power_state"] == source_power_before_migration
Placement, naming, and scheduling
| Key | Required | Meaning |
|---|---|---|
vm_target_namespace |
No | Custom namespace for migrated VMs. The preparation flow creates it if needed. |
multus_namespace |
No | Namespace where NetworkAttachmentDefinitions are created for additional networks. |
pvc_name_template |
No | Forklift PVC naming template. Either a string, or a mapping of provider type to template. |
pvc_name_template_use_generate_name |
No | When True, post-checks treat the rendered PVC name as a prefix because Kubernetes adds a generated suffix. |
target_node_selector |
No | Node label selector used for VM placement tests. |
target_labels |
No | Labels to apply to migrated VM metadata. |
target_affinity |
No | Affinity rules passed to the migrated VM template. |
inventory_timeout |
No | Seconds to wait for cloned VMs in the Forklift inventory. Defaults to 300. |
None has a special meaning in target_labels and target_node_selector: the fixtures replace it with the current session_uuid so each test run gets a unique value.
pvc_name_template accepts two shapes. A plain string is used as is. A mapping must use Provider.ProviderType values, such as vsphere and ovirt, plus the optional key
default. Unknown keys raise a ValueError, and a mapping with neither a matching provider key nor default also raises. The mapping is resolved during preparation, before the
Plan CR is created.
Tip:
vm_target_namespaceandmultus_namespacesolve different problems. Usevm_target_namespaceto choose where migrated VMs land, andmultus_namespaceto choose where the test-created NetworkAttachmentDefinitions live.
Clone placement and throttling
| Key | Required | Meaning |
|---|---|---|
clone_to_same_host |
No | True pins VM2 and later to the ESXi host of the first VM. An explicit target_esxi_host wins. |
pin_to_non_dedicated_host |
No | True pins every VM to an ESXi host outside copyoffload.dedicated_migration_hosts. An explicit target_esxi_host wins. |
disable_drs_for_vms |
No | True disables vSphere DRS per VM after cloning. Not supported for OVA, and it requires a VMware clone provider. |
skip_clone |
No | True skips the clone phase and uses the existing VMs directly. |
per_nic_network_map |
No | True creates per-NIC network mappings, which allows duplicate source network entries in the NetworkMap. |
network_map_overrides |
No | Requirement-based NetworkMap destinations: each entry matches a source network by its guest IP configuration. |
network_map_overrides entries carry a source_ip_config requirement (dhcp, static, or
static-multiple) and a destination mapping whose type is pod, multus, or ignored, plus an
optional network_ip_mode (preserve, dhcp, or none). network_ip_mode is allowed on pod and
multus destinations and rejected on ignored ones. Requirements are resolved against guest IP
origins collected from the source VMs, so tests never hardcode network names;
resolve_network_map_overrides() in utilities/utils.py performs the matching and raises when no
unused source network satisfies a requirement. The overrides must also cover every source network
of the VMs exactly once, otherwise NetworkMap generation raises ValueError.
pin_to_non_dedicated_host needs copyoffload.dedicated_migration_hosts in the provider entry. Without it, the code raises a ValueError.
skip_clone has its own validation. It is rejected when the provider does not support skipping clones, and it is incompatible with disable_drs_for_vms, clone_to_same_host,
migrate_shared_disks, and add_nic, plus preserve_static_ips on every provider except Hyper-V. Because skipped VMs are real and shared, the fixture restores their original
power states during teardown.
XFS verification
XFS tests need both xfs_compatibility: True and an xfs_check block:
"test_warm_migration_xfs": {
"virtual_machines": [
{
"name": "mtv-feature-rhel7-xfs",
"source_vm_power": "on",
"guest_agent": True,
},
],
"warm_migration": True,
"target_power_state": "on",
"xfs_compatibility": True,
"xfs_check": {
"command": "/usr/sbin/xfs_info",
"mount_point": "/",
"expected_output": "crc=0",
},
},
| Key | Required | Meaning |
|---|---|---|
xfs_check.command |
Yes | Command to run on the migrated guest. |
xfs_check.mount_point |
Yes | Argument passed to the command, typically the mount point. |
xfs_check.expected_output |
Yes | String that must appear in the command output. |
The verification step runs [command, mount_point] over SSH and asserts that expected_output is in stdout. It is Linux-only and raises a ValueError for a Windows guest.
Hooks and failure expectations
The repository also supports hook-driven tests. A simple failure-path example looks like this:
"test_post_hook_retain_failed_vm": {
"virtual_machines": [
{
"name": "mtv-tests-rhel8",
"source_vm_power": "on",
"guest_agent": True,
},
],
"warm_migration": False,
"target_power_state": "off",
"pre_hook": {"expected_result": "succeed"},
"post_hook": {"expected_result": "fail"},
"expected_migration_result": "fail",
},
In hook config:
| Key | Meaning |
|---|---|
pre_hook / post_hook |
Dictionaries describing a pre-migration or post-migration Hook CR. |
expected_migration_result |
Tells the test whether a migration failure is expected. |
A hook dictionary must contain exactly one of these keys:
| Key | Values |
|---|---|
expected_result |
succeed or fail. Selects a predefined playbook. |
playbook_base64 |
A base64-encoded Ansible playbook, validated for base64, UTF-8, and YAML syntax. |
aap_job_template_id |
A positive integer AWX job template ID for an AAP hook. |
Specifying more than one, or none of them, raises a ValueError. expected_result and playbook_base64 cannot be empty or whitespace-only.
Copy-Offload-Specific Notes
copyoffload: true is only the plan-side switch. The source provider also needs a copyoffload section in .providers.json.
Warning: A raw plan with
copyoffload: trueis not enough by itself. The test session validates that the source provider is vSphere, that thecopyoffloadsection exists, thatstorage_hostname,storage_username, andstorage_passwordresolve, and thatstorage_vendor_productanddatastore_idare set.
A plan that drives dedicated-host verification looks like this:
"test_copyoffload_dedicated_migration_host_migration": {
"virtual_machines": [
{
"name": "xcopy-template-test",
"guest_agent": True,
"clone": True,
"disk_type": "thin",
"add_disks": [
{"size_gb": 10, "disk_mode": "persistent", "provision_type": "thin"},
{"size_gb": 10, "disk_mode": "persistent", "provision_type": "thin"},
],
},
{
"name": "xcopy-template-test",
"guest_agent": True,
"clone": True,
"disk_type": "thin",
"add_disks": [
{"size_gb": 10, "disk_mode": "persistent", "provision_type": "thin"},
{"size_gb": 10, "disk_mode": "persistent", "provision_type": "thin"},
],
},
],
"warm_migration": False,
"copyoffload": True,
"disable_drs_for_vms": True,
"pin_to_non_dedicated_host": True,
"inventory_timeout": 600,
},
There is one more important implementation detail: when the helper creates an MTV plan with copyoffload=True, it forces pvc_name_template to "pvc".
# Add copy-offload specific parameters if enabled
if copyoffload:
# Set PVC naming template for copy-offload migrations
# The volume populator framework requires this to generate consistent PVC names
# Note: generateName is enabled by default, so Kubernetes adds random suffix automatically
plan_kwargs["pvc_name_template"] = "pvc"
Warning: Do not expect a custom
pvc_name_templateintests_paramsto survive copy-offload plan creation. The helper overwrites it with"pvc".
Provider-side copy-offload settings are documented in Provider Config File.
How Raw Config Becomes prepared_plan
The raw entry from tests_params is not used directly. The repository converts it into a runtime prepared_plan in several steps.
1. The Raw Plan Is Deep-Copied
The fixture copies the selected config, resolves a mapping-form pvc_name_template, and sets up runtime-only storage:
# Deep copy the plan config to avoid mutation
plan: dict[str, Any] = deepcopy(class_plan_config)
if "pvc_name_template" in plan:
plan["pvc_name_template"] = resolve_pvc_name_template(
pvc_name_template=plan["pvc_name_template"],
source_provider_type=source_provider.type,
)
virtual_machines: list[dict[str, Any]] = plan["virtual_machines"]
warm_migration = plan.get("warm_migration", False)
# Initialize separate storage for source VM data (keeps virtual_machines clean for Plan CR serialization)
plan["source_vms_data"] = {}
# Handle custom VM target namespace
vm_target_namespace = plan.get("vm_target_namespace")
if vm_target_namespace:
LOGGER.info(f"Using custom VM target namespace: {vm_target_namespace}")
get_or_create_namespace(
fixture_store=fixture_store,
ocp_admin_client=ocp_admin_client,
namespace_name=vm_target_namespace,
)
plan["_vm_target_namespace"] = vm_target_namespace
else:
plan["_vm_target_namespace"] = target_namespace
2. The Clone Phase Runs
Unless skip_clone is set, each VM is cloned, the plan-level placement flags are applied, and shared disks are relinked between clones:
skip_clone = plan.get("skip_clone", False)
if skip_clone:
if not source_provider.supports_skip_clone():
raise ValueError(
f"skip_clone=True is not supported for provider type '{source_provider.type}'; "
"VMs must be cloned from templates."
)
skip_clone_incompatible = ["disable_drs_for_vms", "clone_to_same_host"]
if source_provider.type != Provider.ProviderType.HYPERV:
skip_clone_incompatible.append("preserve_static_ips")
conflicting = [flag for flag in skip_clone_incompatible if plan.get(flag)]
if has_shared_disk_config:
conflicting.append("migrate_shared_disks")
if has_add_nic_config:
conflicting.append("add_nic")
if conflicting:
raise ValueError(
f"skip_clone=True is incompatible with {conflicting}; these options require the cloning phase."
)
3. Each VM Is Cloned and Renamed
In the clone phase, the fixture resolves each VM through the clone provider, applies the placement flags, sets the source power state, and rewrites the VM name to the actual runtime name:
if not skip_clone:
for vm in virtual_machines:
clone_options = {**vm, "enable_ctk": warm_migration}
# Pin VM2+ to same ESXi host as VM1 (required for per-host inflight throttling).
# Uses setdefault to respect any explicit per-VM target_esxi_host override.
if plan.get("clone_to_same_host", False) and first_vm_esxi_host:
clone_options.setdefault("target_esxi_host", first_vm_esxi_host)
# Pin every VM to an ESXi host outside the configured dedicated hosts, so
# dedicated-host verification can never coincide with a VM's own host (MTV-6136).
if plan.get("pin_to_non_dedicated_host", False):
if non_dedicated_host_name is None:
non_dedicated_host_name = resolve_non_dedicated_esxi_host(
source_provider_inventory=source_provider_inventory,
source_provider_data=source_provider_data,
)
clone_options.setdefault("target_esxi_host", non_dedicated_host_name)
provider_vm_api = clone_provider.get_vm_by_name(
query=vm["name"],
vm_name_suffix=vm_name_suffix,
clone_vm=True,
session_uuid=fixture_store["session_uuid"],
clone_options=clone_options,
)
4. Hooks Are Created
After the VM loop, configured hooks become real Hook custom resources:
create_hook_if_configured(plan, "pre_hook", "pre", fixture_store, ocp_admin_client, target_namespace)
create_hook_if_configured(plan, "post_hook", "post", fixture_store, ocp_admin_client, target_namespace)
5. The Test Adds VM IDs and Creates the MTV Plan
Right before creating the MTV Plan, the tests add VM IDs from the Forklift inventory and pass the prepared values into create_plan_resource():
self.__class__.plan_resource = create_plan_resource(
ocp_admin_client=ocp_admin_client,
fixture_store=fixture_store,
source_provider=source_provider,
destination_provider=destination_provider,
storage_map=self.storage_map,
network_map=self.network_map,
virtual_machines_list=prepared_plan["virtual_machines"],
target_power_state=prepared_plan["target_power_state"],
target_namespace=target_namespace,
warm_migration=prepared_plan["warm_migration"],
preserve_static_ips=prepared_plan["preserve_static_ips"],
vm_target_namespace=prepared_plan["vm_target_namespace"],
pvc_name_template=prepared_plan["pvc_name_template"],
pvc_name_template_use_generate_name=prepared_plan["pvc_name_template_use_generate_name"],
target_labels=target_vm_labels["vm_labels"],
target_affinity=prepared_plan["target_affinity"],
enable_nested_virtualization=prepared_plan["enable_nested_virtualization"],
)
create_plan_resource() accepts only the flags that map onto the Plan CR: warm_migration, copyoffload, preserve_static_ips, pvc_name_template,
pvc_name_template_use_generate_name, target_node_selector, target_labels, target_affinity, vm_target_namespace, migrate_shared_disks, target_power_state,
enable_nested_virtualization, xfs_compatibility, run_preflight_inspection, and rdm_as_lun. Everything else is consumed by fixtures or by post-migration checks.
Runtime Fields Added During Preparation
These fields are derived at runtime. You do not write them yourself in tests_params.
| Field | Added by | Purpose |
|---|---|---|
source_vms_data |
prepared_plan |
Stores rich source VM details for later validation without polluting virtual_machines. |
_vm_target_namespace |
prepared_plan |
Resolved namespace used by post-migration validation. |
_pre_hook_name / _pre_hook_namespace |
Hook creation | Created Hook CR reference for plan creation. |
_post_hook_name / _post_hook_namespace |
Hook creation | Created Hook CR reference for plan creation. |
virtual_machines[*].name |
prepared_plan |
Rewritten to the actual runtime VM name after provider lookup or cloning. |
virtual_machines[*].targetName |
prepared_plan |
Set for OVA only, as a per-session name so parallel runs do not collide. |
virtual_machines[*].snapshots_before_migration |
prepared_plan or snapshot test setup |
Snapshot baseline used for post-migration checks. |
virtual_machines[*].connected_nic_mac / disconnected_nic_mac |
add_nic handling |
MAC of the NIC added for the test. |
virtual_machines[*].luks |
LUKS fixture | Replaced by a {"name": ...} Secret reference. |
virtual_machines[*].id |
populate_vm_ids() |
Forklift VM ID required by plan creation. |
A few raw config fields are resolved by companion fixtures instead of prepared_plan itself:
multus_namespaceis consumed by the network setup fixture that creates NADs.target_labelsis resolved bytarget_vm_labels.target_node_selectoris resolved bylabeled_worker_node.
Tip: Keep
tests_paramsdeclarative. Do not hand-write runtime fields such asid,source_vms_data,_vm_target_namespace, orsnapshots_before_migration. Let the fixtures derive them.
Practical Authoring Guidelines
- Start with one VM and one or two plan-level flags, then add more only when the scenario really needs them.
- Treat
virtual_machinesas source-side intent andprepared_planas runtime state. They are not the same thing. - Use
Noneintentionally intarget_labelsortarget_node_selectorwhen you want a unique value per run. - Expect VM names in
prepared_plan["virtual_machines"]to differ from the rawnameonce preparation is complete. - For copy-offload scenarios, think in two layers: test plan config in
tests_params, and provider/storage config in.providers.json. - Copy the key names exactly. Required keys are read directly, so a typo there is a
KeyError; optional flags are read with.get(), so a misspelled flag such asskip_cloneorclone_to_same_hostsilently takes its default and changes the scenario instead of failing.