mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-06 12:09:01 +00:00
refactor: move OVA/OVF archive I/O out of the OvfMetadata model into OvfReader (#222)
OvfMetadata.cs opened files and drove SharpCompress.Readers.ReaderFactory inside a Models/ type, the only SharpCompress consumer in the codebase. Move TAR extraction and OVF XML parsing to a new static PSProxmoxVE.Core.Utilities.OvfReader (ReadOva, internal ParseOvf) and leave OvfMetadata as a data type: properties plus the pure MapNicModel helper. The move is behavior-preserving: bus-type inference, href validation, DtdProcessing.Prohibit, and disk-slot handling are unchanged. OvfMetadata.FromOva is removed rather than kept as an [Obsolete] forwarder — PowerShell ignores ObsoleteAttribute at invocation, the only other caller is a non-code-owned integration test, and ADR 0028's deprecation-path bar is for parameter-binding surface, not a Core static method reached by fully-qualified type name. The integration test now calls OvfReader.ReadOva directly. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
142f18af1d
commit
22b65ffd3d
@@ -1,9 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
|
||||
namespace PSProxmoxVE.Core.Models.Vms
|
||||
{
|
||||
@@ -57,294 +52,6 @@ namespace PSProxmoxVE.Core.Models.Vms
|
||||
/// <summary>OS type hint from the OVF OperatingSystemSection.</summary>
|
||||
public string OsTypeHint { get; set; } = string.Empty;
|
||||
|
||||
// OVF namespace constants
|
||||
private const string OvfNs = "http://schemas.dmtf.org/ovf/envelope/1";
|
||||
private const string RasdNs = "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData";
|
||||
private const string VssdNs = "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData";
|
||||
|
||||
// ovf:href is used as a path segment in a PVE property string (import-from=storage:import/ova/href);
|
||||
// PVE property strings are comma-separated, so ',' and any path separator must be rejected.
|
||||
// \A/\z (not ^/$) so a trailing newline cannot sneak past the anchor under .NET's default regex options.
|
||||
private static readonly Regex ValidHrefPattern = new Regex(@"\A[A-Za-z0-9._-]+\z", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Parses an OVA file (TAR archive) and extracts OVF metadata.
|
||||
/// </summary>
|
||||
/// <param name="ovaPath">Path to the OVA file.</param>
|
||||
/// <returns>Parsed OVF metadata.</returns>
|
||||
public static OvfMetadata FromOva(string ovaPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ovaPath))
|
||||
throw new ArgumentException("OVA path must not be null or empty.", nameof(ovaPath));
|
||||
if (!File.Exists(ovaPath))
|
||||
throw new FileNotFoundException("OVA file not found.", ovaPath);
|
||||
|
||||
var ovfXml = ExtractOvfFromTar(ovaPath);
|
||||
if (ovfXml == null)
|
||||
throw new InvalidOperationException("No .ovf file found inside the OVA archive.");
|
||||
|
||||
return ParseOvfXml(ovfXml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the .ovf XML content from a TAR archive using SharpCompress.
|
||||
/// </summary>
|
||||
private static string? ExtractOvfFromTar(string tarPath)
|
||||
{
|
||||
using var stream = File.OpenRead(tarPath);
|
||||
using var reader = SharpCompress.Readers.ReaderFactory.OpenReader(stream, new SharpCompress.Readers.ReaderOptions());
|
||||
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory &&
|
||||
reader.Entry.Key != null &&
|
||||
reader.Entry.Key.EndsWith(".ovf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
using var sr = new StreamReader(entryStream, Encoding.UTF8);
|
||||
return sr.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses OVF XML and extracts VM metadata.
|
||||
/// </summary>
|
||||
internal static OvfMetadata ParseOvfXml(string xml)
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
var readerSettings = new XmlReaderSettings
|
||||
{
|
||||
DtdProcessing = DtdProcessing.Prohibit,
|
||||
XmlResolver = null
|
||||
};
|
||||
using (var stringReader = new StringReader(xml))
|
||||
using (var xmlReader = XmlReader.Create(stringReader, readerSettings))
|
||||
{
|
||||
doc.Load(xmlReader);
|
||||
}
|
||||
|
||||
var nsm = new XmlNamespaceManager(doc.NameTable);
|
||||
nsm.AddNamespace("ovf", OvfNs);
|
||||
nsm.AddNamespace("rasd", RasdNs);
|
||||
nsm.AddNamespace("vssd", VssdNs);
|
||||
|
||||
var metadata = new OvfMetadata();
|
||||
|
||||
// Extract VM name from VirtualSystem
|
||||
var vsNode = doc.SelectSingleNode("//ovf:VirtualSystem", nsm);
|
||||
if (vsNode != null)
|
||||
{
|
||||
// Try ovf:id attribute first, then Name element
|
||||
var idAttr = vsNode.Attributes?["ovf:id"];
|
||||
if (idAttr != null && !string.IsNullOrEmpty(idAttr.Value))
|
||||
{
|
||||
metadata.Name = idAttr.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Try VirtualSystemIdentifier from VirtualSystemSettingData
|
||||
var vsId = doc.SelectSingleNode("//vssd:VirtualSystemIdentifier", nsm);
|
||||
if (vsId != null && !string.IsNullOrEmpty(vsId.InnerText))
|
||||
{
|
||||
metadata.Name = vsId.InnerText;
|
||||
}
|
||||
|
||||
// Extract OS type hint from OperatingSystemSection
|
||||
var osSection = doc.SelectSingleNode("//ovf:OperatingSystemSection", nsm);
|
||||
if (osSection != null)
|
||||
{
|
||||
var osTypeAttr = osSection.Attributes?["ovf:id"];
|
||||
var description = osSection.SelectSingleNode("ovf:Description", nsm);
|
||||
var osDesc = description?.InnerText ?? osTypeAttr?.Value ?? string.Empty;
|
||||
metadata.OsTypeHint = MapOsType(osDesc);
|
||||
}
|
||||
|
||||
// Build file reference map: fileRef -> fileName
|
||||
var fileRefs = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var fileNodes = doc.SelectNodes("//ovf:References/ovf:File", nsm);
|
||||
if (fileNodes != null)
|
||||
{
|
||||
foreach (XmlNode fileNode in fileNodes)
|
||||
{
|
||||
var id = fileNode.Attributes?["ovf:id"]?.Value;
|
||||
var href = fileNode.Attributes?["ovf:href"]?.Value;
|
||||
if (id != null && href != null)
|
||||
{
|
||||
if (!ValidHrefPattern.IsMatch(href) || href == "." || href == "..")
|
||||
throw new InvalidDataException($"OVF descriptor references a disallowed file name: '{href}'.");
|
||||
fileRefs[id] = href;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build disk reference map: diskId -> fileRef
|
||||
var diskFileMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var diskNodes = doc.SelectNodes("//ovf:DiskSection/ovf:Disk", nsm);
|
||||
if (diskNodes != null)
|
||||
{
|
||||
foreach (XmlNode diskNode in diskNodes)
|
||||
{
|
||||
var diskId = diskNode.Attributes?["ovf:diskId"]?.Value;
|
||||
var fileRef = diskNode.Attributes?["ovf:fileRef"]?.Value;
|
||||
if (diskId != null && fileRef != null)
|
||||
{
|
||||
diskFileMap[diskId] = fileRef;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse hardware items
|
||||
var items = doc.SelectNodes("//ovf:VirtualHardwareSection/ovf:Item", nsm);
|
||||
if (items != null)
|
||||
{
|
||||
foreach (XmlNode item in items)
|
||||
{
|
||||
var resourceTypeNode = item.SelectSingleNode("rasd:ResourceType", nsm);
|
||||
if (resourceTypeNode == null) continue;
|
||||
|
||||
if (!int.TryParse(resourceTypeNode.InnerText.Trim(), out int resourceType))
|
||||
continue;
|
||||
|
||||
switch (resourceType)
|
||||
{
|
||||
case 3: // Processor
|
||||
var vcpuNode = item.SelectSingleNode("rasd:VirtualQuantity", nsm);
|
||||
if (vcpuNode != null && int.TryParse(vcpuNode.InnerText.Trim(), out int vcpus))
|
||||
metadata.CpuCount = vcpus;
|
||||
break;
|
||||
|
||||
case 4: // Memory
|
||||
var memNode = item.SelectSingleNode("rasd:VirtualQuantity", nsm);
|
||||
var unitsNode = item.SelectSingleNode("rasd:AllocationUnits", nsm);
|
||||
if (memNode != null && long.TryParse(memNode.InnerText.Trim(), out long memVal))
|
||||
{
|
||||
var units = unitsNode?.InnerText?.Trim() ?? "byte * 2^20";
|
||||
metadata.MemoryMB = ConvertToMB(memVal, units);
|
||||
}
|
||||
break;
|
||||
|
||||
case 5: // IDE Controller
|
||||
case 6: // Parallel SCSI HBA
|
||||
case 20: // Other storage device (VMware's SATA AHCI controller)
|
||||
// Controllers themselves don't produce disk entries; skip.
|
||||
break;
|
||||
|
||||
case 17: // Disk Drive
|
||||
var diskRef = ExtractDiskReference(item, nsm, diskFileMap, fileRefs);
|
||||
if (diskRef != null)
|
||||
{
|
||||
// Determine bus type from parent controller
|
||||
diskRef.BusType = DetermineBusType(item, items, nsm);
|
||||
metadata.Disks.Add(diskRef);
|
||||
}
|
||||
break;
|
||||
|
||||
case 10: // Ethernet Adapter
|
||||
var adapterName = item.SelectSingleNode("rasd:ElementName", nsm)?.InnerText
|
||||
?? item.SelectSingleNode("rasd:Caption", nsm)?.InnerText
|
||||
?? "Network adapter";
|
||||
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
|
||||
{
|
||||
AdapterName = adapterName,
|
||||
ConnectionName = connection,
|
||||
ResourceSubType = nicSubType
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private static OvfDiskReference? ExtractDiskReference(
|
||||
XmlNode item,
|
||||
XmlNamespaceManager nsm,
|
||||
Dictionary<string, string> diskFileMap,
|
||||
Dictionary<string, string> fileRefs)
|
||||
{
|
||||
// The HostResource element typically contains a reference like "ovf:/disk/vmdisk1"
|
||||
var hostResource = item.SelectSingleNode("rasd:HostResource", nsm)?.InnerText ?? string.Empty;
|
||||
string? diskId = null;
|
||||
|
||||
if (hostResource.Contains("/disk/"))
|
||||
{
|
||||
var idx = hostResource.LastIndexOf("/disk/", StringComparison.Ordinal);
|
||||
diskId = hostResource.Substring(idx + 6);
|
||||
}
|
||||
else if (hostResource.Contains("disk/"))
|
||||
{
|
||||
var idx = hostResource.LastIndexOf("disk/", StringComparison.Ordinal);
|
||||
diskId = hostResource.Substring(idx + 5);
|
||||
}
|
||||
|
||||
if (diskId != null && diskFileMap.TryGetValue(diskId, out var fileRef) && fileRefs.TryGetValue(fileRef, out var fileName))
|
||||
{
|
||||
return new OvfDiskReference { FileName = fileName };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string DetermineBusType(XmlNode diskItem, XmlNodeList allItems, XmlNamespaceManager nsm)
|
||||
{
|
||||
// Look at the Parent element to find which controller this disk is attached to
|
||||
var parentNode = diskItem.SelectSingleNode("rasd:Parent", nsm);
|
||||
if (parentNode == null)
|
||||
return "scsi"; // default
|
||||
|
||||
var parentId = parentNode.InnerText.Trim();
|
||||
|
||||
foreach (XmlNode item in allItems)
|
||||
{
|
||||
var instanceId = item.SelectSingleNode("rasd:InstanceID", nsm)?.InnerText?.Trim();
|
||||
if (instanceId != parentId) continue;
|
||||
|
||||
var resourceTypeNode = item.SelectSingleNode("rasd:ResourceType", nsm);
|
||||
if (resourceTypeNode == null) continue;
|
||||
|
||||
if (int.TryParse(resourceTypeNode.InnerText.Trim(), out int rt))
|
||||
{
|
||||
switch (rt)
|
||||
{
|
||||
case 5: return "ide";
|
||||
case 6: return "scsi";
|
||||
case 20: return "sata";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return "scsi"; // default fallback
|
||||
}
|
||||
|
||||
private static int ConvertToMB(long value, string allocationUnits)
|
||||
{
|
||||
// Common OVF allocation units:
|
||||
// "byte * 2^20" = MiB
|
||||
// "byte * 2^30" = GiB
|
||||
// "byte * 2^10" = KiB
|
||||
// "MegaBytes" or "MB"
|
||||
var lower = allocationUnits.ToLowerInvariant();
|
||||
|
||||
if (lower.Contains("2^30") || lower.Contains("gib") || lower.Contains("gigabyte"))
|
||||
return (int)(value * 1024);
|
||||
if (lower.Contains("2^20") || lower.Contains("mib") || lower.Contains("megabyte") || lower.Contains("mb"))
|
||||
return (int)value;
|
||||
if (lower.Contains("2^10") || lower.Contains("kib") || lower.Contains("kilobyte") || lower.Contains("kb"))
|
||||
return (int)(value / 1024);
|
||||
if (lower.Contains("byte"))
|
||||
return (int)(value / (1024 * 1024));
|
||||
|
||||
// Default: assume MiB
|
||||
return (int)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an OVF ResourceSubType for a NIC to a PVE network model string.
|
||||
/// </summary>
|
||||
@@ -371,58 +78,5 @@ namespace PSProxmoxVE.Core.Models.Vms
|
||||
// Unknown model — default to e1000 (widely compatible, no driver install needed)
|
||||
return "e1000";
|
||||
}
|
||||
|
||||
private static string MapOsType(string osDescription)
|
||||
{
|
||||
if (string.IsNullOrEmpty(osDescription))
|
||||
return "other";
|
||||
|
||||
var lower = osDescription.ToLowerInvariant();
|
||||
|
||||
// Windows variants
|
||||
if (lower.Contains("windows 11") || lower.Contains("win11"))
|
||||
return "win11";
|
||||
if (lower.Contains("windows 10") || lower.Contains("win10"))
|
||||
return "win10";
|
||||
if (lower.Contains("windows server 2022") || lower.Contains("2022"))
|
||||
return "win11";
|
||||
if (lower.Contains("windows server 2019") || lower.Contains("2019"))
|
||||
return "win10";
|
||||
if (lower.Contains("windows server 2016") || lower.Contains("2016"))
|
||||
return "win10";
|
||||
if (lower.Contains("windows 8") || lower.Contains("win8"))
|
||||
return "win8";
|
||||
if (lower.Contains("windows 7") || lower.Contains("win7"))
|
||||
return "win7";
|
||||
if (lower.Contains("windows"))
|
||||
return "win10";
|
||||
|
||||
// Linux variants
|
||||
if (lower.Contains("linux") || lower.Contains("ubuntu") || lower.Contains("debian") ||
|
||||
lower.Contains("centos") || lower.Contains("rhel") || lower.Contains("red hat") ||
|
||||
lower.Contains("fedora") || lower.Contains("suse") || lower.Contains("alma") ||
|
||||
lower.Contains("rocky"))
|
||||
return "l26";
|
||||
|
||||
// FreeBSD
|
||||
if (lower.Contains("freebsd"))
|
||||
return "l26";
|
||||
|
||||
// Solaris
|
||||
if (lower.Contains("solaris"))
|
||||
return "solaris";
|
||||
|
||||
// Try numeric OVF OS ID
|
||||
if (int.TryParse(osDescription, out int osId))
|
||||
{
|
||||
// Common CIM OS IDs
|
||||
if (osId >= 56 && osId <= 70) return "win10"; // Various Windows
|
||||
if (osId >= 93 && osId <= 113) return "l26"; // Various Linux
|
||||
if (osId == 36) return "l26"; // Linux
|
||||
if (osId == 101 || osId == 106) return "l26"; // Linux 64-bit
|
||||
}
|
||||
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads OVA archives and OVF descriptors into <see cref="OvfMetadata"/>.
|
||||
/// </summary>
|
||||
public static class OvfReader
|
||||
{
|
||||
private const string OvfNs = "http://schemas.dmtf.org/ovf/envelope/1";
|
||||
private const string RasdNs = "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData";
|
||||
private const string VssdNs = "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData";
|
||||
|
||||
// ovf:href is used as a path segment in a PVE property string (import-from=storage:import/ova/href);
|
||||
// PVE property strings are comma-separated, so ',' and any path separator must be rejected.
|
||||
// \A/\z (not ^/$) so a trailing newline cannot sneak past the anchor under .NET's default regex options.
|
||||
private static readonly Regex ValidHrefPattern = new Regex(@"\A[A-Za-z0-9._-]+\z", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Parses an OVA file (TAR archive) and extracts OVF metadata.
|
||||
/// </summary>
|
||||
/// <param name="ovaPath">Path to the OVA file.</param>
|
||||
/// <returns>Parsed OVF metadata.</returns>
|
||||
public static OvfMetadata ReadOva(string ovaPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ovaPath))
|
||||
throw new ArgumentException("OVA path must not be null or empty.", nameof(ovaPath));
|
||||
if (!File.Exists(ovaPath))
|
||||
throw new FileNotFoundException("OVA file not found.", ovaPath);
|
||||
|
||||
var ovfXml = ExtractOvfFromTar(ovaPath);
|
||||
if (ovfXml == null)
|
||||
throw new InvalidOperationException("No .ovf file found inside the OVA archive.");
|
||||
|
||||
return ParseOvf(ovfXml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the .ovf XML content from a TAR archive using SharpCompress.
|
||||
/// </summary>
|
||||
private static string? ExtractOvfFromTar(string tarPath)
|
||||
{
|
||||
using var stream = File.OpenRead(tarPath);
|
||||
using var reader = SharpCompress.Readers.ReaderFactory.OpenReader(stream, new SharpCompress.Readers.ReaderOptions());
|
||||
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory &&
|
||||
reader.Entry.Key != null &&
|
||||
reader.Entry.Key.EndsWith(".ovf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
using var sr = new StreamReader(entryStream, Encoding.UTF8);
|
||||
return sr.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses OVF XML and extracts VM metadata.
|
||||
/// </summary>
|
||||
internal static OvfMetadata ParseOvf(string xml)
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
var readerSettings = new XmlReaderSettings
|
||||
{
|
||||
DtdProcessing = DtdProcessing.Prohibit,
|
||||
XmlResolver = null
|
||||
};
|
||||
using (var stringReader = new StringReader(xml))
|
||||
using (var xmlReader = XmlReader.Create(stringReader, readerSettings))
|
||||
{
|
||||
doc.Load(xmlReader);
|
||||
}
|
||||
|
||||
var nsm = new XmlNamespaceManager(doc.NameTable);
|
||||
nsm.AddNamespace("ovf", OvfNs);
|
||||
nsm.AddNamespace("rasd", RasdNs);
|
||||
nsm.AddNamespace("vssd", VssdNs);
|
||||
|
||||
var metadata = new OvfMetadata();
|
||||
|
||||
// Extract VM name from VirtualSystem
|
||||
var vsNode = doc.SelectSingleNode("//ovf:VirtualSystem", nsm);
|
||||
if (vsNode != null)
|
||||
{
|
||||
// Try ovf:id attribute first, then Name element
|
||||
var idAttr = vsNode.Attributes?["ovf:id"];
|
||||
if (idAttr != null && !string.IsNullOrEmpty(idAttr.Value))
|
||||
{
|
||||
metadata.Name = idAttr.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Try VirtualSystemIdentifier from VirtualSystemSettingData
|
||||
var vsId = doc.SelectSingleNode("//vssd:VirtualSystemIdentifier", nsm);
|
||||
if (vsId != null && !string.IsNullOrEmpty(vsId.InnerText))
|
||||
{
|
||||
metadata.Name = vsId.InnerText;
|
||||
}
|
||||
|
||||
// Extract OS type hint from OperatingSystemSection
|
||||
var osSection = doc.SelectSingleNode("//ovf:OperatingSystemSection", nsm);
|
||||
if (osSection != null)
|
||||
{
|
||||
var osTypeAttr = osSection.Attributes?["ovf:id"];
|
||||
var description = osSection.SelectSingleNode("ovf:Description", nsm);
|
||||
var osDesc = description?.InnerText ?? osTypeAttr?.Value ?? string.Empty;
|
||||
metadata.OsTypeHint = MapOsType(osDesc);
|
||||
}
|
||||
|
||||
// Build file reference map: fileRef -> fileName
|
||||
var fileRefs = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var fileNodes = doc.SelectNodes("//ovf:References/ovf:File", nsm);
|
||||
if (fileNodes != null)
|
||||
{
|
||||
foreach (XmlNode fileNode in fileNodes)
|
||||
{
|
||||
var id = fileNode.Attributes?["ovf:id"]?.Value;
|
||||
var href = fileNode.Attributes?["ovf:href"]?.Value;
|
||||
if (id != null && href != null)
|
||||
{
|
||||
if (!ValidHrefPattern.IsMatch(href) || href == "." || href == "..")
|
||||
throw new InvalidDataException($"OVF descriptor references a disallowed file name: '{href}'.");
|
||||
fileRefs[id] = href;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build disk reference map: diskId -> fileRef
|
||||
var diskFileMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var diskNodes = doc.SelectNodes("//ovf:DiskSection/ovf:Disk", nsm);
|
||||
if (diskNodes != null)
|
||||
{
|
||||
foreach (XmlNode diskNode in diskNodes)
|
||||
{
|
||||
var diskId = diskNode.Attributes?["ovf:diskId"]?.Value;
|
||||
var fileRef = diskNode.Attributes?["ovf:fileRef"]?.Value;
|
||||
if (diskId != null && fileRef != null)
|
||||
{
|
||||
diskFileMap[diskId] = fileRef;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse hardware items
|
||||
var items = doc.SelectNodes("//ovf:VirtualHardwareSection/ovf:Item", nsm);
|
||||
if (items != null)
|
||||
{
|
||||
foreach (XmlNode item in items)
|
||||
{
|
||||
var resourceTypeNode = item.SelectSingleNode("rasd:ResourceType", nsm);
|
||||
if (resourceTypeNode == null) continue;
|
||||
|
||||
if (!int.TryParse(resourceTypeNode.InnerText.Trim(), out int resourceType))
|
||||
continue;
|
||||
|
||||
switch (resourceType)
|
||||
{
|
||||
case 3: // Processor
|
||||
var vcpuNode = item.SelectSingleNode("rasd:VirtualQuantity", nsm);
|
||||
if (vcpuNode != null && int.TryParse(vcpuNode.InnerText.Trim(), out int vcpus))
|
||||
metadata.CpuCount = vcpus;
|
||||
break;
|
||||
|
||||
case 4: // Memory
|
||||
var memNode = item.SelectSingleNode("rasd:VirtualQuantity", nsm);
|
||||
var unitsNode = item.SelectSingleNode("rasd:AllocationUnits", nsm);
|
||||
if (memNode != null && long.TryParse(memNode.InnerText.Trim(), out long memVal))
|
||||
{
|
||||
var units = unitsNode?.InnerText?.Trim() ?? "byte * 2^20";
|
||||
metadata.MemoryMB = ConvertToMB(memVal, units);
|
||||
}
|
||||
break;
|
||||
|
||||
case 5: // IDE Controller
|
||||
case 6: // Parallel SCSI HBA
|
||||
case 20: // Other storage device (VMware's SATA AHCI controller)
|
||||
// Controllers themselves don't produce disk entries; skip.
|
||||
break;
|
||||
|
||||
case 17: // Disk Drive
|
||||
var diskRef = ExtractDiskReference(item, nsm, diskFileMap, fileRefs);
|
||||
if (diskRef != null)
|
||||
{
|
||||
// Determine bus type from parent controller
|
||||
diskRef.BusType = DetermineBusType(item, items, nsm);
|
||||
metadata.Disks.Add(diskRef);
|
||||
}
|
||||
break;
|
||||
|
||||
case 10: // Ethernet Adapter
|
||||
var adapterName = item.SelectSingleNode("rasd:ElementName", nsm)?.InnerText
|
||||
?? item.SelectSingleNode("rasd:Caption", nsm)?.InnerText
|
||||
?? "Network adapter";
|
||||
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
|
||||
{
|
||||
AdapterName = adapterName,
|
||||
ConnectionName = connection,
|
||||
ResourceSubType = nicSubType
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private static OvfDiskReference? ExtractDiskReference(
|
||||
XmlNode item,
|
||||
XmlNamespaceManager nsm,
|
||||
Dictionary<string, string> diskFileMap,
|
||||
Dictionary<string, string> fileRefs)
|
||||
{
|
||||
// The HostResource element typically contains a reference like "ovf:/disk/vmdisk1"
|
||||
var hostResource = item.SelectSingleNode("rasd:HostResource", nsm)?.InnerText ?? string.Empty;
|
||||
string? diskId = null;
|
||||
|
||||
if (hostResource.Contains("/disk/"))
|
||||
{
|
||||
var idx = hostResource.LastIndexOf("/disk/", StringComparison.Ordinal);
|
||||
diskId = hostResource.Substring(idx + 6);
|
||||
}
|
||||
else if (hostResource.Contains("disk/"))
|
||||
{
|
||||
var idx = hostResource.LastIndexOf("disk/", StringComparison.Ordinal);
|
||||
diskId = hostResource.Substring(idx + 5);
|
||||
}
|
||||
|
||||
if (diskId != null && diskFileMap.TryGetValue(diskId, out var fileRef) && fileRefs.TryGetValue(fileRef, out var fileName))
|
||||
{
|
||||
return new OvfDiskReference { FileName = fileName };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string DetermineBusType(XmlNode diskItem, XmlNodeList allItems, XmlNamespaceManager nsm)
|
||||
{
|
||||
// Look at the Parent element to find which controller this disk is attached to
|
||||
var parentNode = diskItem.SelectSingleNode("rasd:Parent", nsm);
|
||||
if (parentNode == null)
|
||||
return "scsi"; // default
|
||||
|
||||
var parentId = parentNode.InnerText.Trim();
|
||||
|
||||
foreach (XmlNode item in allItems)
|
||||
{
|
||||
var instanceId = item.SelectSingleNode("rasd:InstanceID", nsm)?.InnerText?.Trim();
|
||||
if (instanceId != parentId) continue;
|
||||
|
||||
var resourceTypeNode = item.SelectSingleNode("rasd:ResourceType", nsm);
|
||||
if (resourceTypeNode == null) continue;
|
||||
|
||||
if (int.TryParse(resourceTypeNode.InnerText.Trim(), out int rt))
|
||||
{
|
||||
switch (rt)
|
||||
{
|
||||
case 5: return "ide";
|
||||
case 6: return "scsi";
|
||||
case 20: return "sata";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return "scsi"; // default fallback
|
||||
}
|
||||
|
||||
private static int ConvertToMB(long value, string allocationUnits)
|
||||
{
|
||||
// Common OVF allocation units:
|
||||
// "byte * 2^20" = MiB
|
||||
// "byte * 2^30" = GiB
|
||||
// "byte * 2^10" = KiB
|
||||
// "MegaBytes" or "MB"
|
||||
var lower = allocationUnits.ToLowerInvariant();
|
||||
|
||||
if (lower.Contains("2^30") || lower.Contains("gib") || lower.Contains("gigabyte"))
|
||||
return (int)(value * 1024);
|
||||
if (lower.Contains("2^20") || lower.Contains("mib") || lower.Contains("megabyte") || lower.Contains("mb"))
|
||||
return (int)value;
|
||||
if (lower.Contains("2^10") || lower.Contains("kib") || lower.Contains("kilobyte") || lower.Contains("kb"))
|
||||
return (int)(value / 1024);
|
||||
if (lower.Contains("byte"))
|
||||
return (int)(value / (1024 * 1024));
|
||||
|
||||
// Default: assume MiB
|
||||
return (int)value;
|
||||
}
|
||||
|
||||
private static string MapOsType(string osDescription)
|
||||
{
|
||||
if (string.IsNullOrEmpty(osDescription))
|
||||
return "other";
|
||||
|
||||
var lower = osDescription.ToLowerInvariant();
|
||||
|
||||
// Windows variants
|
||||
if (lower.Contains("windows 11") || lower.Contains("win11"))
|
||||
return "win11";
|
||||
if (lower.Contains("windows 10") || lower.Contains("win10"))
|
||||
return "win10";
|
||||
if (lower.Contains("windows server 2022") || lower.Contains("2022"))
|
||||
return "win11";
|
||||
if (lower.Contains("windows server 2019") || lower.Contains("2019"))
|
||||
return "win10";
|
||||
if (lower.Contains("windows server 2016") || lower.Contains("2016"))
|
||||
return "win10";
|
||||
if (lower.Contains("windows 8") || lower.Contains("win8"))
|
||||
return "win8";
|
||||
if (lower.Contains("windows 7") || lower.Contains("win7"))
|
||||
return "win7";
|
||||
if (lower.Contains("windows"))
|
||||
return "win10";
|
||||
|
||||
// Linux variants
|
||||
if (lower.Contains("linux") || lower.Contains("ubuntu") || lower.Contains("debian") ||
|
||||
lower.Contains("centos") || lower.Contains("rhel") || lower.Contains("red hat") ||
|
||||
lower.Contains("fedora") || lower.Contains("suse") || lower.Contains("alma") ||
|
||||
lower.Contains("rocky"))
|
||||
return "l26";
|
||||
|
||||
// FreeBSD
|
||||
if (lower.Contains("freebsd"))
|
||||
return "l26";
|
||||
|
||||
// Solaris
|
||||
if (lower.Contains("solaris"))
|
||||
return "solaris";
|
||||
|
||||
// Try numeric OVF OS ID
|
||||
if (int.TryParse(osDescription, out int osId))
|
||||
{
|
||||
// Common CIM OS IDs
|
||||
if (osId >= 56 && osId <= 70) return "win10"; // Various Windows
|
||||
if (osId >= 93 && osId <= 113) return "l26"; // Various Linux
|
||||
if (osId == 36) return "l26"; // Linux
|
||||
if (osId == 101 || osId == 106) return "l26"; // Linux 64-bit
|
||||
}
|
||||
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
using PSProxmoxVE.Core.Utilities;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Vms
|
||||
{
|
||||
@@ -108,7 +109,7 @@ namespace PSProxmoxVE.Cmdlets.Vms
|
||||
OvfMetadata metadata;
|
||||
try
|
||||
{
|
||||
metadata = OvfMetadata.FromOva(Path);
|
||||
metadata = OvfReader.ReadOva(Path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using Xunit;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Tests.Models
|
||||
{
|
||||
public class OvfMetadataTests
|
||||
{
|
||||
private const string OvfHeader =
|
||||
"<Envelope xmlns=\"http://schemas.dmtf.org/ovf/envelope/1\" " +
|
||||
"xmlns:ovf=\"http://schemas.dmtf.org/ovf/envelope/1\" " +
|
||||
"xmlns:rasd=\"http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData\" " +
|
||||
"xmlns:vssd=\"http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData\">";
|
||||
|
||||
private static string BuildDescriptor(string href, int controllerResourceType)
|
||||
{
|
||||
return
|
||||
OvfHeader +
|
||||
"<References><File ovf:id=\"file1\" ovf:href=\"" + href + "\"/></References>" +
|
||||
"<DiskSection><Disk ovf:diskId=\"vmdisk1\" ovf:fileRef=\"file1\"/></DiskSection>" +
|
||||
"<VirtualSystem ovf:id=\"test-vm\">" +
|
||||
"<VirtualHardwareSection>" +
|
||||
"<Item>" +
|
||||
"<rasd:InstanceID>1</rasd:InstanceID>" +
|
||||
"<rasd:ResourceType>" + controllerResourceType + "</rasd:ResourceType>" +
|
||||
"</Item>" +
|
||||
"<Item>" +
|
||||
"<rasd:InstanceID>2</rasd:InstanceID>" +
|
||||
"<rasd:ResourceType>17</rasd:ResourceType>" +
|
||||
"<rasd:Parent>1</rasd:Parent>" +
|
||||
"<rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>" +
|
||||
"</Item>" +
|
||||
"</VirtualHardwareSection>" +
|
||||
"</VirtualSystem>" +
|
||||
"</Envelope>";
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvfXml_VMwareScsiController_ResourceType6_MapsToScsi()
|
||||
{
|
||||
var xml = BuildDescriptor("disk1.vmdk", 6);
|
||||
var metadata = OvfMetadata.ParseOvfXml(xml);
|
||||
|
||||
Assert.Single(metadata.Disks);
|
||||
Assert.Equal("scsi", metadata.Disks[0].BusType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvfXml_VMwareSataController_ResourceType20_MapsToSata()
|
||||
{
|
||||
var xml = BuildDescriptor("disk1.vmdk", 20);
|
||||
var metadata = OvfMetadata.ParseOvfXml(xml);
|
||||
|
||||
Assert.Single(metadata.Disks);
|
||||
Assert.Equal("sata", metadata.Disks[0].BusType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvfXml_HrefWithEmbeddedProperty_Throws()
|
||||
{
|
||||
var xml = BuildDescriptor("d.vmdk,cache=unsafe", 6);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => OvfMetadata.ParseOvfXml(xml));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvfXml_HrefWithPathTraversal_Throws()
|
||||
{
|
||||
var xml = BuildDescriptor("../x.vmdk", 6);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => OvfMetadata.ParseOvfXml(xml));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvfXml_HrefIsDotDot_Throws()
|
||||
{
|
||||
var xml = BuildDescriptor("..", 6);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => OvfMetadata.ParseOvfXml(xml));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvfXml_HrefIsDot_Throws()
|
||||
{
|
||||
var xml = BuildDescriptor(".", 6);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => OvfMetadata.ParseOvfXml(xml));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvfXml_DescriptorWithInternalEntity_Throws()
|
||||
{
|
||||
var xml =
|
||||
"<?xml version=\"1.0\"?>" +
|
||||
"<!DOCTYPE Envelope [<!ENTITY boom \"boom\">]>" +
|
||||
OvfHeader +
|
||||
"<VirtualSystem ovf:id=\"&boom;\"/>" +
|
||||
"</Envelope>";
|
||||
|
||||
Assert.ThrowsAny<XmlException>(() => OvfMetadata.ParseOvfXml(xml));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Xunit;
|
||||
using PSProxmoxVE.Core.Utilities;
|
||||
|
||||
namespace PSProxmoxVE.Core.Tests.Utilities
|
||||
{
|
||||
public class OvfReaderTests
|
||||
{
|
||||
private const string OvfHeader =
|
||||
"<Envelope xmlns=\"http://schemas.dmtf.org/ovf/envelope/1\" " +
|
||||
"xmlns:ovf=\"http://schemas.dmtf.org/ovf/envelope/1\" " +
|
||||
"xmlns:rasd=\"http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData\" " +
|
||||
"xmlns:vssd=\"http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData\">";
|
||||
|
||||
private static string BuildDescriptor(string href, int controllerResourceType)
|
||||
{
|
||||
return
|
||||
OvfHeader +
|
||||
"<References><File ovf:id=\"file1\" ovf:href=\"" + href + "\"/></References>" +
|
||||
"<DiskSection><Disk ovf:diskId=\"vmdisk1\" ovf:fileRef=\"file1\"/></DiskSection>" +
|
||||
"<VirtualSystem ovf:id=\"test-vm\">" +
|
||||
"<VirtualHardwareSection>" +
|
||||
"<Item>" +
|
||||
"<rasd:InstanceID>1</rasd:InstanceID>" +
|
||||
"<rasd:ResourceType>" + controllerResourceType + "</rasd:ResourceType>" +
|
||||
"</Item>" +
|
||||
"<Item>" +
|
||||
"<rasd:InstanceID>2</rasd:InstanceID>" +
|
||||
"<rasd:ResourceType>17</rasd:ResourceType>" +
|
||||
"<rasd:Parent>1</rasd:Parent>" +
|
||||
"<rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>" +
|
||||
"</Item>" +
|
||||
"</VirtualHardwareSection>" +
|
||||
"</VirtualSystem>" +
|
||||
"</Envelope>";
|
||||
}
|
||||
|
||||
// Minimal POSIX ustar header: 512-byte fixed-size header fields,
|
||||
// followed by the file content padded to a 512-byte boundary,
|
||||
// followed by two all-zero 512-byte end-of-archive blocks.
|
||||
private static byte[] BuildTarWithEntry(string entryName, byte[] content)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
var header = new byte[512];
|
||||
var nameBytes = Encoding.ASCII.GetBytes(entryName);
|
||||
System.Array.Copy(nameBytes, header, nameBytes.Length);
|
||||
WriteOctalField(header, 100, 8, 0x1A4); // mode 0644
|
||||
WriteOctalField(header, 108, 8, 0); // uid
|
||||
WriteOctalField(header, 116, 8, 0); // gid
|
||||
WriteOctalField(header, 124, 12, content.Length);
|
||||
WriteOctalField(header, 136, 12, 0); // mtime
|
||||
for (int i = 148; i < 156; i++) header[i] = (byte)' ';
|
||||
header[156] = (byte)'0'; // regular file
|
||||
var magic = Encoding.ASCII.GetBytes("ustar\0");
|
||||
System.Array.Copy(magic, 0, header, 257, magic.Length);
|
||||
header[263] = (byte)'0';
|
||||
header[264] = (byte)'0';
|
||||
|
||||
int checksum = 0;
|
||||
foreach (var b in header) checksum += b;
|
||||
var chkBytes = Encoding.ASCII.GetBytes(System.Convert.ToString(checksum, 8).PadLeft(6, '0') + "\0 ");
|
||||
System.Array.Copy(chkBytes, 0, header, 148, chkBytes.Length);
|
||||
|
||||
ms.Write(header, 0, header.Length);
|
||||
ms.Write(content, 0, content.Length);
|
||||
var pad = (512 - content.Length % 512) % 512;
|
||||
if (pad > 0) ms.Write(new byte[pad], 0, pad);
|
||||
ms.Write(new byte[1024], 0, 1024); // two end-of-archive zero blocks
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteOctalField(byte[] header, int offset, int length, long value)
|
||||
{
|
||||
var octal = System.Convert.ToString(value, 8);
|
||||
if (value < 0 || octal.Length > length - 1)
|
||||
throw new System.ArgumentOutOfRangeException(nameof(value), value, "Does not fit the octal field.");
|
||||
var bytes = Encoding.ASCII.GetBytes(octal.PadLeft(length - 1, '0'));
|
||||
System.Array.Copy(bytes, 0, header, offset, bytes.Length);
|
||||
header[offset + length - 1] = 0;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadOva_TarWithOvfEntry_ParsesMetadata()
|
||||
{
|
||||
var xml = BuildDescriptor("disk1.vmdk", 6);
|
||||
var tarBytes = BuildTarWithEntry("appliance.ovf", Encoding.UTF8.GetBytes(xml));
|
||||
|
||||
var tempPath = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(tempPath, tarBytes);
|
||||
|
||||
var metadata = OvfReader.ReadOva(tempPath);
|
||||
|
||||
Assert.Equal("test-vm", metadata.Name);
|
||||
Assert.Single(metadata.Disks);
|
||||
Assert.Equal("disk1.vmdk", metadata.Disks[0].FileName);
|
||||
Assert.Equal("scsi", metadata.Disks[0].BusType);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadOva_TarWithNoOvfEntry_Throws()
|
||||
{
|
||||
var tarBytes = BuildTarWithEntry("readme.txt", Encoding.UTF8.GetBytes("not an ovf"));
|
||||
|
||||
var tempPath = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(tempPath, tarBytes);
|
||||
|
||||
Assert.Throws<System.InvalidOperationException>(() => OvfReader.ReadOva(tempPath));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvf_VMwareScsiController_ResourceType6_MapsToScsi()
|
||||
{
|
||||
var xml = BuildDescriptor("disk1.vmdk", 6);
|
||||
var metadata = OvfReader.ParseOvf(xml);
|
||||
|
||||
Assert.Single(metadata.Disks);
|
||||
Assert.Equal("scsi", metadata.Disks[0].BusType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvf_VMwareSataController_ResourceType20_MapsToSata()
|
||||
{
|
||||
var xml = BuildDescriptor("disk1.vmdk", 20);
|
||||
var metadata = OvfReader.ParseOvf(xml);
|
||||
|
||||
Assert.Single(metadata.Disks);
|
||||
Assert.Equal("sata", metadata.Disks[0].BusType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvf_HrefWithEmbeddedProperty_Throws()
|
||||
{
|
||||
var xml = BuildDescriptor("d.vmdk,cache=unsafe", 6);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => OvfReader.ParseOvf(xml));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvf_HrefWithPathTraversal_Throws()
|
||||
{
|
||||
var xml = BuildDescriptor("../x.vmdk", 6);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => OvfReader.ParseOvf(xml));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvf_HrefIsDotDot_Throws()
|
||||
{
|
||||
var xml = BuildDescriptor("..", 6);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => OvfReader.ParseOvf(xml));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvf_HrefIsDot_Throws()
|
||||
{
|
||||
var xml = BuildDescriptor(".", 6);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => OvfReader.ParseOvf(xml));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOvf_DescriptorWithInternalEntity_Throws()
|
||||
{
|
||||
var xml =
|
||||
"<?xml version=\"1.0\"?>" +
|
||||
"<!DOCTYPE Envelope [<!ENTITY boom \"boom\">]>" +
|
||||
OvfHeader +
|
||||
"<VirtualSystem ovf:id=\"&boom;\"/>" +
|
||||
"</Envelope>";
|
||||
|
||||
Assert.ThrowsAny<XmlException>(() => OvfReader.ParseOvf(xml));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ Describe 'OVA Import — Integration' -Tag 'Integration' {
|
||||
return
|
||||
}
|
||||
|
||||
$metadata = [PSProxmoxVE.Core.Models.Vms.OvfMetadata]::FromOva($script:OvaPath)
|
||||
$metadata = [PSProxmoxVE.Core.Utilities.OvfReader]::ReadOva($script:OvaPath)
|
||||
$metadata | Should -Not -BeNullOrEmpty
|
||||
$metadata.Name | Should -Not -BeNullOrEmpty
|
||||
$metadata.CpuCount | Should -BeGreaterThan 0
|
||||
|
||||
Reference in New Issue
Block a user