mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-06 12:09:01 +00:00
089bb7c81e
* refactor: one task-response parser, and the dead code #154 names (part A) Unifies the 8 byte-similar private ParseTask methods in BackupService, ContainerService, NetworkService, NodeService, SnapshotService, StorageService, TemplateService, and VmService into one shared PveTaskResponse.Parse(json, node) utility, on the variant that stamps Status = "running" for a bare-UPID response. BackupService, NodeService, TemplateService, and VmService previously left Status null for that case; their non--Wait task output now reports "running" like the other four services already did. Removes the duplicate ClusterConfigService.GetClusterStatus in favor of ClusterService's; ClusterConfigService now holds a ClusterService built from the same injected/default client, and WaitForQuorum and Get-PveClusterStatus go through it. Removes dead code named in issue #154 and its fold-in comments: the never-called PveHttpClient/IPveHttpClient sync wrappers Put/Delete, the never-thrown PveAuthenticationException, a #pragma around an already-nullable field, the unreferenced TestHelper mock-handler helpers, three hand-rolled version-warning blocks (now PveCmdletBase.WarnIfBelowVersion), an unreachable catch(HttpRequestException) arm in WaitForStatusTransition, dead ExitStatus-checking branches in ImportPveOvaCmdlet after WaitForTask (which already throws on failure), and an unreachable int branch in ApiValueHelper.IsExited. Fixes the WaitForStatusTransition catch removal's premise: PveHttpClient read the response body outside its HttpRequestException try block, so a mid-body stream drop could still escape unwrapped. Moves the body read inside the try so every HttpRequestException the client can throw becomes a PveApiException, matching what the removed catch assumed. Part of #154. * Add service files: BackupService, ClusterConfigService, ContainerService, NetworkService * Add service files: NodeService, SnapshotService, StorageService, TemplateService * Add VmService and Utilities files * Remove unused PveAuthenticationException (never thrown, caught, or tested) * Add cmdlet files: GetPveClusterStatus, SDN subnets, PveCmdletBase, SendPveFile, ImportPveOva * Add test files: BackupServiceTests, ClusterConfigServiceTests, NodeServiceTests * Add remaining test files: TemplateServiceTests, VmServiceTests, TestHelper, ApiValueHelperTests, PveTaskResponseTests * Add VmServiceTests --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
198 lines
8.9 KiB
C#
198 lines
8.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using PSProxmoxVE.Core.Authentication;
|
|
using PSProxmoxVE.Core.Client;
|
|
using PSProxmoxVE.Core.Models.Nodes;
|
|
using PSProxmoxVE.Core.Models.Vms;
|
|
using PSProxmoxVE.Core.Utilities;
|
|
|
|
namespace PSProxmoxVE.Core.Services
|
|
{
|
|
/// <summary>
|
|
/// Service for node-level and cluster-version Proxmox VE API operations.
|
|
/// </summary>
|
|
public class NodeService : PveServiceBase
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of <see cref="NodeService"/> with no injected client.
|
|
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
|
|
/// </summary>
|
|
public NodeService() { }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of <see cref="NodeService"/> with an injected HTTP client.
|
|
/// The caller owns the client's lifetime; this service will not dispose it.
|
|
/// </summary>
|
|
/// <param name="client">The HTTP client to use for all requests.</param>
|
|
public NodeService(IPveHttpClient client) : base(client) { }
|
|
|
|
/// <summary>
|
|
/// Returns all cluster nodes.
|
|
/// </summary>
|
|
public PveNode[] GetNodes(PveSession session)
|
|
{
|
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
|
|
|
return Invoke(session, client =>
|
|
{
|
|
var response = client.GetAsync("nodes").GetAwaiter().GetResult();
|
|
var data = JObject.Parse(response)["data"]
|
|
?? throw new InvalidOperationException("Response did not contain a 'data' field.");
|
|
return data.ToObject<PveNode[]>() ?? Array.Empty<PveNode>();
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns detailed status for a specific node.
|
|
/// </summary>
|
|
public PveNodeStatus GetNodeStatus(PveSession session, string node)
|
|
{
|
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
|
|
|
return Invoke(session, client =>
|
|
{
|
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/status").GetAwaiter().GetResult();
|
|
var data = JObject.Parse(response)["data"]
|
|
?? throw new InvalidOperationException("Response did not contain a 'data' field.");
|
|
var status = data.ToObject<PveNodeStatus>()
|
|
?? throw new InvalidOperationException("Failed to deserialize node status.");
|
|
// The /nodes/{node}/status response does not include the node name.
|
|
if (string.IsNullOrEmpty(status.Node))
|
|
status.Node = node;
|
|
return status;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the configuration of a specific node.
|
|
/// </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)
|
|
{
|
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
|
|
|
return Invoke(session, client =>
|
|
{
|
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/config").GetAwaiter().GetResult();
|
|
var data = JObject.Parse(response)["data"];
|
|
return JsonHelper.ToDictionary(data as JObject);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the configuration of a specific node.
|
|
/// </summary>
|
|
/// <param name="session">The authenticated PVE session.</param>
|
|
/// <param name="node">The cluster node name.</param>
|
|
/// <param name="config">Configuration parameters to update.</param>
|
|
public void SetNodeConfig(PveSession session, string node, Dictionary<string, string> config)
|
|
{
|
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
|
|
|
Invoke(session, client =>
|
|
{
|
|
client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/config", config).GetAwaiter().GetResult();
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the DNS configuration of a specific node.
|
|
/// </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)
|
|
{
|
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
|
|
|
return Invoke(session, client =>
|
|
{
|
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/dns").GetAwaiter().GetResult();
|
|
var data = JObject.Parse(response)["data"];
|
|
return JsonHelper.ToDictionary(data as JObject);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the DNS configuration of a specific node.
|
|
/// </summary>
|
|
/// <param name="session">The authenticated PVE session.</param>
|
|
/// <param name="node">The cluster node name.</param>
|
|
/// <param name="config">DNS configuration parameters to update.</param>
|
|
public void SetNodeDns(PveSession session, string node, Dictionary<string, string> config)
|
|
{
|
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
|
|
|
Invoke(session, client =>
|
|
{
|
|
client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/dns", config).GetAwaiter().GetResult();
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts all VMs and containers on a node. Returns the task UPID.
|
|
/// </summary>
|
|
/// <param name="session">The authenticated PVE session.</param>
|
|
/// <param name="node">The cluster node name.</param>
|
|
/// <param name="config">Optional parameters (e.g. vms to limit which VMs start).</param>
|
|
public PveTask StartAll(PveSession session, string node, Dictionary<string, string>? config = null)
|
|
{
|
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
|
|
|
var formData = config ?? new Dictionary<string, string>();
|
|
return Invoke(session, client =>
|
|
{
|
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/startall", formData).GetAwaiter().GetResult();
|
|
return PveTaskResponse.Parse(response, node);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops all VMs and containers on a node. Returns the task UPID.
|
|
/// </summary>
|
|
/// <param name="session">The authenticated PVE session.</param>
|
|
/// <param name="node">The cluster node name.</param>
|
|
/// <param name="config">Optional parameters (e.g. vms, force-stop).</param>
|
|
public PveTask StopAll(PveSession session, string node, Dictionary<string, string>? config = null)
|
|
{
|
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
|
|
|
var formData = config ?? new Dictionary<string, string>();
|
|
return Invoke(session, client =>
|
|
{
|
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/stopall", formData).GetAwaiter().GetResult();
|
|
return PveTaskResponse.Parse(response, node);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the Proxmox VE version running on the server.
|
|
/// </summary>
|
|
public PveVersion GetVersion(PveSession session)
|
|
{
|
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
|
|
|
return Invoke(session, client =>
|
|
{
|
|
var response = client.GetAsync("version").GetAwaiter().GetResult();
|
|
var data = JObject.Parse(response)["data"];
|
|
var versionStr = data?["version"]?.ToString();
|
|
if (string.IsNullOrEmpty(versionStr))
|
|
throw new InvalidOperationException("Failed to retrieve PVE version from API response.");
|
|
return PveVersion.Parse(versionStr!);
|
|
});
|
|
}
|
|
}
|
|
}
|