Provider And Inventory Reference
mtv-api-tests uses two sources of truth for source-side data:
- direct provider classes for actions such as connect, clone, power control, snapshots, and VM inspection
- Forklift inventory adapters for the names, IDs, networks, and storages that MTV itself consumes
That split explains most of the repository's behavior. If a problem is about cloning, power state, guest information, or direct SDK access, start with the provider class. If a problem is about NetworkMap, StorageMap, or a VM ID inside a Plan, start with Forklift inventory.
End-To-End Flow
- The suite loads
.providers.jsonfrom the repository root. py_config["source_provider"]selects one top-level provider entry from that file.create_source_provider()creates the sourceSecretand sourceProviderCR, then instantiates the matchingBaseProvidersubclass.source_provider_inventorychooses the matchingForkliftInventoryadapter for that provider type.prepared_planclones or creates source VMs, normalizes them throughvm_dict(), rewrites the VM name if needed, and waits for Forklift inventory to see the VM.get_network_migration_map()andget_storage_migration_map()turn inventory data intoNetworkMapandStorageMapCR payloads.populate_vm_ids()copies Forklift VM IDs into thePlanpayload just beforecreate_plan_resource()runs.
The synchronization step is explicit in conftest.py:
for vm in virtual_machines:
# Get VM object first (without full vm_dict analysis)
# Add enable_ctk flag for warm migrations
clone_options = {**vm, "enable_ctk": warm_migration}
provider_vm_api = source_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,
)
source_vm_details = source_provider.vm_dict(
provider_vm_api=provider_vm_api,
name=vm["name"],
namespace=source_vms_namespace,
clone=False, # Already cloned above
vm_name_suffix=vm_name_suffix,
session_uuid=fixture_store["session_uuid"],
clone_options=vm,
)
vm["name"] = source_vm_details["name"]
if source_provider.type != Provider.ProviderType.OVA:
source_provider_inventory.wait_for_vm(name=vm["name"], timeout=300)
Note: A map-generation failure often means Forklift inventory has not finished syncing the VM yet. The suite intentionally waits for inventory before creating maps, because MTV consumes inventory objects, not the provider SDK's in-memory objects.
Provider Configuration And Selection
The selection logic is intentionally simple: .providers.json is loaded, and py_config["source_provider"] picks one named entry.
@pytest.fixture(scope="session")
def source_provider_data(source_providers: dict[str, dict[str, Any]], fixture_store: dict[str, Any]) -> dict[str, Any]:
"""Resolve source provider configuration from .providers.json."""
if not source_providers:
raise MissingProvidersFileError()
requested_provider = py_config["source_provider"]
if requested_provider not in source_providers:
raise ValueError(
f"Source provider '{requested_provider}' not found in '.providers.json'. "
f"Available providers: {sorted(source_providers.keys())}"
)
_source_provider = source_providers[requested_provider]
fixture_store["source_provider_data"] = _source_provider
return _source_provider
The repository does not commit a sample .providers.json, so the code is the best reference for required fields.
| Field | Why the suite reads it |
|---|---|
type |
Selects the provider class and the Forklift inventory adapter |
version |
Used in generated resource names by base_resource_name |
api_url |
Written into the source provider secret and the Provider CR |
username, password |
Read before provider-specific branching |
fqdn |
Used by the CA certificate helper when certificate download is needed |
The factory in utilities/utils.py reads the common connection fields before it knows which provider type it is handling:
secret_string_data = {
"url": source_provider_data_copy["api_url"],
"insecureSkipVerify": "true" if insecure else "false",
}
provider_args = {
"username": source_provider_data_copy["username"],
"password": source_provider_data_copy["password"],
"fixture_store": fixture_store,
}
Provider-specific extras are then added on top:
| Source type | Extra fields read by code |
|---|---|
vsphere |
optional copyoffload, optional vddk_init_image |
rhv |
no extra SDK login fields, but the current code still expects fqdn for CA cert download |
openstack |
project_name, user_domain_name, region_name, user_domain_id, project_domain_id |
openshift |
no extra provider-specific fields; the source uses the destination cluster secret |
ova |
no extra provider-specific fields; direct provider logic is intentionally minimal |
Warning:
create_source_provider()readsapi_url,username, andpasswordbefore provider-specific branching, so every provider entry needs those keys.versionis also needed for generated resource names. When certificate download is involved,fqdnmatters too, because the certificate helper always connects to<fqdn>:443.
The BaseProvider Contract
BaseProvider is the common surface that keeps the rest of the suite provider-agnostic. Each concrete provider can use its own SDK, but it must expose the same core operations to the test code.
The normalized VM shape is defined in libs/base_provider.py:
VIRTUAL_MACHINE_TEMPLATE: dict[str, Any] = {
"id": "",
"name": "",
"provider_type": "", # "ovirt" / "vsphere" / "openstack"
"provider_vm_api": None,
"network_interfaces": [],
"disks": [],
"cpu": {},
"memory_in_mb": 0,
"snapshots_data": [],
"power_state": "",
}
The most important abstraction for mapping logic is get_vm_or_template_networks():
def get_vm_or_template_networks(
self,
names: list[str],
inventory: ForkliftInventory,
) -> list[dict[str, str]]:
"""Get network mappings for VMs or templates (before cloning).
This method handles provider-specific differences:
- RHV: Queries template networks directly (templates don't exist in inventory yet)
- VMware/OpenStack/OVA/OpenShift: Queries VM networks from Forklift inventory
"""
In practice, BaseProvider gives the suite five important guarantees:
connect()anddisconnect()manage the direct SDK session.testis used immediately after provider creation to fail fast when the source is not reachable.vm_dict()returns a normalized view of the source or destination VM.clone_vm()anddelete_vm()let the suite prepare and clean up source-side test VMs without special-case code in every test.get_vm_or_template_networks()lets the suite size destination networking even before cloned VMs have finished syncing into inventory.
Provider-Specific Behavior
vSphere
VMWareProvider in libs/providers/vmware.py is the most feature-rich source implementation in the repo.
- It uses
pyVmomifor clone, power, disk, and guest-information operations. - Clone-time options cover disk provisioning (
thin,thick-lazy,thick-eager), extra disks, datastore overrides, ESXi host overrides, MAC regeneration, and Change Block Tracking for warm migrations. - If
copyoffload.esxi_clone_methodis set tossh, the provider patches the MTVProviderCR to setesxiCloneMethod. - When copy-offload is configured, the source
ProviderCR also gets the annotationforklift.konveyor.io/empty-vddk-init-image: yes. - Standard network and storage mappings come from
VsphereForkliftInventory. Copy-offload storage mappings can bypass inventory and use explicit datastore IDs instead.
Note: The test-side vSphere SDK connection uses
disableSslCertValidation=True, while the ForkliftProviderCR still honorssource_provider_insecure_skip_verifyand can includecacert. That means direct provider access and MTV-side validation are related, but not identical, code paths.
RHV / oVirt
OvirtProvider in libs/providers/rhv.py has two important behaviors that are easy to miss.
- It refuses to connect unless the RHV datacenter named
MTV-CNVexists and isup. clone_vm()clones from a template, not from a running VM. In other words,source_vm_nameis treated as a template name in RHV flows.get_vm_or_template_networks()ignores inventory during pre-clone network discovery and queries template NICs directly.- Later, once cloned VMs exist and Forklift has synced them, final
NetworkMapandStorageMapgeneration still comes from inventory. - RHV always fetches a CA certificate in
create_source_provider(), even when insecure mode is enabled, because the code comments call out imageio as a dependency for that certificate.
OpenStack
OpenStackProvider in libs/providers/openstack.py clones more like an image pipeline than a simple VM copy.
- It snapshots the source server.
- It creates new volumes from the resulting snapshots.
- It preserves boot order across the recreated volumes.
- It boots the cloned server from those new volumes.
OpenStack is also the strictest inventory-sync path. The suite does not treat “VM exists in inventory” as enough. It also waits for attached volumes and networks to become queryable:
if self.provider_type == Provider.ProviderType.OPENSTACK:
if not (
self._check_openstack_volumes_synced(vm, name)
and self._check_openstack_networks_synced(vm, name)
):
return None
That matters because OpenStack storage mapping is based on volume type, and network mapping is based on inventory network objects matched against VM address data. If either side is late, map generation will be wrong.
OpenShift
OCPProvider is both the destination provider for migrations and a supported source provider for source-side CNV test setups.
- If OpenShift is the source,
prepared_plancreates source CNV VMs and a source-sideNetworkAttachmentDefinitionautomatically. OpenshiftForkliftInventoryresolves storage by following the VM's data volumes to PVCs and then readingstorageClassName.- It resolves networks from the VM template:
multus.networkNamebecomes a named source network, and a pod network becomes{"type": "pod"}. - On destination lookups,
OCPProvider.vm_dict()sanitizes VM names to Kubernetes-safe resource names before querying the cluster.
OVA
OVAProvider is intentionally thin.
connect()is effectively a no-op.clone_vm()anddelete_vm()are not implemented.vm_dict()only fills the normalized template with the provider type andpower_state="off".- In the current
prepared_planimplementation, OVA sources are rewritten to the fixed VM name1nisim-rhel9-efi. - In practice, OVA mapping behavior depends much more on Forklift inventory than on direct provider logic.
Note: The warm-migration test suites currently skip OpenStack, OpenShift, and OVA sources. RHV warm coverage is gated by a Jira marker. In this repository, vSphere is the fully exercised warm-migration source.
Forklift Inventory Adapters
Forklift inventory lives behind the forklift-inventory route in the MTV namespace. Each adapter subclasses ForkliftInventory and knows how to translate that provider's inventory model into the source-side names and IDs that MTV map CRs expect.
The adapter selection is defined in conftest.py:
providers = {
Provider.ProviderType.OVA: OvaForkliftInventory,
Provider.ProviderType.RHV: OvirtForkliftInventory,
Provider.ProviderType.VSPHERE: VsphereForkliftInventory,
Provider.ProviderType.OPENSHIFT: OpenshiftForkliftInventory,
Provider.ProviderType.OPENSTACK: OpenstackForliftinventory,
}
provider_instance = providers.get(source_provider.type)
The adapters all use the same base route plumbing in libs/forklift_inventory.py: they discover the provider ID, build a provider-specific URL path, and then query VM, network, and storage endpoints through the inventory service.
| Source type | Adapter | Storage resolution | Network resolution |
|---|---|---|---|
| vSphere | VsphereForkliftInventory |
Each disk's datastore.id is matched to an inventory datastore name |
Each inventory VM network id is matched to an inventory network name |
| RHV | OvirtForkliftInventory |
Disk attachment -> disk -> storageDomain -> storage domain name |
NIC profile -> network ID -> inventory network path |
| OpenStack | OpenstackForliftinventory |
Attached volume -> volume details -> volumeType |
VM addresses keys are matched to inventory network names |
| OpenShift | OpenshiftForkliftInventory |
VM data volumes -> PVC -> storageClassName |
VM template networks become either named multus networks or {"type": "pod"} |
| OVA | OvaForkliftInventory |
Inventory storage entries whose name contains the VM name; mapping returns storage id |
Inventory VM network ID is matched to an inventory network name |
Two adapter details are especially practical:
- RHV uses provider-side template queries for pre-clone network discovery, but inventory for final map generation.
- OpenShift storage resolution is not just “whatever inventory says.” The adapter actually follows data volumes to PVCs and reads the live
storageClassName.
How Source Network Mappings Are Resolved
Network mapping is a two-step process.
First, the suite decides how many destination networks it needs. It does that in the multus_network_name fixture by calling source_provider.get_vm_or_template_networks(). This is why RHV can use template networks before clones exist.
Second, once the cloned or prepared source VM has been synced into Forklift inventory, the actual NetworkMap payload is built from inventory data.
The core rule lives in utilities/utils.py:
for index, network in enumerate(source_provider_inventory.vms_networks_mappings(vms=vms)):
if pod_only or index == 0:
_destination = _destination_pod
else:
multus_network_name_str = multus_network_name["name"]
multus_namespace = multus_network_name["namespace"]
nad_name = f"{multus_network_name_str}-{multus_counter}"
_destination = {
"name": nad_name,
"namespace": multus_namespace,
"type": "multus",
}
multus_counter += 1
network_map_list.append({
"destination": _destination,
"source": network,
})
What that means in plain language:
- The first source network is always mapped to the destination pod network.
- Every additional source network is mapped to a generated Multus NAD.
- Those NADs are named from a base name plus a numeric suffix:
{base}-1,{base}-2, and so on. - If the plan config sets
multus_namespace, the NADs are created there instead of in the main target namespace.
Warning: Network mapping is order-based. The first source network returned by inventory becomes the pod network, so inventory ordering matters for multi-NIC VMs.
Tip: If a source VM has
Nnetworks, the suite createsN - 1NADs, because the first network is reserved for the destination pod network.
How Source Storage Mappings Are Resolved
The storage path is simpler than the network path in standard migrations: the helper trusts the selected inventory adapter to tell it which source storage objects matter, then it adds the destination storage class.
The standard branch of get_storage_migration_map() looks like this:
storage_migration_map = source_provider_inventory.vms_storages_mappings(vms=vms)
for storage in storage_migration_map:
storage_map_list.append({
"destination": {"storageClass": target_storage_class},
"source": storage,
})
A few practical consequences follow from that:
- The destination storage class is
py_config["storage_class"]unless the caller passes an explicitstorage_classargument. - The meaning of the source side is provider-specific. On vSphere it is a datastore. On RHV it is a storage domain. On OpenStack it is a volume type. On OpenShift it is a storage class. On OVA it is a storage ID.
- If the adapter returns the wrong source identifier, the
StorageMapwill be wrong even if the direct provider SDK can see the disks just fine.
vSphere Copy-Offload
Copy-offload is the main exception to inventory-derived storage mapping. In copy-offload mode, get_storage_migration_map() can build the source side from explicit datastore IDs instead of asking inventory.
if datastore_id and offload_plugin_config:
datastores_to_map = [datastore_id]
if secondary_datastore_id:
datastores_to_map.append(secondary_datastore_id)
for ds_id in datastores_to_map:
destination_config = {
"storageClass": target_storage_class,
}
if access_mode:
destination_config["accessMode"] = access_mode
if volume_mode:
destination_config["volumeMode"] = volume_mode
storage_map_list.append({
"destination": destination_config,
"source": {"id": ds_id},
"offloadPlugin": offload_plugin_config,
})
else:
storage_migration_map = source_provider_inventory.vms_storages_mappings(vms=vms)
for storage in storage_migration_map:
storage_map_list.append({
"destination": {"storageClass": target_storage_class},
"source": storage,
})
The repo validates copy-offload prerequisites in conftest.py:
required_credentials = ["storage_hostname", "storage_username", "storage_password"]
required_params = ["storage_vendor_product", "datastore_id"]
In practice, the copy-offload flow expects:
storage_vendor_productdatastore_idstorage_hostnamestorage_usernamestorage_password
Optional extensions used by the code include:
secondary_datastore_idnon_xcopy_datastore_idesxi_clone_methodesxi_hostesxi_useresxi_password
The storage secret builder also knows vendor-specific extras. Depending on storage_vendor_product, the repo may look for keys such as ontap_svm, vantara_storage_id, vantara_storage_port, vantara_hostgroup_id_list, pure_cluster_prefix, powerflex_system_id, or powermax_symmetrix_id.
The test suite builds the offload plugin config like this:
offload_plugin_config = {
"vsphereXcopyConfig": {
"secretRef": copyoffload_storage_secret.name,
"storageVendorProduct": storage_vendor_product,
}
}
When copyoffload=True, create_plan_resource() also forces pvc_name_template to "pvc" because the volume-populator path expects predictable PVC naming.
Warning: Copy-offload in this repository is a vSphere-specific path. The validation fixture explicitly fails if the selected source provider is not vSphere.
Tip: Copy-offload credentials can come from environment variables as well as
.providers.json. The code looks for names such asCOPYOFFLOAD_STORAGE_HOSTNAME,COPYOFFLOAD_STORAGE_USERNAME, andCOPYOFFLOAD_STORAGE_PASSWORD, plus vendor-specificCOPYOFFLOAD_<FIELD>overrides.
Inventory IDs And PVC Naming
Forklift inventory does two more important jobs after maps are built.
First, it provides the VM IDs that go into the migration Plan:
def populate_vm_ids(plan: dict[str, Any], inventory: ForkliftInventory) -> None:
if not isinstance(plan, dict) or not isinstance(plan.get("virtual_machines"), list):
raise ValueError("plan must contain 'virtual_machines' list")
for vm in plan["virtual_machines"]:
vm_name = vm["name"]
vm_data = inventory.get_vm(vm_name)
vm["id"] = vm_data["id"]
Second, it can supply disk filenames for PVC-template validation. In utilities/post_migration.py, the {{.FileName}} wildcard is resolved from Forklift inventory disk file paths, not from the direct provider SDK. That validation path is only enabled for vSphere sources.
Tip: If you use
pvc_name_templatewith{{.FileName}}, you are depending on inventory disk metadata. The suite sorts vSphere source disks by(controller_key, unit_number)before it renders expected PVC names.
Real Test Config Examples
These are exact snippets from tests/tests_config/config.py. They are useful because they show the real shapes the repository already exercises.
Comprehensive Warm Migration Example
This example combines custom VM namespace placement, cross-namespace Multus, static IP preservation, and a PVC naming template based on inventory file names.
"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,
"vm_target_namespace": "custom-vm-namespace",
"multus_namespace": "default", # Cross-namespace NAD access
"pvc_name_template": '{{ .FileName | trimSuffix ".vmdk" | replace "_" "-" }}-{{.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,
}
]
}
},
},
Mixed-Datastore Copy-Offload Example
This example shows a vSphere copy-offload plan where an added disk is intentionally placed on a non-XCOPY datastore.
"test_copyoffload_mixed_datastore_migration": {
"virtual_machines": [
{
"name": "xcopy-template-test",
"source_vm_power": "off",
"guest_agent": True,
"clone": True,
"disk_type": "thin",
"add_disks": [
{
"size_gb": 30,
"provision_type": "thin",
"datastore_id": "non_xcopy_datastore_id",
},
],
},
],
"warm_migration": False,
"copyoffload": True,
},
Warning: Symbolic datastore values such as
secondary_datastore_idandnon_xcopy_datastore_idare not global magic strings. The vSphere provider resolves them from the selected provider'scopyoffloadconfiguration.
Once you know which data comes from the direct provider class and which data comes from Forklift inventory, the rest of the repository becomes much easier to predict. Provider classes explain how source VMs are created and inspected. Inventory adapters explain how MTV sees those VMs. The map helpers then turn that inventory view into the exact StorageMap, NetworkMap, and Plan payloads that the migration uses.