mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-07-26 07:58:14 +00:00
feat: emit native types instead of Newtonsoft JObject/JArray in public APIs
Replace all Newtonsoft.Json.Linq types (JObject, JArray, JToken) exposed in public service methods, model properties, and cmdlet OutputTypes with native .NET types (Dictionary<string, object?>, List<Dictionary<string, object?>>, PSObject). New utilities: - JsonHelper: ToNative, ToDictionary, ToListOfDictionaries for recursive JToken→native conversion - NativeListConverter / NativeDictionaryConverter: JsonConverters for model properties that deserialize JArray/JObject to native types Services updated (8 methods): - NodeService: GetNodeConfig, GetNodeDns → Dictionary<string, object?> - BackupService: GetNotBackedUp → List<Dictionary<string, object?>> - ClusterConfigService: GetClusterConfig, GetTotem, GetQdevice → Dictionary - HaService: GetManagerStatus → Dictionary<string, object?> - VmService: GetGuestExecStatus → Dictionary<string, object?> Models updated (2 properties): - PvePool.Members: JArray → List<Dictionary<string, object?>> - PveHaRule.Properties: JObject → Dictionary<string, object?> Cmdlets updated (5): - GetPveClusterConfigCmdlet: OutputType JToken → Dictionary<string, object> - GetPveNodeConfigCmdlet: iteration updated for Dictionary - GetPveNodeDnsCmdlet: iteration updated for Dictionary - GetPveBackupInfoCmdlet: iteration updated for List<Dictionary> - InvokePveVmGuestExecCmdlet: polling updated for Dictionary Added D013 to DECISIONS.md: cmdlets must emit only native or module-defined types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -358,3 +358,52 @@ extracted to `const string` fields for maintainability.
|
|||||||
Auth header names (`PVEAPIToken=`, `CSRFPreventionToken`) were inline string literals
|
Auth header names (`PVEAPIToken=`, `CSRFPreventionToken`) were inline string literals
|
||||||
used in multiple places. Extracting to `const string ApiTokenPrefix` and
|
used in multiple places. Extracting to `const string ApiTokenPrefix` and
|
||||||
`CsrfHeaderName` fields improves maintainability and reduces typo risk.
|
`CsrfHeaderName` fields improves maintainability and reduces typo risk.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D013 — Cmdlets must emit only native or module-defined types
|
||||||
|
|
||||||
|
**Status**: Active
|
||||||
|
**Finding refs**: F085
|
||||||
|
**Resolved in scan**: 2026-03-25
|
||||||
|
|
||||||
|
### Decision
|
||||||
|
All cmdlet output types and public model properties must be native .NET types (`string`,
|
||||||
|
`int`, `bool`, `Dictionary<string, object?>`, `List<T>`, `PSObject`, `void`) or types
|
||||||
|
defined within the module itself (`Pve*` classes). Never expose third-party types like
|
||||||
|
Newtonsoft's `JObject`, `JArray`, or `JToken` in public APIs.
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
PowerShell enumerates `JArray` unexpectedly and `JObject` properties are not discoverable
|
||||||
|
via `Get-Member` or tab completion. Users piping module output into `Format-Table`,
|
||||||
|
`Select-Object`, or `Where-Object` get confusing behavior when the underlying type is a
|
||||||
|
Newtonsoft container. Native dictionaries and lists work naturally in PowerShell pipelines.
|
||||||
|
|
||||||
|
### Anti-pattern (do not reintroduce)
|
||||||
|
```csharp
|
||||||
|
// Service returning Newtonsoft type
|
||||||
|
public JObject GetNodeConfig(...) { ... }
|
||||||
|
|
||||||
|
// Model exposing Newtonsoft type
|
||||||
|
[JsonProperty("members")]
|
||||||
|
public JArray? Members { get; set; }
|
||||||
|
|
||||||
|
// Cmdlet OutputType referencing Newtonsoft type
|
||||||
|
[OutputType(typeof(JObject))]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Correct pattern
|
||||||
|
```csharp
|
||||||
|
// Service returns native dictionary
|
||||||
|
public Dictionary<string, object?> GetNodeConfig(...) { ... }
|
||||||
|
|
||||||
|
// Model uses native type with converter for deserialization
|
||||||
|
[JsonProperty("members")]
|
||||||
|
[JsonConverter(typeof(NativeListConverter))]
|
||||||
|
public List<Dictionary<string, object?>>? Members { get; set; }
|
||||||
|
|
||||||
|
// Cmdlet OutputType uses native or module type
|
||||||
|
[OutputType(typeof(Dictionary<string, object>))]
|
||||||
|
// or
|
||||||
|
[OutputType(typeof(PSObject))]
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using PSProxmoxVE.Core.Utilities;
|
||||||
|
|
||||||
namespace PSProxmoxVE.Core.Models.Cluster;
|
namespace PSProxmoxVE.Core.Models.Cluster;
|
||||||
|
|
||||||
@@ -21,11 +22,12 @@ public class PvePool
|
|||||||
public string? Comment { get; set; }
|
public string? Comment { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The pool members (VMs, containers, storage). Returned as a raw array
|
/// The pool members (VMs, containers, storage). Returned as a list of dictionaries
|
||||||
/// when querying a specific pool.
|
/// when querying a specific pool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("members")]
|
[JsonProperty("members")]
|
||||||
public JArray? Members { get; set; }
|
[JsonConverter(typeof(NativeListConverter))]
|
||||||
|
public List<Dictionary<string, object?>>? Members { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using PSProxmoxVE.Core.Utilities;
|
||||||
|
|
||||||
namespace PSProxmoxVE.Core.Models.HA;
|
namespace PSProxmoxVE.Core.Models.HA;
|
||||||
|
|
||||||
@@ -43,7 +44,8 @@ public class PveHaRule
|
|||||||
/// Rule-specific properties that vary by rule type.
|
/// Rule-specific properties that vary by rule type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("properties")]
|
[JsonProperty("properties")]
|
||||||
public JObject? Properties { get; set; }
|
[JsonConverter(typeof(NativeDictionaryConverter))]
|
||||||
|
public Dictionary<string, object?>? Properties { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using PSProxmoxVE.Core.Authentication;
|
|||||||
using PSProxmoxVE.Core.Client;
|
using PSProxmoxVE.Core.Client;
|
||||||
using PSProxmoxVE.Core.Models.Backup;
|
using PSProxmoxVE.Core.Models.Backup;
|
||||||
using PSProxmoxVE.Core.Models.Vms;
|
using PSProxmoxVE.Core.Models.Vms;
|
||||||
|
using PSProxmoxVE.Core.Utilities;
|
||||||
|
|
||||||
namespace PSProxmoxVE.Core.Services
|
namespace PSProxmoxVE.Core.Services
|
||||||
{
|
{
|
||||||
@@ -173,7 +174,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the list of guests not covered by any backup job.
|
/// Returns the list of guests not covered by any backup job.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public JArray GetNotBackedUp(PveSession session)
|
public List<Dictionary<string, object?>> GetNotBackedUp(PveSession session)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
@@ -183,7 +184,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
var response = client.GetAsync("cluster/backup-info/not-backed-up")
|
var response = client.GetAsync("cluster/backup-info/not-backed-up")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data as JArray ?? new JArray();
|
return JsonHelper.ToListOfDictionaries(data as JArray);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
|
|||||||
using PSProxmoxVE.Core.Authentication;
|
using PSProxmoxVE.Core.Authentication;
|
||||||
using PSProxmoxVE.Core.Client;
|
using PSProxmoxVE.Core.Client;
|
||||||
using PSProxmoxVE.Core.Models.Cluster;
|
using PSProxmoxVE.Core.Models.Cluster;
|
||||||
|
using PSProxmoxVE.Core.Utilities;
|
||||||
|
|
||||||
namespace PSProxmoxVE.Core.Services
|
namespace PSProxmoxVE.Core.Services
|
||||||
{
|
{
|
||||||
@@ -31,9 +32,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the cluster configuration directory (GET /cluster/config).
|
/// Returns the cluster configuration directory (GET /cluster/config).
|
||||||
/// The response is a mixed structure returned as a raw JObject.
|
/// The response is a mixed structure returned as a Dictionary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public JObject GetClusterConfig(PveSession session)
|
public Dictionary<string, object?> GetClusterConfig(PveSession session)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
var response = client.GetAsync("cluster/config").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/config").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data as JObject ?? new JObject();
|
return data is JObject obj ? JsonHelper.ToDictionary(obj) : new Dictionary<string, object?>();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -258,7 +259,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the Corosync totem configuration (GET /cluster/config/totem).
|
/// Returns the Corosync totem configuration (GET /cluster/config/totem).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public JObject GetTotem(PveSession session)
|
public Dictionary<string, object?> GetTotem(PveSession session)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
@@ -267,7 +268,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
var response = client.GetAsync("cluster/config/totem").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/config/totem").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data as JObject ?? new JObject();
|
return JsonHelper.ToDictionary(data as JObject);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -278,7 +279,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the external quorum device (qdevice) status (GET /cluster/config/qdevice).
|
/// Returns the external quorum device (qdevice) status (GET /cluster/config/qdevice).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public JObject GetQdevice(PveSession session)
|
public Dictionary<string, object?> GetQdevice(PveSession session)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
@@ -287,7 +288,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
var response = client.GetAsync("cluster/config/qdevice").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/config/qdevice").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data as JObject ?? new JObject();
|
return JsonHelper.ToDictionary(data as JObject);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
|
|||||||
using PSProxmoxVE.Core.Authentication;
|
using PSProxmoxVE.Core.Authentication;
|
||||||
using PSProxmoxVE.Core.Client;
|
using PSProxmoxVE.Core.Client;
|
||||||
using PSProxmoxVE.Core.Models.HA;
|
using PSProxmoxVE.Core.Models.HA;
|
||||||
|
using PSProxmoxVE.Core.Utilities;
|
||||||
|
|
||||||
namespace PSProxmoxVE.Core.Services
|
namespace PSProxmoxVE.Core.Services
|
||||||
{
|
{
|
||||||
@@ -357,7 +358,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the full HA manager status as a raw JSON object.
|
/// Returns the full HA manager status as a raw JSON object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public JObject GetManagerStatus(PveSession session)
|
public Dictionary<string, object?> GetManagerStatus(PveSession session)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
@@ -366,7 +367,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
var response = client.GetAsync("cluster/ha/status/manager_status").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/ha/status/manager_status").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data as JObject ?? new JObject();
|
return JsonHelper.ToDictionary(data as JObject);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using PSProxmoxVE.Core.Authentication;
|
|||||||
using PSProxmoxVE.Core.Client;
|
using PSProxmoxVE.Core.Client;
|
||||||
using PSProxmoxVE.Core.Models.Nodes;
|
using PSProxmoxVE.Core.Models.Nodes;
|
||||||
using PSProxmoxVE.Core.Models.Vms;
|
using PSProxmoxVE.Core.Models.Vms;
|
||||||
|
using PSProxmoxVE.Core.Utilities;
|
||||||
|
|
||||||
namespace PSProxmoxVE.Core.Services
|
namespace PSProxmoxVE.Core.Services
|
||||||
{
|
{
|
||||||
@@ -78,7 +79,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
/// <param name="node">The cluster node name.</param>
|
/// <param name="node">The cluster node name.</param>
|
||||||
public JObject GetNodeConfig(PveSession session, string node)
|
public Dictionary<string, object?> GetNodeConfig(PveSession session, string node)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
@@ -88,7 +89,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/config").GetAwaiter().GetResult();
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/config").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data as JObject ?? new JObject();
|
return JsonHelper.ToDictionary(data as JObject);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -124,7 +125,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
/// <param name="node">The cluster node name.</param>
|
/// <param name="node">The cluster node name.</param>
|
||||||
public JObject GetNodeDns(PveSession session, string node)
|
public Dictionary<string, object?> GetNodeDns(PveSession session, string node)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
@@ -134,7 +135,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/dns").GetAwaiter().GetResult();
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/dns").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data as JObject ?? new JObject();
|
return JsonHelper.ToDictionary(data as JObject);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using PSProxmoxVE.Core.Authentication;
|
|||||||
using PSProxmoxVE.Core.Client;
|
using PSProxmoxVE.Core.Client;
|
||||||
using PSProxmoxVE.Core.Models.Nodes;
|
using PSProxmoxVE.Core.Models.Nodes;
|
||||||
using PSProxmoxVE.Core.Models.Vms;
|
using PSProxmoxVE.Core.Models.Vms;
|
||||||
|
using PSProxmoxVE.Core.Utilities;
|
||||||
|
|
||||||
namespace PSProxmoxVE.Core.Services
|
namespace PSProxmoxVE.Core.Services
|
||||||
{
|
{
|
||||||
@@ -624,7 +625,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the status/result of a guest agent exec command by PID.
|
/// Gets the status/result of a guest agent exec command by PID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public JObject GetGuestExecStatus(PveSession session, string node, int vmid, int pid)
|
public Dictionary<string, object?> GetGuestExecStatus(PveSession session, string node, int vmid, int pid)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
@@ -634,7 +635,8 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/exec-status?pid={pid}")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/exec-status?pid={pid}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return JObject.Parse(response)["data"] as JObject ?? new JObject();
|
var data = JObject.Parse(response)["data"];
|
||||||
|
return JsonHelper.ToDictionary(data as JObject);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides helper methods for converting Newtonsoft.Json types to native .NET types.
|
||||||
|
/// </summary>
|
||||||
|
public static class JsonHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a JToken to a native .NET type recursively.
|
||||||
|
/// JObject becomes Dictionary<string, object>, JArray becomes List<object>, JValue becomes primitive.
|
||||||
|
/// </summary>
|
||||||
|
public static object? ToNative(JToken? token)
|
||||||
|
{
|
||||||
|
if (token == null || token.Type == JTokenType.Null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
switch (token)
|
||||||
|
{
|
||||||
|
case JObject obj:
|
||||||
|
return obj.Properties().ToDictionary(
|
||||||
|
p => p.Name,
|
||||||
|
p => ToNative(p.Value));
|
||||||
|
|
||||||
|
case JArray arr:
|
||||||
|
return arr.Select(ToNative).ToList();
|
||||||
|
|
||||||
|
case JValue val:
|
||||||
|
return val.Value;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return token.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a JObject to a Dictionary<string, object?>.
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, object?> ToDictionary(JObject? obj)
|
||||||
|
{
|
||||||
|
if (obj == null) return new Dictionary<string, object?>();
|
||||||
|
return obj.Properties().ToDictionary(
|
||||||
|
p => p.Name,
|
||||||
|
p => ToNative(p.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a JArray to a List of Dictionaries.
|
||||||
|
/// Each element should be a JObject; non-object elements are skipped.
|
||||||
|
/// </summary>
|
||||||
|
public static List<Dictionary<string, object?>> ToListOfDictionaries(JArray? arr)
|
||||||
|
{
|
||||||
|
if (arr == null) return new List<Dictionary<string, object?>>();
|
||||||
|
return arr.OfType<JObject>().Select(ToDictionary).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON converter that deserializes a JSON array into a List of Dictionary<string, object?>.
|
||||||
|
/// Each array element is expected to be an object; non-object elements are skipped.
|
||||||
|
/// </summary>
|
||||||
|
public class NativeListConverter : JsonConverter<List<Dictionary<string, object?>>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override List<Dictionary<string, object?>> ReadJson(JsonReader reader, Type objectType, List<Dictionary<string, object?>>? existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
var token = JToken.Load(reader);
|
||||||
|
return token is JArray arr ? JsonHelper.ToListOfDictionaries(arr) : new List<Dictionary<string, object?>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void WriteJson(JsonWriter writer, List<Dictionary<string, object?>>? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
serializer.Serialize(writer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON converter that deserializes a JSON object into a Dictionary<string, object?>.
|
||||||
|
/// </summary>
|
||||||
|
public class NativeDictionaryConverter : JsonConverter<Dictionary<string, object?>>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Dictionary<string, object?> ReadJson(JsonReader reader, Type objectType, Dictionary<string, object?>? existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
var token = JToken.Load(reader);
|
||||||
|
return token is JObject obj ? JsonHelper.ToDictionary(obj) : new Dictionary<string, object?>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void WriteJson(JsonWriter writer, Dictionary<string, object?>? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
serializer.Serialize(writer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Management.Automation;
|
using System.Management.Automation;
|
||||||
using PSProxmoxVE.Core.Services;
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
@@ -26,12 +27,9 @@ namespace PSProxmoxVE.Cmdlets.Backup
|
|||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
var pso = new PSObject();
|
var pso = new PSObject();
|
||||||
if (item is Newtonsoft.Json.Linq.JObject jObj)
|
foreach (var kvp in item)
|
||||||
{
|
{
|
||||||
foreach (var prop in jObj.Properties())
|
pso.Properties.Add(new PSNoteProperty(kvp.Key, kvp.Value));
|
||||||
{
|
|
||||||
pso.Properties.Add(new PSNoteProperty(prop.Name, prop.Value?.ToObject<object>()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
WriteObject(pso);
|
WriteObject(pso);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Management.Automation;
|
using System.Management.Automation;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using PSProxmoxVE.Core.Services;
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
namespace PSProxmoxVE.Cmdlets.Cluster
|
namespace PSProxmoxVE.Cmdlets.Cluster
|
||||||
@@ -12,7 +12,7 @@ namespace PSProxmoxVE.Cmdlets.Cluster
|
|||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Cmdlet(VerbsCommon.Get, "PveClusterConfig")]
|
[Cmdlet(VerbsCommon.Get, "PveClusterConfig")]
|
||||||
[OutputType(typeof(JObject))]
|
[OutputType(typeof(Dictionary<string, object>))]
|
||||||
public sealed class GetPveClusterConfigCmdlet : PveCmdletBase
|
public sealed class GetPveClusterConfigCmdlet : PveCmdletBase
|
||||||
{
|
{
|
||||||
protected override void ProcessRecord()
|
protected override void ProcessRecord()
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ namespace PSProxmoxVE.Cmdlets.Nodes
|
|||||||
var config = service.GetNodeConfig(session, Node);
|
var config = service.GetNodeConfig(session, Node);
|
||||||
|
|
||||||
var psObj = new PSObject();
|
var psObj = new PSObject();
|
||||||
foreach (var prop in config.Properties())
|
foreach (var kvp in config)
|
||||||
{
|
{
|
||||||
psObj.Properties.Add(new PSNoteProperty(prop.Name, prop.Value.ToString()));
|
psObj.Properties.Add(new PSNoteProperty(kvp.Key, kvp.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteObject(psObj);
|
WriteObject(psObj);
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ namespace PSProxmoxVE.Cmdlets.Nodes
|
|||||||
var dns = service.GetNodeDns(session, Node);
|
var dns = service.GetNodeDns(session, Node);
|
||||||
|
|
||||||
var psObj = new PSObject();
|
var psObj = new PSObject();
|
||||||
foreach (var prop in dns.Properties())
|
foreach (var kvp in dns)
|
||||||
{
|
{
|
||||||
psObj.Properties.Add(new PSNoteProperty(prop.Name, prop.Value.ToString()));
|
psObj.Properties.Add(new PSNoteProperty(kvp.Key, kvp.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteObject(psObj);
|
WriteObject(psObj);
|
||||||
|
|||||||
@@ -53,19 +53,19 @@ namespace PSProxmoxVE.Cmdlets.Vms
|
|||||||
// Poll for completion with timeout
|
// Poll for completion with timeout
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
var deadline = TimeSpan.FromSeconds(Timeout);
|
var deadline = TimeSpan.FromSeconds(Timeout);
|
||||||
Newtonsoft.Json.Linq.JObject result;
|
System.Collections.Generic.Dictionary<string, object?> result;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
System.Threading.Thread.Sleep(1000);
|
System.Threading.Thread.Sleep(1000);
|
||||||
if (sw.Elapsed >= deadline)
|
if (sw.Elapsed >= deadline)
|
||||||
throw new TimeoutException($"Guest command did not complete within {Timeout} seconds.");
|
throw new TimeoutException($"Guest command did not complete within {Timeout} seconds.");
|
||||||
result = service.GetGuestExecStatus(session, Node, VmId, pid);
|
result = service.GetGuestExecStatus(session, Node, VmId, pid);
|
||||||
} while (result["exited"]?.ToObject<int>() != 1);
|
} while (!result.TryGetValue("exited", out var exited) || !Equals(exited, 1L));
|
||||||
|
|
||||||
var output = new PSObject();
|
var output = new PSObject();
|
||||||
output.Properties.Add(new PSNoteProperty("ExitCode", result["exitcode"]?.ToObject<int>() ?? -1));
|
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["out-data"]?.ToString())));
|
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["err-data"]?.ToString())));
|
output.Properties.Add(new PSNoteProperty("Stderr", DecodeBase64(result.TryGetValue("err-data", out var ed) ? ed?.ToString() : null)));
|
||||||
output.Properties.Add(new PSNoteProperty("Pid", pid));
|
output.Properties.Add(new PSNoteProperty("Pid", pid));
|
||||||
|
|
||||||
WriteObject(output);
|
WriteObject(output);
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ namespace PSProxmoxVE.Core.Tests.Services
|
|||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetNotBackedUp_ReturnsJArray()
|
public void GetNotBackedUp_ReturnsListOfDictionaries()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var json = @"{
|
var json = @"{
|
||||||
@@ -379,14 +379,14 @@ namespace PSProxmoxVE.Core.Tests.Services
|
|||||||
var result = service.GetNotBackedUp(CreateSession());
|
var result = service.GetNotBackedUp(CreateSession());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsType<JArray>(result);
|
Assert.IsType<List<Dictionary<string, object?>>>(result);
|
||||||
Assert.Equal(2, result.Count);
|
Assert.Equal(2, result.Count);
|
||||||
Assert.Equal(100, result[0]["vmid"]!.Value<int>());
|
Assert.Equal(100L, result[0]["vmid"]);
|
||||||
Assert.Equal("webserver", result[0]["name"]!.Value<string>());
|
Assert.Equal("webserver", result[0]["name"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetNotBackedUp_EmptyData_ReturnsEmptyJArray()
|
public void GetNotBackedUp_EmptyData_ReturnsEmptyList()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var json = @"{""data"": []}";
|
var json = @"{""data"": []}";
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ namespace PSProxmoxVE.Core.Tests.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetNodeConfig_ReturnsJObject()
|
public void GetNodeConfig_ReturnsDictionary()
|
||||||
{
|
{
|
||||||
// Arrange
|
// 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""}}";
|
||||||
@@ -84,6 +84,7 @@ namespace PSProxmoxVE.Core.Tests.Services
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(config);
|
Assert.NotNull(config);
|
||||||
|
Assert.IsType<Dictionary<string, object?>>(config);
|
||||||
Assert.Equal("Primary node", config["description"]?.ToString());
|
Assert.Equal("Primary node", config["description"]?.ToString());
|
||||||
Assert.Equal("AA:BB:CC:DD:EE:FF", config["wakeonlan"]?.ToString());
|
Assert.Equal("AA:BB:CC:DD:EE:FF", config["wakeonlan"]?.ToString());
|
||||||
mockClient.Verify(c => c.GetAsync("nodes/pve1/config"), Times.Once);
|
mockClient.Verify(c => c.GetAsync("nodes/pve1/config"), Times.Once);
|
||||||
@@ -116,7 +117,7 @@ namespace PSProxmoxVE.Core.Tests.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetNodeDns_ReturnsJObject()
|
public void GetNodeDns_ReturnsDictionary()
|
||||||
{
|
{
|
||||||
// Arrange
|
// 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""}}";
|
||||||
@@ -129,6 +130,7 @@ namespace PSProxmoxVE.Core.Tests.Services
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(dns);
|
Assert.NotNull(dns);
|
||||||
|
Assert.IsType<Dictionary<string, object?>>(dns);
|
||||||
Assert.Equal("8.8.8.8", dns["dns1"]?.ToString());
|
Assert.Equal("8.8.8.8", dns["dns1"]?.ToString());
|
||||||
Assert.Equal("8.8.4.4", dns["dns2"]?.ToString());
|
Assert.Equal("8.8.4.4", dns["dns2"]?.ToString());
|
||||||
Assert.Equal("example.com", dns["search"]?.ToString());
|
Assert.Equal("example.com", dns["search"]?.ToString());
|
||||||
|
|||||||
Reference in New Issue
Block a user