fix: OVA import — scsi bus, boot order, NIC model from OVF, metadata verification

Locally verified against PVE 9.1 with Ubuntu 24.04 cloud OVA:

- Use scsi bus (not sata) for imported disks, matching PVE UI behavior
- Set boot=order=scsi0 for first disk
- Parse NIC ResourceSubType from OVF (VmxNet3, E1000, E1000e, etc.)
  and map to PVE network model instead of hardcoding virtio
- Add MapNicModel() to OvfMetadata with fallback to e1000 for unknown types
- Integration test verifies name, cores, memory, ostype, scsi0, net0, boot

qmpstatus fix (from previous local testing):
- WaitForStatusTransition polls status/current endpoint for qmpstatus
- PVE reports status=running but qmpstatus=paused for suspended VMs
- SDN subnet ID uses zone prefix: {zone}-{ip}-{prefix}

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-20 15:56:03 -05:00
parent 04af2dce37
commit 0fc15d74ee
3 changed files with 58 additions and 7 deletions
+33 -1
View File
@@ -28,6 +28,9 @@ namespace PSProxmoxVE.Core.Models.Vms
/// <summary>The connection name (e.g. "VM Network", "bridged").</summary> /// <summary>The connection name (e.g. "VM Network", "bridged").</summary>
public string ConnectionName { get; set; } = string.Empty; public string ConnectionName { get; set; } = string.Empty;
/// <summary>The OVF ResourceSubType (e.g. "E1000", "vmxnet3", "VirtualE1000e").</summary>
public string ResourceSubType { get; set; } = string.Empty;
} }
/// <summary> /// <summary>
@@ -227,10 +230,12 @@ namespace PSProxmoxVE.Core.Models.Vms
?? item.SelectSingleNode("rasd:Caption", nsm)?.InnerText ?? item.SelectSingleNode("rasd:Caption", nsm)?.InnerText
?? "Network adapter"; ?? "Network adapter";
var connection = item.SelectSingleNode("rasd:Connection", nsm)?.InnerText ?? string.Empty; var connection = item.SelectSingleNode("rasd:Connection", nsm)?.InnerText ?? string.Empty;
var nicSubType = item.SelectSingleNode("rasd:ResourceSubType", nsm)?.InnerText ?? string.Empty;
metadata.NetworkAdapters.Add(new OvfNetworkAdapter metadata.NetworkAdapters.Add(new OvfNetworkAdapter
{ {
AdapterName = adapterName, AdapterName = adapterName,
ConnectionName = connection ConnectionName = connection,
ResourceSubType = nicSubType
}); });
break; break;
} }
@@ -323,6 +328,33 @@ namespace PSProxmoxVE.Core.Models.Vms
return (int)value; return (int)value;
} }
/// <summary>
/// Maps an OVF ResourceSubType for a NIC to a PVE network model string.
/// </summary>
public static string MapNicModel(string resourceSubType)
{
if (string.IsNullOrEmpty(resourceSubType))
return "virtio";
var lower = resourceSubType.ToLowerInvariant();
if (lower.Contains("vmxnet3"))
return "vmxnet3";
if (lower.Contains("e1000e") || lower.Contains("virtuale1000e"))
return "e1000e";
if (lower.Contains("e1000") || lower.Contains("virtuale1000"))
return "e1000";
if (lower.Contains("vmxnet2") || lower.Contains("vmxnet"))
return "vmxnet3"; // PVE doesn't support vmxnet2, use vmxnet3
if (lower.Contains("pcnet"))
return "e1000"; // pcnet not supported on PVE, fallback to e1000
if (lower.Contains("virtio"))
return "virtio";
// Unknown model — default to e1000 (widely compatible, no driver install needed)
return "e1000";
}
private static string MapOsType(string osDescription) private static string MapOsType(string osDescription)
{ {
if (string.IsNullOrEmpty(osDescription)) if (string.IsNullOrEmpty(osDescription))
@@ -223,20 +223,30 @@ namespace PSProxmoxVE.Cmdlets.Vms
if (!string.IsNullOrEmpty(metadata.OsTypeHint) && metadata.OsTypeHint != "other") if (!string.IsNullOrEmpty(metadata.OsTypeHint) && metadata.OsTypeHint != "other")
vmConfig["ostype"] = metadata.OsTypeHint; vmConfig["ostype"] = metadata.OsTypeHint;
// Add disk import-from parameters // Add disk import-from parameters (use scsi bus like PVE UI)
string? firstDisk = null;
for (int i = 0; i < metadata.Disks.Count; i++) for (int i = 0; i < metadata.Disks.Count; i++)
{ {
var disk = metadata.Disks[i]; var disk = metadata.Disks[i];
var diskSlot = $"{disk.BusType}{i}"; var diskSlot = $"scsi{i}";
var importFrom = $"{Storage}:import/{fileName}/{disk.FileName}"; var importFrom = $"{Storage}:import/{fileName}/{disk.FileName}";
vmConfig[diskSlot] = $"{TargetStorage}:0,import-from={importFrom}"; vmConfig[diskSlot] = $"{TargetStorage}:0,import-from={importFrom}";
firstDisk ??= diskSlot;
WriteVerbose($" Disk {diskSlot}: import-from={importFrom} -> {TargetStorage}"); WriteVerbose($" Disk {diskSlot}: import-from={importFrom} -> {TargetStorage}");
} }
// Add network adapters // Set boot order to the first disk
if (firstDisk != null)
vmConfig["boot"] = $"order={firstDisk}";
// Set sockets (PVE default)
vmConfig["sockets"] = 1;
// Add network adapters with model from OVF
for (int i = 0; i < metadata.NetworkAdapters.Count; i++) for (int i = 0; i < metadata.NetworkAdapters.Count; i++)
{ {
vmConfig[$"net{i}"] = "virtio,bridge=vmbr0"; var nicModel = OvfMetadata.MapNicModel(metadata.NetworkAdapters[i].ResourceSubType);
vmConfig[$"net{i}"] = $"{nicModel},bridge=vmbr0";
} }
var createTask = vmService.CreateVm(session, Node, vmConfig); var createTask = vmService.CreateVm(session, Node, vmConfig);
@@ -1214,7 +1214,7 @@ Describe 'Integration Tests' -Tag 'Integration' {
$metadata.Disks.Count | Should -BeGreaterThan 0 $metadata.Disks.Count | Should -BeGreaterThan 0
} }
It 'Should import OVA as a VM (Import-PveOva)' { It 'Should import OVA as a VM with correct metadata (Import-PveOva)' {
if (Skip-IfNoTarget) { return } if (Skip-IfNoTarget) { return }
if (-not $script:OvaPath -or -not (Test-Path $script:OvaPath)) { if (-not $script:OvaPath -or -not (Test-Path $script:OvaPath)) {
Set-ItResult -Skipped -Because 'PVETEST_OVA_PATH not set or file not found' Set-ItResult -Skipped -Because 'PVETEST_OVA_PATH not set or file not found'
@@ -1228,9 +1228,18 @@ Describe 'Integration Tests' -Tag 'Integration' {
$vm | Should -Not -BeNullOrEmpty $vm | Should -Not -BeNullOrEmpty
$script:CreatedVmIds.Add($vm.VmId) $script:CreatedVmIds.Add($vm.VmId)
# Verify the VM exists # Verify VM exists with correct name
$found = Get-PveVm -Node $script:Node -Name 'pester-ova-vm' | Select-Object -First 1 $found = Get-PveVm -Node $script:Node -Name 'pester-ova-vm' | Select-Object -First 1
$found | Should -Not -BeNullOrEmpty $found | Should -Not -BeNullOrEmpty
$found.Name | Should -Be 'pester-ova-vm'
# Verify config matches OVF metadata
$config = Get-PveVmConfig -Node $script:Node -VmId $vm.VmId
$config.Memory | Should -BeGreaterThan 0
$config.Cores | Should -BeGreaterThan 0
$config.OsType | Should -Be 'l26'
$config.Scsi0 | Should -Not -BeNullOrEmpty -Because 'disk should be imported to scsi0'
$config.Net0 | Should -Not -BeNullOrEmpty -Because 'network adapter should be configured'
} }
It 'Should start the OVA-imported VM' { It 'Should start the OVA-imported VM' {