diff --git a/docs/cmdlets/Get-PveBackupInfo.md b/docs/cmdlets/Get-PveBackupInfo.md
index dacb4ad..436a3f2 100644
--- a/docs/cmdlets/Get-PveBackupInfo.md
+++ b/docs/cmdlets/Get-PveBackupInfo.md
@@ -68,7 +68,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
### None
## OUTPUTS
-### System.Management.Automation.PSObject
+### PSProxmoxVE.Core.Models.Backup.PveBackupInfo
## NOTES
## RELATED LINKS
diff --git a/docs/cmdlets/Get-PveClusterConfig.md b/docs/cmdlets/Get-PveClusterConfig.md
index 0d3dee8..20ea02b 100644
--- a/docs/cmdlets/Get-PveClusterConfig.md
+++ b/docs/cmdlets/Get-PveClusterConfig.md
@@ -68,7 +68,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
### None
## OUTPUTS
-### System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Object, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]
+### PSProxmoxVE.Core.Models.Cluster.PveClusterConfigEntry
## NOTES
## RELATED LINKS
diff --git a/docs/cmdlets/Get-PveNodeConfig.md b/docs/cmdlets/Get-PveNodeConfig.md
index f3ef745..d34d578 100644
--- a/docs/cmdlets/Get-PveNodeConfig.md
+++ b/docs/cmdlets/Get-PveNodeConfig.md
@@ -84,7 +84,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
### System.String
## OUTPUTS
-### System.Management.Automation.PSObject
+### PSProxmoxVE.Core.Models.Nodes.PveNodeConfig
## NOTES
## RELATED LINKS
diff --git a/docs/cmdlets/Get-PveNodeDns.md b/docs/cmdlets/Get-PveNodeDns.md
index 6a8d46d..c39e015 100644
--- a/docs/cmdlets/Get-PveNodeDns.md
+++ b/docs/cmdlets/Get-PveNodeDns.md
@@ -84,7 +84,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
### System.String
## OUTPUTS
-### System.Management.Automation.PSObject
+### PSProxmoxVE.Core.Models.Nodes.PveNodeDns
## NOTES
## RELATED LINKS
diff --git a/src/PSProxmoxVE.Core/Models/Backup/PveBackupInfo.cs b/src/PSProxmoxVE.Core/Models/Backup/PveBackupInfo.cs
new file mode 100644
index 0000000..77866a7
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Backup/PveBackupInfo.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Utilities;
+
+namespace PSProxmoxVE.Core.Models.Backup;
+
+///
+/// Represents one guest not covered by any backup job, as returned by
+/// the /cluster/backup-info/not-backed-up endpoint.
+///
+public class PveBackupInfo
+{
+ ///
+ /// Name of the guest.
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// Type of the guest ("qemu" or "lxc").
+ ///
+ [JsonProperty("type")]
+ public string? Type { get; set; }
+
+ ///
+ /// VMID of the guest.
+ ///
+ [JsonProperty("vmid")]
+ public int? VmId { get; set; }
+
+ ///
+ /// Raw landing spot for any key not mapped to a typed property above.
+ ///
+ [JsonExtensionData]
+ private IDictionary? ExtensionData { get; set; }
+
+ private Dictionary? _additionalProperties;
+
+ ///
+ /// Any keys not surfaced as a typed property above. Keys map to native
+ /// .NET values so the dictionary works naturally in PowerShell pipelines.
+ ///
+ [JsonIgnore]
+ public Dictionary AdditionalProperties =>
+ _additionalProperties ??= ExtensionData == null
+ ? new Dictionary()
+ : ExtensionData.ToDictionary(kvp => kvp.Key, kvp => JsonHelper.ToNative(kvp.Value));
+
+ ///
+ public override string ToString()
+ {
+ return $"{Type ?? "guest"} {VmId?.ToString() ?? "?"} ({Name ?? "N/A"}) not backed up";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Cluster/PveClusterConfigEntry.cs b/src/PSProxmoxVE.Core/Models/Cluster/PveClusterConfigEntry.cs
new file mode 100644
index 0000000..48ef450
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Cluster/PveClusterConfigEntry.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Utilities;
+
+namespace PSProxmoxVE.Core.Models.Cluster;
+
+///
+/// Represents one entry of the directory index returned by GET /cluster/config.
+/// The endpoint's item schema documents no named properties ("properties": {}),
+/// but its "links" metadata gives the child-URL template as "{name}", so every
+/// entry carries a "name" key (e.g. "nodes", "totem", "qdevice", "join",
+/// "apiversion"). Any other key lands in .
+///
+public class PveClusterConfigEntry
+{
+ ///
+ /// The sub-resource name (e.g. "nodes", "totem", "qdevice", "join", "apiversion").
+ ///
+ [JsonProperty("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// Raw landing spot for any key not mapped to a typed property above.
+ ///
+ [JsonExtensionData]
+ private IDictionary? ExtensionData { get; set; }
+
+ private Dictionary? _additionalProperties;
+
+ ///
+ /// Any keys not surfaced as a typed property above. Keys map to native
+ /// .NET values so the dictionary works naturally in PowerShell pipelines.
+ ///
+ [JsonIgnore]
+ public Dictionary AdditionalProperties =>
+ _additionalProperties ??= ExtensionData == null
+ ? new Dictionary()
+ : ExtensionData.ToDictionary(kvp => kvp.Key, kvp => JsonHelper.ToNative(kvp.Value));
+
+ ///
+ public override string ToString()
+ {
+ return $"Cluster Config Entry: {Name ?? "N/A"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Nodes/PveNodeConfig.cs b/src/PSProxmoxVE.Core/Models/Nodes/PveNodeConfig.cs
new file mode 100644
index 0000000..e16543b
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Nodes/PveNodeConfig.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Utilities;
+
+namespace PSProxmoxVE.Core.Models.Nodes;
+
+///
+/// Represents the configuration of a Proxmox VE node,
+/// as returned by the /nodes/{node}/config endpoint.
+///
+public class PveNodeConfig
+{
+ ///
+ /// Node specific ACME settings.
+ ///
+ [JsonProperty("acme")]
+ public string? Acme { get; set; }
+
+ ///
+ /// RAM usage target for ballooning, in percent of total memory.
+ ///
+ [JsonProperty("ballooning-target")]
+ public int? BallooningTarget { get; set; }
+
+ ///
+ /// Description for the node, shown in the web interface node notes panel.
+ ///
+ [JsonProperty("description")]
+ public string? Description { get; set; }
+
+ ///
+ /// SHA1 digest of the current configuration file.
+ ///
+ [JsonProperty("digest")]
+ public string? Digest { get; set; }
+
+ ///
+ /// The location of the node, overriding the datacenter config default.
+ ///
+ [JsonProperty("location")]
+ public string? Location { get; set; }
+
+ ///
+ /// Initial delay in seconds before starting all on-boot Virtual Guests.
+ ///
+ [JsonProperty("startall-onboot-delay")]
+ public int? StartAllOnbootDelay { get; set; }
+
+ ///
+ /// Node specific wake-on-LAN settings.
+ ///
+ [JsonProperty("wakeonlan")]
+ public string? WakeOnLan { get; set; }
+
+ ///
+ /// Raw landing spot for any config key not mapped to a typed property above
+ /// (e.g. per-domain "acmedomain0", "acmedomain1", ... entries).
+ ///
+ [JsonExtensionData]
+ private IDictionary? ExtensionData { get; set; }
+
+ private Dictionary? _additionalProperties;
+
+ ///
+ /// Any node config keys not surfaced as a typed property above. Keys map to
+ /// native .NET values so the dictionary works naturally in PowerShell pipelines.
+ ///
+ [JsonIgnore]
+ public Dictionary AdditionalProperties =>
+ _additionalProperties ??= ExtensionData == null
+ ? new Dictionary()
+ : ExtensionData.ToDictionary(kvp => kvp.Key, kvp => JsonHelper.ToNative(kvp.Value));
+
+ ///
+ public override string ToString()
+ {
+ return $"Node Config | Description: {Description ?? "N/A"} | WakeOnLan: {WakeOnLan ?? "N/A"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Nodes/PveNodeDns.cs b/src/PSProxmoxVE.Core/Models/Nodes/PveNodeDns.cs
new file mode 100644
index 0000000..42eb6d9
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Nodes/PveNodeDns.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Utilities;
+
+namespace PSProxmoxVE.Core.Models.Nodes;
+
+///
+/// Represents the DNS configuration of a Proxmox VE node,
+/// as returned by the /nodes/{node}/dns endpoint.
+///
+public class PveNodeDns
+{
+ ///
+ /// First name server IP address.
+ ///
+ [JsonProperty("dns1")]
+ public string? Dns1 { get; set; }
+
+ ///
+ /// Second name server IP address.
+ ///
+ [JsonProperty("dns2")]
+ public string? Dns2 { get; set; }
+
+ ///
+ /// Third name server IP address.
+ ///
+ [JsonProperty("dns3")]
+ public string? Dns3 { get; set; }
+
+ ///
+ /// Search domain for host-name lookup.
+ ///
+ [JsonProperty("search")]
+ public string? Search { get; set; }
+
+ ///
+ /// Raw landing spot for any DNS config key not mapped to a typed property above.
+ ///
+ [JsonExtensionData]
+ private IDictionary? ExtensionData { get; set; }
+
+ private Dictionary? _additionalProperties;
+
+ ///
+ /// Any DNS config keys not surfaced as a typed property above. Keys map to
+ /// native .NET values so the dictionary works naturally in PowerShell pipelines.
+ ///
+ [JsonIgnore]
+ public Dictionary AdditionalProperties =>
+ _additionalProperties ??= ExtensionData == null
+ ? new Dictionary()
+ : ExtensionData.ToDictionary(kvp => kvp.Key, kvp => JsonHelper.ToNative(kvp.Value));
+
+ ///
+ public override string ToString()
+ {
+ return $"DNS | Search: {Search ?? "N/A"} | {Dns1 ?? "N/A"}, {Dns2 ?? "N/A"}, {Dns3 ?? "N/A"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Vms/PveGuestExecStatus.cs b/src/PSProxmoxVE.Core/Models/Vms/PveGuestExecStatus.cs
new file mode 100644
index 0000000..d1e7ba3
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Vms/PveGuestExecStatus.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Utilities;
+
+namespace PSProxmoxVE.Core.Models.Vms;
+
+///
+/// Represents the status of a guest-agent command started via exec, as returned
+/// by the /nodes/{node}/qemu/{vmid}/agent/exec-status endpoint.
+///
+public class PveGuestExecStatus
+{
+ ///
+ /// Whether the command has exited yet. PVE has been observed sending this as
+ /// a boolean, an integer (1/0), or a string ("1"/"0");
+ /// normalizes all three.
+ ///
+ [JsonProperty("exited")]
+ [JsonConverter(typeof(TolerantBooleanConverter))]
+ public bool? Exited { get; set; }
+
+ ///
+ /// Process exit code, if it was normally terminated.
+ ///
+ [JsonProperty("exitcode")]
+ public int? ExitCode { get; set; }
+
+ ///
+ /// Signal number or exception code, if the process was abnormally terminated.
+ ///
+ [JsonProperty("signal")]
+ public int? Signal { get; set; }
+
+ ///
+ /// Base64-encoded stdout of the process.
+ ///
+ [JsonProperty("out-data")]
+ public string? OutData { get; set; }
+
+ ///
+ /// Base64-encoded stderr of the process.
+ ///
+ [JsonProperty("err-data")]
+ public string? ErrData { get; set; }
+
+ ///
+ /// True if stdout was not fully captured.
+ ///
+ [JsonProperty("out-truncated")]
+ public bool? OutTruncated { get; set; }
+
+ ///
+ /// True if stderr was not fully captured.
+ ///
+ [JsonProperty("err-truncated")]
+ public bool? ErrTruncated { get; set; }
+
+ ///
+ /// Raw landing spot for any key not mapped to a typed property above.
+ ///
+ [JsonExtensionData]
+ private IDictionary? ExtensionData { get; set; }
+
+ private Dictionary? _additionalProperties;
+
+ ///
+ /// Any keys not surfaced as a typed property above. Keys map to native
+ /// .NET values so the dictionary works naturally in PowerShell pipelines.
+ ///
+ [JsonIgnore]
+ public Dictionary AdditionalProperties =>
+ _additionalProperties ??= ExtensionData == null
+ ? new Dictionary()
+ : ExtensionData.ToDictionary(kvp => kvp.Key, kvp => JsonHelper.ToNative(kvp.Value));
+
+ ///
+ public override string ToString()
+ {
+ return Exited == true
+ ? $"Exec Status | Exited, code {ExitCode?.ToString() ?? "N/A"}"
+ : "Exec Status | Running";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Services/BackupService.cs b/src/PSProxmoxVE.Core/Services/BackupService.cs
index 53010d6..cf4f3ee 100644
--- a/src/PSProxmoxVE.Core/Services/BackupService.cs
+++ b/src/PSProxmoxVE.Core/Services/BackupService.cs
@@ -139,7 +139,7 @@ namespace PSProxmoxVE.Core.Services
///
/// Returns the list of guests not covered by any backup job.
///
- public List> GetNotBackedUp(PveSession session)
+ public List GetNotBackedUp(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
@@ -148,7 +148,7 @@ namespace PSProxmoxVE.Core.Services
var response = client.GetAsync("cluster/backup-info/not-backed-up")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
- return JsonHelper.ToListOfDictionaries(data as JArray);
+ return data?.ToObject>() ?? new List();
});
}
}
diff --git a/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs b/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs
index bb9c9fc..d151e5e 100644
--- a/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs
+++ b/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs
@@ -6,7 +6,6 @@ using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Exceptions;
using PSProxmoxVE.Core.Models.Cluster;
-using PSProxmoxVE.Core.Utilities;
namespace PSProxmoxVE.Core.Services
{
@@ -39,10 +38,11 @@ namespace PSProxmoxVE.Core.Services
}
///
- /// Returns the cluster configuration directory (GET /cluster/config).
- /// The response is a mixed structure returned as a Dictionary.
+ /// Returns the cluster configuration directory (GET /cluster/config). The
+ /// endpoint is a directory index: an array of entries (name: "nodes",
+ /// "totem", "qdevice", "join", "apiversion"), not a single object.
///
- public Dictionary GetClusterConfig(PveSession session)
+ public List GetClusterConfig(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
@@ -50,7 +50,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.GetAsync("cluster/config").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
- return data is JObject obj ? JsonHelper.ToDictionary(obj) : new Dictionary();
+ return data?.ToObject>() ?? new List();
});
}
diff --git a/src/PSProxmoxVE.Core/Services/NodeService.cs b/src/PSProxmoxVE.Core/Services/NodeService.cs
index d6d3d0a..25593e0 100644
--- a/src/PSProxmoxVE.Core/Services/NodeService.cs
+++ b/src/PSProxmoxVE.Core/Services/NodeService.cs
@@ -71,7 +71,7 @@ namespace PSProxmoxVE.Core.Services
///
/// The authenticated PVE session.
/// The cluster node name.
- public Dictionary GetNodeConfig(PveSession session, string node)
+ public PveNodeConfig GetNodeConfig(PveSession session, string node)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
@@ -80,7 +80,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/config").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
- return JsonHelper.ToDictionary(data as JObject);
+ return data?.ToObject() ?? new PveNodeConfig();
});
}
@@ -107,7 +107,7 @@ namespace PSProxmoxVE.Core.Services
///
/// The authenticated PVE session.
/// The cluster node name.
- public Dictionary GetNodeDns(PveSession session, string node)
+ public PveNodeDns GetNodeDns(PveSession session, string node)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
@@ -116,7 +116,7 @@ namespace PSProxmoxVE.Core.Services
{
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/dns").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
- return JsonHelper.ToDictionary(data as JObject);
+ return data?.ToObject() ?? new PveNodeDns();
});
}
diff --git a/src/PSProxmoxVE.Core/Services/VmService.cs b/src/PSProxmoxVE.Core/Services/VmService.cs
index 593555c..2ed9bca 100644
--- a/src/PSProxmoxVE.Core/Services/VmService.cs
+++ b/src/PSProxmoxVE.Core/Services/VmService.cs
@@ -605,7 +605,7 @@ namespace PSProxmoxVE.Core.Services
///
/// Gets the status/result of a guest agent exec command by PID.
///
- public Dictionary GetGuestExecStatus(PveSession session, string node, int vmid, int pid)
+ public PveGuestExecStatus GetGuestExecStatus(PveSession session, string node, int vmid, int pid)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
@@ -615,7 +615,7 @@ namespace PSProxmoxVE.Core.Services
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/exec-status?pid={pid}")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
- return JsonHelper.ToDictionary(data as JObject);
+ return data?.ToObject() ?? new PveGuestExecStatus();
});
}
diff --git a/src/PSProxmoxVE.Core/Utilities/NativeJsonConverters.cs b/src/PSProxmoxVE.Core/Utilities/NativeJsonConverters.cs
index 0c4b007..6102e5c 100644
--- a/src/PSProxmoxVE.Core/Utilities/NativeJsonConverters.cs
+++ b/src/PSProxmoxVE.Core/Utilities/NativeJsonConverters.cs
@@ -43,4 +43,25 @@ namespace PSProxmoxVE.Core.Utilities
serializer.Serialize(writer, value);
}
}
+
+ ///
+ /// JSON converter for a boolean field the PVE API sends as boolean, integer
+ /// (1/0), or string ("1"/"0") depending on endpoint and version. Delegates
+ /// the tolerant interpretation to .
+ ///
+ public class TolerantBooleanConverter : JsonConverter
+ {
+ ///
+ public override bool? ReadJson(JsonReader reader, Type objectType, bool? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ var token = JToken.Load(reader);
+ return token.Type == JTokenType.Null ? (bool?)null : ApiValueHelper.IsExited(JsonHelper.ToNative(token));
+ }
+
+ ///
+ public override void WriteJson(JsonWriter writer, bool? value, JsonSerializer serializer)
+ {
+ serializer.Serialize(writer, value);
+ }
+ }
}
diff --git a/src/PSProxmoxVE/Cmdlets/Backup/GetPveBackupInfoCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Backup/GetPveBackupInfoCmdlet.cs
index 99d68a9..b453810 100644
--- a/src/PSProxmoxVE/Cmdlets/Backup/GetPveBackupInfoCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Backup/GetPveBackupInfoCmdlet.cs
@@ -1,5 +1,5 @@
-using System.Collections.Generic;
using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Backup;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Backup
@@ -12,7 +12,7 @@ namespace PSProxmoxVE.Cmdlets.Backup
///
///
[Cmdlet(VerbsCommon.Get, "PveBackupInfo")]
- [OutputType(typeof(PSObject))]
+ [OutputType(typeof(PveBackupInfo))]
public sealed class GetPveBackupInfoCmdlet : PveCmdletBase
{
protected override void ProcessPveRecord()
@@ -26,12 +26,7 @@ namespace PSProxmoxVE.Cmdlets.Backup
foreach (var item in items)
{
- var pso = new PSObject();
- foreach (var kvp in item)
- {
- pso.Properties.Add(new PSNoteProperty(kvp.Key, kvp.Value));
- }
- WriteObject(pso);
+ WriteObject(item);
}
}
}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigCmdlet.cs
index 21e0c6f..96eaedc 100644
--- a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigCmdlet.cs
@@ -1,5 +1,5 @@
-using System.Collections.Generic;
using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Cluster;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
@@ -7,12 +7,12 @@ namespace PSProxmoxVE.Cmdlets.Cluster
///
/// Gets the cluster configuration.
///
- /// Returns the raw cluster configuration including nodes, totem settings,
- /// and cluster version information.
+ /// Returns the cluster configuration directory (GET /cluster/config): one entry
+ /// per available sub-resource (nodes, totem, qdevice, join, apiversion).
///
///
[Cmdlet(VerbsCommon.Get, "PveClusterConfig")]
- [OutputType(typeof(Dictionary))]
+ [OutputType(typeof(PveClusterConfigEntry))]
public sealed class GetPveClusterConfigCmdlet : PveCmdletBase
{
protected override void ProcessPveRecord()
@@ -21,8 +21,11 @@ namespace PSProxmoxVE.Cmdlets.Cluster
var service = new ClusterConfigService();
WriteVerbose("Getting cluster configuration...");
- var config = service.GetClusterConfig(session);
- WriteObject(config);
+ var entries = service.GetClusterConfig(session);
+ foreach (var entry in entries)
+ {
+ WriteObject(entry);
+ }
}
}
}
diff --git a/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeConfigCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeConfigCmdlet.cs
index 89be9ef..eb38ee7 100644
--- a/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeConfigCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeConfigCmdlet.cs
@@ -1,4 +1,5 @@
using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Nodes;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Nodes
@@ -11,7 +12,7 @@ namespace PSProxmoxVE.Cmdlets.Nodes
///
///
[Cmdlet(VerbsCommon.Get, "PveNodeConfig")]
- [OutputType(typeof(PSObject))]
+ [OutputType(typeof(PveNodeConfig))]
public sealed class GetPveNodeConfigCmdlet : PveCmdletBase
{
/// The Proxmox VE node name.
@@ -27,13 +28,7 @@ namespace PSProxmoxVE.Cmdlets.Nodes
WriteVerbose($"Getting configuration for node '{Node}'...");
var config = service.GetNodeConfig(session, Node);
- var psObj = new PSObject();
- foreach (var kvp in config)
- {
- psObj.Properties.Add(new PSNoteProperty(kvp.Key, kvp.Value));
- }
-
- WriteObject(psObj);
+ WriteObject(config);
}
}
}
diff --git a/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeDnsCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeDnsCmdlet.cs
index a3d2ba3..5268f5f 100644
--- a/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeDnsCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Nodes/GetPveNodeDnsCmdlet.cs
@@ -1,4 +1,5 @@
using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Nodes;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Nodes
@@ -11,7 +12,7 @@ namespace PSProxmoxVE.Cmdlets.Nodes
///
///
[Cmdlet(VerbsCommon.Get, "PveNodeDns")]
- [OutputType(typeof(PSObject))]
+ [OutputType(typeof(PveNodeDns))]
public sealed class GetPveNodeDnsCmdlet : PveCmdletBase
{
/// The Proxmox VE node name.
@@ -27,13 +28,7 @@ namespace PSProxmoxVE.Cmdlets.Nodes
WriteVerbose($"Getting DNS configuration for node '{Node}'...");
var dns = service.GetNodeDns(session, Node);
- var psObj = new PSObject();
- foreach (var kvp in dns)
- {
- psObj.Properties.Add(new PSNoteProperty(kvp.Key, kvp.Value));
- }
-
- WriteObject(psObj);
+ WriteObject(dns);
}
}
}
diff --git a/src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestExecCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestExecCmdlet.cs
index a0510f0..3ed2727 100644
--- a/src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestExecCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Vms/InvokePveVmGuestExecCmdlet.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
using PSProxmoxVE.Core.Utilities;
@@ -54,19 +55,19 @@ namespace PSProxmoxVE.Cmdlets.Vms
// Poll for completion with timeout
var sw = Stopwatch.StartNew();
var deadline = TimeSpan.FromSeconds(Timeout);
- System.Collections.Generic.Dictionary result;
+ PveGuestExecStatus result;
do
{
System.Threading.Thread.Sleep(1000);
if (sw.Elapsed >= deadline)
throw new TimeoutException($"Guest command did not complete within {Timeout} seconds.");
result = service.GetGuestExecStatus(session, Node, VmId, pid);
- } while (!result.TryGetValue("exited", out var exited) || !ApiValueHelper.IsExited(exited));
+ } while (!ApiValueHelper.IsExited(result.Exited));
var output = new PSObject();
- output.Properties.Add(new PSNoteProperty("ExitCode", result.TryGetValue("exitcode", out var ec) && ec is long ecl ? (int)ecl : -1));
- output.Properties.Add(new PSNoteProperty("Stdout", DecodeBase64(result.TryGetValue("out-data", out var od) ? od?.ToString() : null)));
- output.Properties.Add(new PSNoteProperty("Stderr", DecodeBase64(result.TryGetValue("err-data", out var ed) ? ed?.ToString() : null)));
+ output.Properties.Add(new PSNoteProperty("ExitCode", result.ExitCode ?? -1));
+ output.Properties.Add(new PSNoteProperty("Stdout", DecodeBase64(result.OutData)));
+ output.Properties.Add(new PSNoteProperty("Stderr", DecodeBase64(result.ErrData)));
output.Properties.Add(new PSNoteProperty("Pid", pid));
WriteObject(output);
diff --git a/src/PSProxmoxVE/PSProxmoxVE.dll-Help.xml b/src/PSProxmoxVE/PSProxmoxVE.dll-Help.xml
index f41d705..84d303d 100644
--- a/src/PSProxmoxVE/PSProxmoxVE.dll-Help.xml
+++ b/src/PSProxmoxVE/PSProxmoxVE.dll-Help.xml
@@ -2088,7 +2088,7 @@
- System.Management.Automation.PSObject
+ PSProxmoxVE.Core.Models.Backup.PveBackupInfo
@@ -2476,7 +2476,7 @@
- System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Object, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]
+ PSProxmoxVE.Core.Models.Cluster.PveClusterConfigEntry
@@ -6478,7 +6478,7 @@
- System.Management.Automation.PSObject
+ PSProxmoxVE.Core.Models.Nodes.PveNodeConfig
@@ -6605,7 +6605,7 @@
- System.Management.Automation.PSObject
+ PSProxmoxVE.Core.Models.Nodes.PveNodeDns
diff --git a/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml b/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml
index 9cbb8a5..a0a6044 100644
--- a/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml
+++ b/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml
@@ -1415,5 +1415,31 @@
+
+
+ PSProxmoxVE.Core.Models.Cluster.PveClusterConfigEntry
+
+ PSProxmoxVE.Core.Models.Cluster.PveClusterConfigEntry
+
+
+
+
+
+ 20
+ Left
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/BackupModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/BackupModelTests.cs
index d7671f5..33b1993 100644
--- a/tests/PSProxmoxVE.Core.Tests/Models/BackupModelTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Models/BackupModelTests.cs
@@ -122,5 +122,33 @@ namespace PSProxmoxVE.Core.Tests.Models
Assert.Null(jobs[1].Node);
Assert.Null(jobs[1].Exclude);
}
+
+ [Fact]
+ public void PveBackupInfo_Deserialize_HasDocumentedFields()
+ {
+ var json = @"{""data"": [
+ { ""vmid"": 100, ""name"": ""webserver"", ""type"": ""qemu"" }
+ ]}";
+ var data = JObject.Parse(json)["data"];
+ Assert.NotNull(data);
+ var items = data.ToObject();
+ Assert.NotNull(items);
+ Assert.Equal(100, items[0].VmId);
+ Assert.Equal("webserver", items[0].Name);
+ Assert.Equal("qemu", items[0].Type);
+ }
+
+ [Fact]
+ public void PveBackupInfo_UnmappedKey_LandsInAdditionalProperties()
+ {
+ var json = @"{""data"": [
+ { ""vmid"": 200, ""name"": ""db"", ""type"": ""lxc"", ""comment"": ""prod"" }
+ ]}";
+ var data = JObject.Parse(json)["data"];
+ var items = data!.ToObject();
+ Assert.NotNull(items);
+ Assert.Equal("prod", items![0].AdditionalProperties["comment"]);
+ Assert.False(items[0].AdditionalProperties.ContainsKey("vmid"));
+ }
}
}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/ClusterModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/ClusterModelTests.cs
index 63856db..9fede8b 100644
--- a/tests/PSProxmoxVE.Core.Tests/Models/ClusterModelTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Models/ClusterModelTests.cs
@@ -143,5 +143,32 @@ namespace PSProxmoxVE.Core.Tests.Models
Assert.Null(entries[1].Nodes);
Assert.Null(entries[1].Quorate);
}
+
+ [Fact]
+ public void PveClusterConfigEntry_Deserialize_HasNameFromLinksTemplate()
+ {
+ // GET /cluster/config is a directory index: the item schema documents
+ // no named fields, but the endpoint's "links" metadata gives the
+ // child-URL template as "{name}", so every entry carries a "name" key.
+ var json = @"{""data"": [{""name"": ""nodes""}, {""name"": ""totem""}]}";
+ var data = JObject.Parse(json)["data"];
+ Assert.NotNull(data);
+ var entries = data.ToObject>();
+ Assert.NotNull(entries);
+ Assert.Equal(2, entries!.Count);
+ Assert.Equal("nodes", entries[0].Name);
+ Assert.Equal("totem", entries[1].Name);
+ }
+
+ [Fact]
+ public void PveClusterConfigEntry_UnmappedKey_LandsInAdditionalProperties()
+ {
+ var json = @"{""data"": [{""name"": ""nodes"", ""extra"": ""x""}]}";
+ var data = JObject.Parse(json)["data"];
+ var entries = data!.ToObject>();
+ Assert.NotNull(entries);
+ Assert.Equal("x", entries![0].AdditionalProperties["extra"]);
+ Assert.False(entries[0].AdditionalProperties.ContainsKey("name"));
+ }
}
}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/NodeModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/NodeModelTests.cs
index df6137a..b3087cc 100644
--- a/tests/PSProxmoxVE.Core.Tests/Models/NodeModelTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Models/NodeModelTests.cs
@@ -108,5 +108,62 @@ namespace PSProxmoxVE.Core.Tests.Models
Assert.NotNull(nodes[0].LoadAverage);
Assert.Equal(3, nodes[0].LoadAverage!.Length);
}
+
+ [Fact]
+ public void PveNodeConfig_Deserialize_HasDocumentedFields()
+ {
+ var json = @"{""data"": {
+ ""description"": ""Primary node"",
+ ""wakeonlan"": ""AA:BB:CC:DD:EE:FF"",
+ ""ballooning-target"": 80,
+ ""startall-onboot-delay"": 30,
+ ""digest"": ""abc123""
+ }}";
+ var data = JObject.Parse(json)["data"];
+ Assert.NotNull(data);
+ var config = data.ToObject();
+ Assert.NotNull(config);
+ Assert.Equal("Primary node", config.Description);
+ Assert.Equal("AA:BB:CC:DD:EE:FF", config.WakeOnLan);
+ Assert.Equal(80, config.BallooningTarget);
+ Assert.Equal(30, config.StartAllOnbootDelay);
+ Assert.Equal("abc123", config.Digest);
+ }
+
+ [Fact]
+ public void PveNodeConfig_UnmappedKey_LandsInAdditionalProperties()
+ {
+ var json = @"{""data"": {""description"": ""n1"", ""acmedomain0"": ""example.com,plugin=dns""}}";
+ var data = JObject.Parse(json)["data"];
+ var config = data!.ToObject();
+ Assert.NotNull(config);
+ Assert.Equal("example.com,plugin=dns", config!.AdditionalProperties["acmedomain0"]);
+ Assert.False(config.AdditionalProperties.ContainsKey("description"));
+ }
+
+ [Fact]
+ public void PveNodeDns_Deserialize_HasDocumentedFields()
+ {
+ var json = @"{""data"": {""dns1"": ""8.8.8.8"", ""dns2"": ""8.8.4.4"", ""dns3"": ""1.1.1.1"", ""search"": ""example.com""}}";
+ var data = JObject.Parse(json)["data"];
+ Assert.NotNull(data);
+ var dns = data.ToObject();
+ Assert.NotNull(dns);
+ Assert.Equal("8.8.8.8", dns.Dns1);
+ Assert.Equal("8.8.4.4", dns.Dns2);
+ Assert.Equal("1.1.1.1", dns.Dns3);
+ Assert.Equal("example.com", dns.Search);
+ }
+
+ [Fact]
+ public void PveNodeDns_UnmappedKey_LandsInAdditionalProperties()
+ {
+ var json = @"{""data"": {""dns1"": ""8.8.8.8"", ""dns4"": ""9.9.9.9""}}";
+ var data = JObject.Parse(json)["data"];
+ var dns = data!.ToObject();
+ Assert.NotNull(dns);
+ Assert.Equal("9.9.9.9", dns!.AdditionalProperties["dns4"]);
+ Assert.False(dns.AdditionalProperties.ContainsKey("dns1"));
+ }
}
}
diff --git a/tests/PSProxmoxVE.Core.Tests/Models/VmModelTests.cs b/tests/PSProxmoxVE.Core.Tests/Models/VmModelTests.cs
index dd36be9..2397ea7 100644
--- a/tests/PSProxmoxVE.Core.Tests/Models/VmModelTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Models/VmModelTests.cs
@@ -225,5 +225,53 @@ namespace PSProxmoxVE.Core.Tests.Models
Assert.False(config!.AdditionalProperties.ContainsKey("scsihw"));
Assert.False(config.AdditionalProperties.ContainsKey("cores"));
}
+
+ [Fact]
+ public void PveGuestExecStatus_Deserialize_HasDocumentedFields()
+ {
+ var json = @"{""data"": {
+ ""exited"": true,
+ ""exitcode"": 0,
+ ""out-data"": ""aGVsbG8="",
+ ""err-data"": """",
+ ""out-truncated"": false
+ }}";
+ var data = JObject.Parse(json)["data"];
+ Assert.NotNull(data);
+ var status = data.ToObject();
+ Assert.NotNull(status);
+ Assert.True(status.Exited);
+ Assert.Equal(0, status.ExitCode);
+ Assert.Equal("aGVsbG8=", status.OutData);
+ Assert.Equal(string.Empty, status.ErrData);
+ Assert.False(status.OutTruncated);
+ }
+
+ [Theory]
+ [InlineData("true", true)]
+ [InlineData("false", false)]
+ [InlineData("1", true)]
+ [InlineData("0", false)]
+ [InlineData("\"1\"", true)]
+ [InlineData("\"0\"", false)]
+ public void PveGuestExecStatus_Exited_ToleratesBooleanIntegerAndStringForms(string exitedLiteral, bool expected)
+ {
+ var json = $@"{{""data"": {{""exited"": {exitedLiteral}}}}}";
+ var data = JObject.Parse(json)["data"];
+ var status = data!.ToObject();
+ Assert.NotNull(status);
+ Assert.Equal(expected, status!.Exited);
+ }
+
+ [Fact]
+ public void PveGuestExecStatus_UnmappedKey_LandsInAdditionalProperties()
+ {
+ var json = @"{""data"": {""exited"": false, ""newfield"": ""future""}}";
+ var data = JObject.Parse(json)["data"];
+ var status = data!.ToObject();
+ Assert.NotNull(status);
+ Assert.Equal("future", status!.AdditionalProperties["newfield"]);
+ Assert.False(status.AdditionalProperties.ContainsKey("exited"));
+ }
}
}
diff --git a/tests/PSProxmoxVE.Core.Tests/Services/BackupServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/BackupServiceTests.cs
index 4feafaf..f2a1edd 100644
--- a/tests/PSProxmoxVE.Core.Tests/Services/BackupServiceTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Services/BackupServiceTests.cs
@@ -285,12 +285,12 @@ namespace PSProxmoxVE.Core.Tests.Services
// ---------------------------------------------------------------
[Fact]
- public void GetNotBackedUp_ReturnsListOfDictionaries()
+ public void GetNotBackedUp_ReturnsTypedEntriesWithUnknownKeyInAdditionalProperties()
{
// Arrange
var json = @"{
""data"": [
- { ""vmid"": 100, ""name"": ""webserver"", ""type"": ""qemu"" },
+ { ""vmid"": 100, ""name"": ""webserver"", ""type"": ""qemu"", ""comment"": ""prod"" },
{ ""vmid"": 200, ""name"": ""database"", ""type"": ""lxc"" }
]
}";
@@ -304,10 +304,11 @@ namespace PSProxmoxVE.Core.Tests.Services
var result = service.GetNotBackedUp(CreateSession());
// Assert
- Assert.IsType>>(result);
Assert.Equal(2, result.Count);
- Assert.Equal(100L, result[0]["vmid"]);
- Assert.Equal("webserver", result[0]["name"]);
+ Assert.Equal(100, result[0].VmId);
+ Assert.Equal("webserver", result[0].Name);
+ Assert.Equal("qemu", result[0].Type);
+ Assert.Equal("prod", result[0].AdditionalProperties["comment"]);
}
[Fact]
diff --git a/tests/PSProxmoxVE.Core.Tests/Services/ClusterConfigServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/ClusterConfigServiceTests.cs
index 228a6b2..4045988 100644
--- a/tests/PSProxmoxVE.Core.Tests/Services/ClusterConfigServiceTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Services/ClusterConfigServiceTests.cs
@@ -21,10 +21,13 @@ namespace PSProxmoxVE.Core.Tests.Services
}
[Fact]
- public void GetClusterConfig_ReturnsJObject()
+ public void GetClusterConfig_ReturnsTypedEntriesWithUnknownKeyInAdditionalProperties()
{
- // Arrange
- var json = @"{""data"": {""nodes"": {""pve1"": {}}, ""totem"": {""version"": ""2""}}}";
+ // Arrange — GET /cluster/config is a directory index: an array of
+ // entries, not a single object. The item schema documents no named
+ // fields, but the endpoint's "links" metadata gives the child-URL
+ // template as "{name}", so every entry carries a "name" key.
+ var json = @"{""data"": [{""name"": ""nodes""}, {""name"": ""totem"", ""extra"": ""x""}]}";
var mockClient = new Mock();
mockClient.Setup(c => c.GetAsync("cluster/config")).ReturnsAsync(json);
var service = new ClusterConfigService(mockClient.Object);
@@ -33,9 +36,10 @@ namespace PSProxmoxVE.Core.Tests.Services
var config = service.GetClusterConfig(CreateSession());
// Assert
- Assert.NotNull(config);
- Assert.NotNull(config["nodes"]);
- Assert.NotNull(config["totem"]);
+ Assert.Equal(2, config.Count);
+ Assert.Equal("nodes", config[0].Name);
+ Assert.Equal("totem", config[1].Name);
+ Assert.Equal("x", config[1].AdditionalProperties["extra"]);
mockClient.Verify(c => c.GetAsync("cluster/config"), Times.Once);
}
diff --git a/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs
index 959fd48..a76093d 100644
--- a/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Services/NodeServiceTests.cs
@@ -138,10 +138,10 @@ namespace PSProxmoxVE.Core.Tests.Services
}
[Fact]
- public void GetNodeConfig_ReturnsDictionary()
+ public void GetNodeConfig_ReturnsTypedModelWithUnknownKeyInAdditionalProperties()
{
// Arrange
- var json = @"{""data"": {""description"": ""Primary node"", ""wakeonlan"": ""AA:BB:CC:DD:EE:FF""}}";
+ var json = @"{""data"": {""description"": ""Primary node"", ""wakeonlan"": ""AA:BB:CC:DD:EE:FF"", ""acmedomain0"": ""example.com,plugin=dns""}}";
var mockClient = new Mock();
mockClient.Setup(c => c.GetAsync("nodes/pve1/config")).ReturnsAsync(json);
var service = new NodeService(mockClient.Object);
@@ -151,9 +151,9 @@ namespace PSProxmoxVE.Core.Tests.Services
// Assert
Assert.NotNull(config);
- Assert.IsType>(config);
- Assert.Equal("Primary node", config["description"]?.ToString());
- Assert.Equal("AA:BB:CC:DD:EE:FF", config["wakeonlan"]?.ToString());
+ Assert.Equal("Primary node", config.Description);
+ Assert.Equal("AA:BB:CC:DD:EE:FF", config.WakeOnLan);
+ Assert.Equal("example.com,plugin=dns", config.AdditionalProperties["acmedomain0"]);
mockClient.Verify(c => c.GetAsync("nodes/pve1/config"), Times.Once);
}
@@ -184,10 +184,10 @@ namespace PSProxmoxVE.Core.Tests.Services
}
[Fact]
- public void GetNodeDns_ReturnsDictionary()
+ public void GetNodeDns_ReturnsTypedModelWithUnknownKeyInAdditionalProperties()
{
// Arrange
- var json = @"{""data"": {""dns1"": ""8.8.8.8"", ""dns2"": ""8.8.4.4"", ""search"": ""example.com""}}";
+ var json = @"{""data"": {""dns1"": ""8.8.8.8"", ""dns2"": ""8.8.4.4"", ""search"": ""example.com"", ""dns4"": ""1.1.1.1""}}";
var mockClient = new Mock();
mockClient.Setup(c => c.GetAsync("nodes/pve1/dns")).ReturnsAsync(json);
var service = new NodeService(mockClient.Object);
@@ -197,10 +197,10 @@ namespace PSProxmoxVE.Core.Tests.Services
// Assert
Assert.NotNull(dns);
- Assert.IsType>(dns);
- Assert.Equal("8.8.8.8", dns["dns1"]?.ToString());
- Assert.Equal("8.8.4.4", dns["dns2"]?.ToString());
- Assert.Equal("example.com", dns["search"]?.ToString());
+ Assert.Equal("8.8.8.8", dns.Dns1);
+ Assert.Equal("8.8.4.4", dns.Dns2);
+ Assert.Equal("example.com", dns.Search);
+ Assert.Equal("1.1.1.1", dns.AdditionalProperties["dns4"]);
mockClient.Verify(c => c.GetAsync("nodes/pve1/dns"), Times.Once);
}
diff --git a/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs
index 1664113..e8527bc 100644
--- a/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs
+++ b/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs
@@ -113,6 +113,48 @@ namespace PSProxmoxVE.Core.Tests.Services
Assert.Equal("args", ex.ParamName);
}
+ [Fact]
+ public void GetGuestExecStatus_ReturnsTypedModelWithUnknownKeyInAdditionalProperties()
+ {
+ var json = @"{""data"": {
+ ""exited"": true,
+ ""exitcode"": 0,
+ ""out-data"": ""aGVsbG8="",
+ ""err-data"": """",
+ ""newfield"": ""future""
+ }}";
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.GetAsync($"nodes/{TestNode}/qemu/{TestVmId}/agent/exec-status?pid=4242"))
+ .ReturnsAsync(json);
+
+ var service = new VmService(mockClient.Object);
+ var status = service.GetGuestExecStatus(CreateSession(), TestNode, TestVmId, 4242);
+
+ Assert.True(status.Exited);
+ Assert.Equal(0, status.ExitCode);
+ Assert.Equal("aGVsbG8=", status.OutData);
+ Assert.Equal(string.Empty, status.ErrData);
+ Assert.Equal("future", status.AdditionalProperties["newfield"]);
+ }
+
+ [Fact]
+ public void GetGuestExecStatus_ExitedAsString_StillPollsToCompletion()
+ {
+ // PVE has been observed sending "exited" as the string "1"/"0" as well
+ // as a JSON boolean or integer; the poll loop in InvokePveVmGuestExecCmdlet
+ // must not throw on this shape.
+ var json = @"{""data"": {""exited"": ""1"", ""exitcode"": 0}}";
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.GetAsync($"nodes/{TestNode}/qemu/{TestVmId}/agent/exec-status?pid=99"))
+ .ReturnsAsync(json);
+
+ var service = new VmService(mockClient.Object);
+ var status = service.GetGuestExecStatus(CreateSession(), TestNode, TestVmId, 99);
+
+ Assert.True(status.Exited);
+ Assert.Equal(0, status.ExitCode);
+ }
+
[Fact]
public void RebootVm_PostsToTheNativeRebootEndpoint()
{