mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-06 12:09:01 +00:00
NodeService.GetNodeConfig/GetNodeDns, ClusterConfigService.GetClusterConfig, BackupService.GetNotBackedUp and VmService.GetGuestExecStatus returned raw Dictionary/List<Dictionary> instead of a Pve* model, per issue #157. Each now has a typed model under Models/{Nodes,Cluster,Backup,Vms}/ with [JsonProperty] for documented fields and a [JsonExtensionData]-backed AdditionalProperties catch-all, following the PveVmConfig pattern. The five consuming cmdlets and their [OutputType] attributes are updated to match. GetClusterConfig also fixes a latent bug: GET /cluster/config returns a JSON array (a directory index), but the old code did `data is JObject obj ? ... : empty dict`, which silently always returned an empty dictionary since data was a JArray. PveClusterConfigEntry decodes the array correctly and exposes a typed Name property (the array items' schema documents no named fields, but the endpoint's "links" metadata gives the child-URL template as "{name}"). The guest-exec poll loop in InvokePveVmGuestExecCmdlet keeps its exact Stopwatch + Thread.Sleep(1000) structure (ADR 0001 accepted exception); only the type it reads from changed. A TolerantBooleanConverter was added so PveGuestExecStatus.Exited keeps accepting PVE's boolean/integer/string forms, matching what ApiValueHelper.IsExited already tolerated for the old dictionary path. Reviewed with codex-rescue, correctness-reviewer and api-compat-reviewer before commit; both real findings above (the name/subdir key and the Exited string-form regression) came from that pass and are mutation-tested. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
9a92577794
commit
0026ef2b57
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents one guest not covered by any backup job, as returned by
|
||||
/// the /cluster/backup-info/not-backed-up endpoint.
|
||||
/// </summary>
|
||||
public class PveBackupInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the guest.
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the guest ("qemu" or "lxc").
|
||||
/// </summary>
|
||||
[JsonProperty("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// VMID of the guest.
|
||||
/// </summary>
|
||||
[JsonProperty("vmid")]
|
||||
public int? VmId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw landing spot for any key not mapped to a typed property above.
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
private IDictionary<string, JToken>? ExtensionData { get; set; }
|
||||
|
||||
private Dictionary<string, object?>? _additionalProperties;
|
||||
|
||||
/// <summary>
|
||||
/// Any keys not surfaced as a typed property above. Keys map to native
|
||||
/// .NET values so the dictionary works naturally in PowerShell pipelines.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, object?> AdditionalProperties =>
|
||||
_additionalProperties ??= ExtensionData == null
|
||||
? new Dictionary<string, object?>()
|
||||
: ExtensionData.ToDictionary(kvp => kvp.Key, kvp => JsonHelper.ToNative(kvp.Value));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type ?? "guest"} {VmId?.ToString() ?? "?"} ({Name ?? "N/A"}) not backed up";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="AdditionalProperties"/>.
|
||||
/// </summary>
|
||||
public class PveClusterConfigEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The sub-resource name (e.g. "nodes", "totem", "qdevice", "join", "apiversion").
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw landing spot for any key not mapped to a typed property above.
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
private IDictionary<string, JToken>? ExtensionData { get; set; }
|
||||
|
||||
private Dictionary<string, object?>? _additionalProperties;
|
||||
|
||||
/// <summary>
|
||||
/// Any keys not surfaced as a typed property above. Keys map to native
|
||||
/// .NET values so the dictionary works naturally in PowerShell pipelines.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, object?> AdditionalProperties =>
|
||||
_additionalProperties ??= ExtensionData == null
|
||||
? new Dictionary<string, object?>()
|
||||
: ExtensionData.ToDictionary(kvp => kvp.Key, kvp => JsonHelper.ToNative(kvp.Value));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Cluster Config Entry: {Name ?? "N/A"}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the configuration of a Proxmox VE node,
|
||||
/// as returned by the /nodes/{node}/config endpoint.
|
||||
/// </summary>
|
||||
public class PveNodeConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Node specific ACME settings.
|
||||
/// </summary>
|
||||
[JsonProperty("acme")]
|
||||
public string? Acme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// RAM usage target for ballooning, in percent of total memory.
|
||||
/// </summary>
|
||||
[JsonProperty("ballooning-target")]
|
||||
public int? BallooningTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Description for the node, shown in the web interface node notes panel.
|
||||
/// </summary>
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA1 digest of the current configuration file.
|
||||
/// </summary>
|
||||
[JsonProperty("digest")]
|
||||
public string? Digest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The location of the node, overriding the datacenter config default.
|
||||
/// </summary>
|
||||
[JsonProperty("location")]
|
||||
public string? Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initial delay in seconds before starting all on-boot Virtual Guests.
|
||||
/// </summary>
|
||||
[JsonProperty("startall-onboot-delay")]
|
||||
public int? StartAllOnbootDelay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Node specific wake-on-LAN settings.
|
||||
/// </summary>
|
||||
[JsonProperty("wakeonlan")]
|
||||
public string? WakeOnLan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw landing spot for any config key not mapped to a typed property above
|
||||
/// (e.g. per-domain "acmedomain0", "acmedomain1", ... entries).
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
private IDictionary<string, JToken>? ExtensionData { get; set; }
|
||||
|
||||
private Dictionary<string, object?>? _additionalProperties;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, object?> AdditionalProperties =>
|
||||
_additionalProperties ??= ExtensionData == null
|
||||
? new Dictionary<string, object?>()
|
||||
: ExtensionData.ToDictionary(kvp => kvp.Key, kvp => JsonHelper.ToNative(kvp.Value));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Node Config | Description: {Description ?? "N/A"} | WakeOnLan: {WakeOnLan ?? "N/A"}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the DNS configuration of a Proxmox VE node,
|
||||
/// as returned by the /nodes/{node}/dns endpoint.
|
||||
/// </summary>
|
||||
public class PveNodeDns
|
||||
{
|
||||
/// <summary>
|
||||
/// First name server IP address.
|
||||
/// </summary>
|
||||
[JsonProperty("dns1")]
|
||||
public string? Dns1 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Second name server IP address.
|
||||
/// </summary>
|
||||
[JsonProperty("dns2")]
|
||||
public string? Dns2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Third name server IP address.
|
||||
/// </summary>
|
||||
[JsonProperty("dns3")]
|
||||
public string? Dns3 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Search domain for host-name lookup.
|
||||
/// </summary>
|
||||
[JsonProperty("search")]
|
||||
public string? Search { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw landing spot for any DNS config key not mapped to a typed property above.
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
private IDictionary<string, JToken>? ExtensionData { get; set; }
|
||||
|
||||
private Dictionary<string, object?>? _additionalProperties;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, object?> AdditionalProperties =>
|
||||
_additionalProperties ??= ExtensionData == null
|
||||
? new Dictionary<string, object?>()
|
||||
: ExtensionData.ToDictionary(kvp => kvp.Key, kvp => JsonHelper.ToNative(kvp.Value));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"DNS | Search: {Search ?? "N/A"} | {Dns1 ?? "N/A"}, {Dns2 ?? "N/A"}, {Dns3 ?? "N/A"}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the status of a guest-agent command started via exec, as returned
|
||||
/// by the /nodes/{node}/qemu/{vmid}/agent/exec-status endpoint.
|
||||
/// </summary>
|
||||
public class PveGuestExecStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the command has exited yet. PVE has been observed sending this as
|
||||
/// a boolean, an integer (1/0), or a string ("1"/"0"); <see cref="TolerantBooleanConverter"/>
|
||||
/// normalizes all three.
|
||||
/// </summary>
|
||||
[JsonProperty("exited")]
|
||||
[JsonConverter(typeof(TolerantBooleanConverter))]
|
||||
public bool? Exited { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Process exit code, if it was normally terminated.
|
||||
/// </summary>
|
||||
[JsonProperty("exitcode")]
|
||||
public int? ExitCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Signal number or exception code, if the process was abnormally terminated.
|
||||
/// </summary>
|
||||
[JsonProperty("signal")]
|
||||
public int? Signal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Base64-encoded stdout of the process.
|
||||
/// </summary>
|
||||
[JsonProperty("out-data")]
|
||||
public string? OutData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Base64-encoded stderr of the process.
|
||||
/// </summary>
|
||||
[JsonProperty("err-data")]
|
||||
public string? ErrData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if stdout was not fully captured.
|
||||
/// </summary>
|
||||
[JsonProperty("out-truncated")]
|
||||
public bool? OutTruncated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if stderr was not fully captured.
|
||||
/// </summary>
|
||||
[JsonProperty("err-truncated")]
|
||||
public bool? ErrTruncated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw landing spot for any key not mapped to a typed property above.
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
private IDictionary<string, JToken>? ExtensionData { get; set; }
|
||||
|
||||
private Dictionary<string, object?>? _additionalProperties;
|
||||
|
||||
/// <summary>
|
||||
/// Any keys not surfaced as a typed property above. Keys map to native
|
||||
/// .NET values so the dictionary works naturally in PowerShell pipelines.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, object?> AdditionalProperties =>
|
||||
_additionalProperties ??= ExtensionData == null
|
||||
? new Dictionary<string, object?>()
|
||||
: ExtensionData.ToDictionary(kvp => kvp.Key, kvp => JsonHelper.ToNative(kvp.Value));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Exited == true
|
||||
? $"Exec Status | Exited, code {ExitCode?.ToString() ?? "N/A"}"
|
||||
: "Exec Status | Running";
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@ namespace PSProxmoxVE.Core.Services
|
||||
/// <summary>
|
||||
/// Returns the list of guests not covered by any backup job.
|
||||
/// </summary>
|
||||
public List<Dictionary<string, object?>> GetNotBackedUp(PveSession session)
|
||||
public List<PveBackupInfo> 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<List<PveBackupInfo>>() ?? new List<PveBackupInfo>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Dictionary<string, object?> GetClusterConfig(PveSession session)
|
||||
public List<PveClusterConfigEntry> 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<string, object?>();
|
||||
return data?.ToObject<List<PveClusterConfigEntry>>() ?? new List<PveClusterConfigEntry>();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace PSProxmoxVE.Core.Services
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="node">The cluster node name.</param>
|
||||
public Dictionary<string, object?> 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<PveNodeConfig>() ?? new PveNodeConfig();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace PSProxmoxVE.Core.Services
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="node">The cluster node name.</param>
|
||||
public Dictionary<string, object?> 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<PveNodeDns>() ?? new PveNodeDns();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -605,7 +605,7 @@ namespace PSProxmoxVE.Core.Services
|
||||
/// <summary>
|
||||
/// Gets the status/result of a guest agent exec command by PID.
|
||||
/// </summary>
|
||||
public Dictionary<string, object?> 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<PveGuestExecStatus>() ?? new PveGuestExecStatus();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -43,4 +43,25 @@ namespace PSProxmoxVE.Core.Utilities
|
||||
serializer.Serialize(writer, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="ApiValueHelper.IsExited"/>.
|
||||
/// </summary>
|
||||
public class TolerantBooleanConverter : JsonConverter<bool?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, bool? value, JsonSerializer serializer)
|
||||
{
|
||||
serializer.Serialize(writer, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Gets the cluster configuration.</para>
|
||||
/// <para type="description">
|
||||
/// 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).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Get, "PveClusterConfig")]
|
||||
[OutputType(typeof(Dictionary<string, object>))]
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Get, "PveNodeConfig")]
|
||||
[OutputType(typeof(PSObject))]
|
||||
[OutputType(typeof(PveNodeConfig))]
|
||||
public sealed class GetPveNodeConfigCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>The Proxmox VE node name.</summary>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Get, "PveNodeDns")]
|
||||
[OutputType(typeof(PSObject))]
|
||||
[OutputType(typeof(PveNodeDns))]
|
||||
public sealed class GetPveNodeDnsCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>The Proxmox VE node name.</summary>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, object?> 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);
|
||||
|
||||
@@ -2088,7 +2088,7 @@
|
||||
<command:returnValues>
|
||||
<command:returnValue>
|
||||
<dev:type>
|
||||
<maml:name>System.Management.Automation.PSObject</maml:name>
|
||||
<maml:name>PSProxmoxVE.Core.Models.Backup.PveBackupInfo</maml:name>
|
||||
</dev:type>
|
||||
<maml:description>
|
||||
<maml:para></maml:para>
|
||||
@@ -2476,7 +2476,7 @@
|
||||
<command:returnValues>
|
||||
<command:returnValue>
|
||||
<dev:type>
|
||||
<maml:name>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]]</maml:name>
|
||||
<maml:name>PSProxmoxVE.Core.Models.Cluster.PveClusterConfigEntry</maml:name>
|
||||
</dev:type>
|
||||
<maml:description>
|
||||
<maml:para></maml:para>
|
||||
@@ -6478,7 +6478,7 @@
|
||||
<command:returnValues>
|
||||
<command:returnValue>
|
||||
<dev:type>
|
||||
<maml:name>System.Management.Automation.PSObject</maml:name>
|
||||
<maml:name>PSProxmoxVE.Core.Models.Nodes.PveNodeConfig</maml:name>
|
||||
</dev:type>
|
||||
<maml:description>
|
||||
<maml:para></maml:para>
|
||||
@@ -6605,7 +6605,7 @@
|
||||
<command:returnValues>
|
||||
<command:returnValue>
|
||||
<dev:type>
|
||||
<maml:name>System.Management.Automation.PSObject</maml:name>
|
||||
<maml:name>PSProxmoxVE.Core.Models.Nodes.PveNodeDns</maml:name>
|
||||
</dev:type>
|
||||
<maml:description>
|
||||
<maml:para></maml:para>
|
||||
|
||||
@@ -1415,5 +1415,31 @@
|
||||
</TableControl>
|
||||
</View>
|
||||
|
||||
<!-- PSProxmoxVE.Core.Models.Cluster.PveClusterConfigEntry -->
|
||||
<View>
|
||||
<Name>PSProxmoxVE.Core.Models.Cluster.PveClusterConfigEntry</Name>
|
||||
<ViewSelectedBy>
|
||||
<TypeName>PSProxmoxVE.Core.Models.Cluster.PveClusterConfigEntry</TypeName>
|
||||
</ViewSelectedBy>
|
||||
<TableControl>
|
||||
<TableHeaders>
|
||||
<TableColumnHeader>
|
||||
<Label>Name</Label>
|
||||
<Width>20</Width>
|
||||
<Alignment>Left</Alignment>
|
||||
</TableColumnHeader>
|
||||
</TableHeaders>
|
||||
<TableRowEntries>
|
||||
<TableRowEntry>
|
||||
<TableColumnItems>
|
||||
<TableColumnItem>
|
||||
<PropertyName>Name</PropertyName>
|
||||
</TableColumnItem>
|
||||
</TableColumnItems>
|
||||
</TableRowEntry>
|
||||
</TableRowEntries>
|
||||
</TableControl>
|
||||
</View>
|
||||
|
||||
</ViewDefinitions>
|
||||
</Configuration>
|
||||
|
||||
@@ -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<PveBackupInfo[]>();
|
||||
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<PveBackupInfo[]>();
|
||||
Assert.NotNull(items);
|
||||
Assert.Equal("prod", items![0].AdditionalProperties["comment"]);
|
||||
Assert.False(items[0].AdditionalProperties.ContainsKey("vmid"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<System.Collections.Generic.List<PveClusterConfigEntry>>();
|
||||
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<System.Collections.Generic.List<PveClusterConfigEntry>>();
|
||||
Assert.NotNull(entries);
|
||||
Assert.Equal("x", entries![0].AdditionalProperties["extra"]);
|
||||
Assert.False(entries[0].AdditionalProperties.ContainsKey("name"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PveNodeConfig>();
|
||||
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<PveNodeConfig>();
|
||||
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<PveNodeDns>();
|
||||
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<PveNodeDns>();
|
||||
Assert.NotNull(dns);
|
||||
Assert.Equal("9.9.9.9", dns!.AdditionalProperties["dns4"]);
|
||||
Assert.False(dns.AdditionalProperties.ContainsKey("dns1"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PveGuestExecStatus>();
|
||||
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<PveGuestExecStatus>();
|
||||
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<PveGuestExecStatus>();
|
||||
Assert.NotNull(status);
|
||||
Assert.Equal("future", status!.AdditionalProperties["newfield"]);
|
||||
Assert.False(status.AdditionalProperties.ContainsKey("exited"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<Dictionary<string, object?>>>(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]
|
||||
|
||||
@@ -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<IPveHttpClient>();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IPveHttpClient>();
|
||||
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<Dictionary<string, object?>>(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<IPveHttpClient>();
|
||||
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<Dictionary<string, object?>>(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IPveHttpClient>();
|
||||
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<IPveHttpClient>();
|
||||
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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user