mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-08-30 00:47:07 +00:00
feat: add cluster config and HA management cmdlets (F060, F053)
Cluster Config (F060) — 11 new cmdlets: - Get-PveClusterStatus, Get-PveClusterNextId - Get/Set-PveClusterOption (datacenter settings) - Get-PveClusterConfig, Get-PveClusterConfigNode - Add/Remove-PveClusterConfigNode (cluster membership) - Get-PveClusterJoinInfo, Add-PveClusterMember (join workflow) - New-PveCluster (create cluster) HA Management (F053) — 14 new cmdlets: - Get/New/Set/Remove-PveHaResource (HA managed VMs/CTs) - Move-PveHaResource (migrate/relocate via HA manager) - Get/New/Set/Remove-PveHaGroup (node groups + priorities) - Get-PveHaStatus (HA manager status) - Get/New/Set/Remove-PveHaRule (PVE 9.0+ version-gated) All cmdlets follow established conventions: - sealed classes with [OutputType] - ConfirmImpact.High on destructive operations (D006) - SecureString for password parameter in Add-PveClusterMember (D002) - Uri.EscapeDataString on all path segments (D003) - Verb class constants (D011) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PSProxmoxVE.Core.Models.Cluster;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a node entry from the GET /cluster/config/nodes endpoint.
|
||||
/// </summary>
|
||||
public class PveClusterConfigNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The node name.
|
||||
/// </summary>
|
||||
[JsonProperty("node")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The numeric node ID within the cluster Corosync configuration.
|
||||
/// </summary>
|
||||
[JsonProperty("nodeid")]
|
||||
public int? NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The address used for Corosync ring 0 communication.
|
||||
/// </summary>
|
||||
[JsonProperty("ring0_addr")]
|
||||
public string? Ring0Addr { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The address used for Corosync ring 1 communication.
|
||||
/// </summary>
|
||||
[JsonProperty("ring1_addr")]
|
||||
public string? Ring1Addr { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of quorum votes assigned to this node.
|
||||
/// </summary>
|
||||
[JsonProperty("quorum_votes")]
|
||||
public int? QuorumVotes { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var nodeIdStr = NodeId.HasValue ? $" (ID {NodeId})" : string.Empty;
|
||||
return $"Node: {Name ?? "N/A"}{nodeIdStr} | Ring0: {Ring0Addr ?? "N/A"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace PSProxmoxVE.Core.Models.Cluster;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the response from GET /cluster/config/join, containing
|
||||
/// the information needed to join this cluster.
|
||||
/// </summary>
|
||||
public class PveClusterJoinInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The SHA digest of the current cluster configuration.
|
||||
/// </summary>
|
||||
[JsonProperty("config_digest")]
|
||||
public string? ConfigDigest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of nodes in the cluster with their connection details.
|
||||
/// This is a complex nested structure returned as a JArray.
|
||||
/// </summary>
|
||||
[JsonProperty("nodelist")]
|
||||
public JArray? Nodelist { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The preferred node to connect to when joining.
|
||||
/// </summary>
|
||||
[JsonProperty("preferred_node")]
|
||||
public string? PreferredNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Corosync totem configuration as a raw JSON object.
|
||||
/// </summary>
|
||||
[JsonProperty("totem")]
|
||||
public JObject? Totem { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var nodeCount = Nodelist?.Count ?? 0;
|
||||
return $"JoinInfo: PreferredNode={PreferredNode ?? "N/A"} | Nodes={nodeCount}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PSProxmoxVE.Core.Models.Cluster;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the cluster-wide options returned by GET /cluster/options.
|
||||
/// </summary>
|
||||
public class PveClusterOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The keyboard layout for the web console (e.g., "en-us", "de").
|
||||
/// </summary>
|
||||
[JsonProperty("keyboard")]
|
||||
public string? Keyboard { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default language for the web UI.
|
||||
/// </summary>
|
||||
[JsonProperty("language")]
|
||||
public string? Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP proxy for outgoing connections (e.g., for downloading templates).
|
||||
/// </summary>
|
||||
[JsonProperty("http_proxy")]
|
||||
public string? HttpProxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sender email address for cluster notification emails.
|
||||
/// </summary>
|
||||
[JsonProperty("email_from")]
|
||||
public string? EmailFrom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default console viewer (e.g., "applet", "vv", "html5").
|
||||
/// </summary>
|
||||
[JsonProperty("console")]
|
||||
public string? Console { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cluster fencing mode (e.g., "watchdog", "hardware", "both").
|
||||
/// </summary>
|
||||
[JsonProperty("fencing")]
|
||||
public string? Fencing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default migration settings (type, network).
|
||||
/// </summary>
|
||||
[JsonProperty("migration")]
|
||||
public string? Migration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The MAC address prefix used for auto-generated MAC addresses.
|
||||
/// </summary>
|
||||
[JsonProperty("mac_prefix")]
|
||||
public string? MacPrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A description or comment for the cluster.
|
||||
/// </summary>
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of parallel worker processes for bulk operations.
|
||||
/// </summary>
|
||||
[JsonProperty("max_workers")]
|
||||
public int? MaxWorkers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// HA manager settings.
|
||||
/// </summary>
|
||||
[JsonProperty("ha")]
|
||||
public string? Ha { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bandwidth limit settings for various operations (clone, migration, etc.).
|
||||
/// </summary>
|
||||
[JsonProperty("bwlimit")]
|
||||
public string? BwLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Settings controlling next VM/CT ID allocation.
|
||||
/// </summary>
|
||||
[JsonProperty("next-id")]
|
||||
public string? NextId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cluster resource scheduling settings.
|
||||
/// </summary>
|
||||
[JsonProperty("crs")]
|
||||
public string? Crs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// U2F configuration settings.
|
||||
/// </summary>
|
||||
[JsonProperty("u2f")]
|
||||
public string? U2f { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// WebAuthn configuration settings.
|
||||
/// </summary>
|
||||
[JsonProperty("webauthn")]
|
||||
public string? Webauthn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tag style configuration for the web UI.
|
||||
/// </summary>
|
||||
[JsonProperty("tag-style")]
|
||||
public string? TagStyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Notification system configuration.
|
||||
/// </summary>
|
||||
[JsonProperty("notify")]
|
||||
public string? Notify { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom consent text displayed at login.
|
||||
/// </summary>
|
||||
[JsonProperty("consent-text")]
|
||||
public string? ConsentText { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"ClusterOptions: Language={Language ?? "default"} | Console={Console ?? "default"} | Fencing={Fencing ?? "default"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PSProxmoxVE.Core.Models.HA;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Proxmox VE HA group from the /cluster/ha/groups endpoint.
|
||||
/// </summary>
|
||||
public class PveHaGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// The HA group name.
|
||||
/// </summary>
|
||||
[JsonProperty("group")]
|
||||
public string Group { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Comma-separated list of nodes with optional priorities, e.g. "node1:2,node2:1".
|
||||
/// </summary>
|
||||
[JsonProperty("nodes")]
|
||||
public string? Nodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the group is restricted (1) or not (0). When restricted, resources can
|
||||
/// only run on group members.
|
||||
/// </summary>
|
||||
[JsonProperty("restricted")]
|
||||
public int? Restricted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether failback is disabled (1) or enabled (0). When nofailback is set, the
|
||||
/// resource will not automatically migrate back to its preferred node after recovery.
|
||||
/// </summary>
|
||||
[JsonProperty("nofailback")]
|
||||
public int? NoFailback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional comment describing this HA group.
|
||||
/// </summary>
|
||||
[JsonProperty("comment")]
|
||||
public string? Comment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The object type.
|
||||
/// </summary>
|
||||
[JsonProperty("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The configuration digest, used for conflict detection on updates.
|
||||
/// </summary>
|
||||
[JsonProperty("digest")]
|
||||
public string? Digest { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HA Group: {Group} | Nodes: {Nodes ?? "N/A"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PSProxmoxVE.Core.Models.HA;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Proxmox VE HA resource from the /cluster/ha/resources endpoint.
|
||||
/// </summary>
|
||||
public class PveHaResource
|
||||
{
|
||||
/// <summary>
|
||||
/// The HA resource ID, e.g. "vm:100" or "ct:200".
|
||||
/// </summary>
|
||||
[JsonProperty("sid")]
|
||||
public string Sid { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The requested state: "started", "stopped", "disabled", or "ignored".
|
||||
/// </summary>
|
||||
[JsonProperty("state")]
|
||||
public string? State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HA group this resource is assigned to.
|
||||
/// </summary>
|
||||
[JsonProperty("group")]
|
||||
public string? Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of relocations before the resource is placed in an error state.
|
||||
/// </summary>
|
||||
[JsonProperty("max_relocate")]
|
||||
public int? MaxRelocate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of restart attempts before the resource is placed in an error state.
|
||||
/// </summary>
|
||||
[JsonProperty("max_restart")]
|
||||
public int? MaxRestart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional comment describing this HA resource.
|
||||
/// </summary>
|
||||
[JsonProperty("comment")]
|
||||
public string? Comment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The resource type: "vm" or "ct".
|
||||
/// </summary>
|
||||
[JsonProperty("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The configuration digest, used for conflict detection on updates.
|
||||
/// </summary>
|
||||
[JsonProperty("digest")]
|
||||
public string? Digest { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HA Resource: {Sid} | State: {State ?? "N/A"} | Group: {Group ?? "N/A"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace PSProxmoxVE.Core.Models.HA;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Proxmox VE HA rule from the /cluster/ha/rules endpoint.
|
||||
/// Available in PVE 9.0 and later.
|
||||
/// </summary>
|
||||
public class PveHaRule
|
||||
{
|
||||
/// <summary>
|
||||
/// The rule identifier.
|
||||
/// </summary>
|
||||
[JsonProperty("rule")]
|
||||
public string Rule { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The rule type.
|
||||
/// </summary>
|
||||
[JsonProperty("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional comment describing this HA rule.
|
||||
/// </summary>
|
||||
[JsonProperty("comment")]
|
||||
public string? Comment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rule state: "enabled" or "disabled".
|
||||
/// </summary>
|
||||
[JsonProperty("state")]
|
||||
public string? State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The configuration digest, used for conflict detection on updates.
|
||||
/// </summary>
|
||||
[JsonProperty("digest")]
|
||||
public string? Digest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rule-specific properties that vary by rule type.
|
||||
/// </summary>
|
||||
[JsonProperty("properties")]
|
||||
public JObject? Properties { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HA Rule: {Rule} | Type: {Type ?? "N/A"} | State: {State ?? "N/A"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PSProxmoxVE.Core.Models.HA;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an entry from the /cluster/ha/status/current endpoint.
|
||||
/// Entries may be of type "quorum", "manager", or "service".
|
||||
/// </summary>
|
||||
public class PveHaStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The status entry identifier.
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The entry type: "quorum", "manager", or "service".
|
||||
/// </summary>
|
||||
[JsonProperty("type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cluster node name associated with this entry.
|
||||
/// </summary>
|
||||
[JsonProperty("node")]
|
||||
public string? Node { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current status text.
|
||||
/// </summary>
|
||||
[JsonProperty("status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Unix timestamp of the status entry.
|
||||
/// </summary>
|
||||
[JsonProperty("timestamp")]
|
||||
public long? Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The CRM (Cluster Resource Manager) state.
|
||||
/// </summary>
|
||||
[JsonProperty("crm_state")]
|
||||
public string? CrmState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The requested state for a service entry.
|
||||
/// </summary>
|
||||
[JsonProperty("request_state")]
|
||||
public string? RequestState { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"HA Status: {Id ?? "N/A"} | Node: {Node ?? "N/A"} | Status: {Status ?? "N/A"}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Models.Cluster;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for Proxmox VE cluster configuration API operations
|
||||
/// (/cluster/config, /cluster/options, /cluster/nextid).
|
||||
/// </summary>
|
||||
public class ClusterConfigService
|
||||
{
|
||||
private readonly IPveHttpClient? _injectedClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClusterConfigService"/> class.
|
||||
/// </summary>
|
||||
public ClusterConfigService() { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClusterConfigService"/> class with an injected HTTP client.
|
||||
/// </summary>
|
||||
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
|
||||
public ClusterConfigService(IPveHttpClient client)
|
||||
{
|
||||
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cluster configuration directory (GET /cluster/config).
|
||||
/// The response is a mixed structure returned as a raw JObject.
|
||||
/// </summary>
|
||||
public JObject GetClusterConfig(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/config").GetAwaiter().GetResult();
|
||||
return JObject.Parse(response);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new cluster (POST /cluster/config).
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="clusterName">The name for the new cluster.</param>
|
||||
/// <param name="links">Optional Corosync link addresses (e.g., "0=10.0.0.1,1=10.0.1.1").</param>
|
||||
/// <param name="nodeid">Optional node ID for this node.</param>
|
||||
/// <param name="votes">Optional number of quorum votes for this node.</param>
|
||||
/// <returns>The UPID of the cluster creation task.</returns>
|
||||
public string CreateCluster(PveSession session, string clusterName, Dictionary<string, string>? links = null, int? nodeid = null, int? votes = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrEmpty(clusterName)) throw new ArgumentNullException(nameof(clusterName));
|
||||
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
["clustername"] = clusterName
|
||||
};
|
||||
if (links != null)
|
||||
{
|
||||
foreach (var kvp in links)
|
||||
data[kvp.Key] = kvp.Value;
|
||||
}
|
||||
if (nodeid.HasValue)
|
||||
data["nodeid"] = nodeid.Value.ToString();
|
||||
if (votes.HasValue)
|
||||
data["votes"] = votes.Value.ToString();
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.PostAsync("cluster/config", data).GetAwaiter().GetResult();
|
||||
var result = JObject.Parse(response)["data"];
|
||||
return result?.ToString() ?? string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the list of nodes in the cluster configuration (GET /cluster/config/nodes).
|
||||
/// </summary>
|
||||
public PveClusterConfigNode[] GetConfigNodes(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/config/nodes").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveClusterConfigNode[]>() ?? Array.Empty<PveClusterConfigNode>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a node to the cluster configuration (POST /cluster/config/nodes/{node}).
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="node">The node name to add.</param>
|
||||
/// <param name="newNodeIp">The IP address of the new node.</param>
|
||||
/// <param name="links">Optional Corosync link addresses.</param>
|
||||
/// <param name="nodeid">Optional node ID for the new node.</param>
|
||||
/// <param name="votes">Optional number of quorum votes.</param>
|
||||
/// <param name="force">Optional flag to force the operation.</param>
|
||||
/// <param name="apiversion">Optional API version override.</param>
|
||||
/// <returns>The UPID of the add-node task.</returns>
|
||||
public string AddConfigNode(PveSession session, string node, string? newNodeIp = null, Dictionary<string, string>? links = null, int? nodeid = null, int? votes = null, bool? force = null, int? apiversion = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrEmpty(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var data = new Dictionary<string, string>();
|
||||
if (!string.IsNullOrEmpty(newNodeIp))
|
||||
data["new_node_ip"] = newNodeIp!;
|
||||
if (links != null)
|
||||
{
|
||||
foreach (var kvp in links)
|
||||
data[kvp.Key] = kvp.Value;
|
||||
}
|
||||
if (nodeid.HasValue)
|
||||
data["nodeid"] = nodeid.Value.ToString();
|
||||
if (votes.HasValue)
|
||||
data["votes"] = votes.Value.ToString();
|
||||
if (force.HasValue)
|
||||
data["force"] = force.Value ? "1" : "0";
|
||||
if (apiversion.HasValue)
|
||||
data["apiversion"] = apiversion.Value.ToString();
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.PostAsync($"cluster/config/nodes/{Uri.EscapeDataString(node)}", data).GetAwaiter().GetResult();
|
||||
var result = JObject.Parse(response)["data"];
|
||||
return result?.ToString() ?? string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a node from the cluster configuration (DELETE /cluster/config/nodes/{node}).
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="node">The node name to remove.</param>
|
||||
public void RemoveConfigNode(PveSession session, string node)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrEmpty(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.DeleteAsync($"cluster/config/nodes/{Uri.EscapeDataString(node)}").GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cluster join information (GET /cluster/config/join).
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="node">Optional node name to get join info for a specific node.</param>
|
||||
public PveClusterJoinInfo GetJoinInfo(PveSession session, string? node = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
var resource = "cluster/config/join";
|
||||
if (!string.IsNullOrEmpty(node))
|
||||
resource += $"?node={Uri.EscapeDataString(node!)}";
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveClusterJoinInfo>() ?? new PveClusterJoinInfo();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Joins the current node to an existing cluster (POST /cluster/config/join).
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="hostname">The hostname or IP of an existing cluster node.</param>
|
||||
/// <param name="fingerprint">The TLS certificate fingerprint of the cluster node.</param>
|
||||
/// <param name="password">The root password for the cluster node (plain string; cmdlet layer handles SecureString conversion per D002).</param>
|
||||
/// <param name="links">Optional Corosync link addresses.</param>
|
||||
/// <param name="nodeid">Optional node ID for this node.</param>
|
||||
/// <param name="votes">Optional number of quorum votes.</param>
|
||||
/// <param name="force">Optional flag to force the join.</param>
|
||||
/// <returns>The UPID of the join task.</returns>
|
||||
public string JoinCluster(PveSession session, string hostname, string fingerprint, string password, Dictionary<string, string>? links = null, int? nodeid = null, int? votes = null, bool? force = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrEmpty(hostname)) throw new ArgumentNullException(nameof(hostname));
|
||||
if (string.IsNullOrEmpty(fingerprint)) throw new ArgumentNullException(nameof(fingerprint));
|
||||
if (string.IsNullOrEmpty(password)) throw new ArgumentNullException(nameof(password));
|
||||
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
["hostname"] = hostname,
|
||||
["fingerprint"] = fingerprint,
|
||||
["password"] = password
|
||||
};
|
||||
if (links != null)
|
||||
{
|
||||
foreach (var kvp in links)
|
||||
data[kvp.Key] = kvp.Value;
|
||||
}
|
||||
if (nodeid.HasValue)
|
||||
data["nodeid"] = nodeid.Value.ToString();
|
||||
if (votes.HasValue)
|
||||
data["votes"] = votes.Value.ToString();
|
||||
if (force.HasValue)
|
||||
data["force"] = force.Value ? "1" : "0";
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.PostAsync("cluster/config/join", data).GetAwaiter().GetResult();
|
||||
var result = JObject.Parse(response)["data"];
|
||||
return result?.ToString() ?? string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Corosync totem configuration (GET /cluster/config/totem).
|
||||
/// </summary>
|
||||
public JObject GetTotem(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/config/totem").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data as JObject ?? new JObject();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the external quorum device (qdevice) status (GET /cluster/config/qdevice).
|
||||
/// </summary>
|
||||
public JObject GetQdevice(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/config/qdevice").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data as JObject ?? new JObject();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cluster API version (GET /cluster/config/apiversion).
|
||||
/// </summary>
|
||||
public int GetApiVersion(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/config/apiversion").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<int>() ?? 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cluster-wide options (GET /cluster/options).
|
||||
/// </summary>
|
||||
public PveClusterOptions GetClusterOptions(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/options").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveClusterOptions>() ?? new PveClusterOptions();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets cluster-wide options (PUT /cluster/options).
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="options">A dictionary of option names and values to set.</param>
|
||||
public void SetClusterOptions(PveSession session, Dictionary<string, string> options)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.PutAsync("cluster/options", options).GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current cluster status (GET /cluster/status).
|
||||
/// Delegates to the same endpoint as <see cref="ClusterService.GetClusterStatus"/>.
|
||||
/// </summary>
|
||||
public PveClusterStatus[] GetClusterStatus(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/status").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveClusterStatus[]>() ?? Array.Empty<PveClusterStatus>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next available VM/CT ID (GET /cluster/nextid).
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="vmid">Optional specific VMID to check availability for.</param>
|
||||
/// <returns>The next available VMID as an integer.</returns>
|
||||
public int GetNextId(PveSession session, int? vmid = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
var resource = "cluster/nextid";
|
||||
if (vmid.HasValue)
|
||||
resource += $"?vmid={vmid.Value}";
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
// The API returns the ID as a string, parse it to int
|
||||
if (data != null && int.TryParse(data.ToString(), out var id))
|
||||
return id;
|
||||
return 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Models.HA;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for Proxmox VE High Availability (HA) API operations.
|
||||
/// </summary>
|
||||
public class HaService
|
||||
{
|
||||
private readonly IPveHttpClient? _injectedClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HaService"/> class.
|
||||
/// </summary>
|
||||
public HaService() { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HaService"/> class with an injected HTTP client.
|
||||
/// </summary>
|
||||
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
|
||||
public HaService(IPveHttpClient client)
|
||||
{
|
||||
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Resources
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns all HA resources.
|
||||
/// </summary>
|
||||
public PveHaResource[] GetResources(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/ha/resources").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveHaResource[]>() ?? Array.Empty<PveHaResource>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a single HA resource by its SID.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="sid">The resource ID, e.g. "vm:100" or "ct:200".</param>
|
||||
public PveHaResource GetResource(PveSession session, string sid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(sid)) throw new ArgumentNullException(nameof(sid));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync($"cluster/ha/resources/{Uri.EscapeDataString(sid)}")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveHaResource>() ?? new PveHaResource();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new HA resource.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="sid">The resource ID, e.g. "vm:100" or "ct:200".</param>
|
||||
/// <param name="options">Additional configuration options (state, group, max_relocate, etc.).</param>
|
||||
public void CreateResource(PveSession session, string sid, Dictionary<string, string> options)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(sid)) throw new ArgumentNullException(nameof(sid));
|
||||
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||
|
||||
var formData = new Dictionary<string, string>(options) { ["sid"] = sid };
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.PostAsync("cluster/ha/resources", formData).GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing HA resource.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="sid">The resource ID, e.g. "vm:100" or "ct:200".</param>
|
||||
/// <param name="options">Configuration options to update.</param>
|
||||
public void UpdateResource(PveSession session, string sid, Dictionary<string, string> options)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(sid)) throw new ArgumentNullException(nameof(sid));
|
||||
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.PutAsync($"cluster/ha/resources/{Uri.EscapeDataString(sid)}", options)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an HA resource.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="sid">The resource ID, e.g. "vm:100" or "ct:200".</param>
|
||||
public void DeleteResource(PveSession session, string sid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(sid)) throw new ArgumentNullException(nameof(sid));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.DeleteAsync($"cluster/ha/resources/{Uri.EscapeDataString(sid)}")
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests migration of an HA resource to another node. Returns the task UPID.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="sid">The resource ID, e.g. "vm:100" or "ct:200".</param>
|
||||
/// <param name="node">The target node to migrate to.</param>
|
||||
public string MigrateResource(PveSession session, string sid, string node)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(sid)) throw new ArgumentNullException(nameof(sid));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var formData = new Dictionary<string, string> { ["node"] = node };
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.PostAsync(
|
||||
$"cluster/ha/resources/{Uri.EscapeDataString(sid)}/migrate", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToString() ?? string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests relocation of an HA resource to another node. Returns the task UPID.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="sid">The resource ID, e.g. "vm:100" or "ct:200".</param>
|
||||
/// <param name="node">The target node to relocate to.</param>
|
||||
public string RelocateResource(PveSession session, string sid, string node)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(sid)) throw new ArgumentNullException(nameof(sid));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var formData = new Dictionary<string, string> { ["node"] = node };
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.PostAsync(
|
||||
$"cluster/ha/resources/{Uri.EscapeDataString(sid)}/relocate", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToString() ?? string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Groups
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns all HA groups.
|
||||
/// </summary>
|
||||
public PveHaGroup[] GetGroups(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/ha/groups").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveHaGroup[]>() ?? Array.Empty<PveHaGroup>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a single HA group by name.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="group">The HA group name.</param>
|
||||
public PveHaGroup GetGroup(PveSession session, string group)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync($"cluster/ha/groups/{Uri.EscapeDataString(group)}")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveHaGroup>() ?? new PveHaGroup();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new HA group.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="group">The group name.</param>
|
||||
/// <param name="nodes">Comma-separated list of nodes with optional priorities, e.g. "node1:2,node2:1".</param>
|
||||
/// <param name="options">Additional configuration options (restricted, nofailback, comment).</param>
|
||||
public void CreateGroup(PveSession session, string group, string nodes, Dictionary<string, string> options)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
||||
if (string.IsNullOrWhiteSpace(nodes)) throw new ArgumentNullException(nameof(nodes));
|
||||
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||
|
||||
var formData = new Dictionary<string, string>(options)
|
||||
{
|
||||
["group"] = group,
|
||||
["nodes"] = nodes
|
||||
};
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.PostAsync("cluster/ha/groups", formData).GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing HA group.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="group">The group name.</param>
|
||||
/// <param name="options">Configuration options to update.</param>
|
||||
public void UpdateGroup(PveSession session, string group, Dictionary<string, string> options)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
||||
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.PutAsync($"cluster/ha/groups/{Uri.EscapeDataString(group)}", options)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an HA group.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="group">The group name.</param>
|
||||
public void DeleteGroup(PveSession session, string group)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.DeleteAsync($"cluster/ha/groups/{Uri.EscapeDataString(group)}")
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Status
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current HA status from /cluster/ha/status/current.
|
||||
/// </summary>
|
||||
public PveHaStatus[] GetStatus(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/ha/status/current").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveHaStatus[]>() ?? Array.Empty<PveHaStatus>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the full HA manager status as a raw JSON object.
|
||||
/// </summary>
|
||||
public JObject GetManagerStatus(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/ha/status/manager_status").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data as JObject ?? new JObject();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rules (PVE 9.0+)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns all HA rules. Requires PVE 9.0 or later.
|
||||
/// </summary>
|
||||
public PveHaRule[] GetRules(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync("cluster/ha/rules").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveHaRule[]>() ?? Array.Empty<PveHaRule>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a single HA rule by its ID. Requires PVE 9.0 or later.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="rule">The rule identifier.</param>
|
||||
public PveHaRule GetRule(PveSession session, string rule)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(rule)) throw new ArgumentNullException(nameof(rule));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.GetAsync($"cluster/ha/rules/{Uri.EscapeDataString(rule)}")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveHaRule>() ?? new PveHaRule();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new HA rule. Requires PVE 9.0 or later.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="options">Rule configuration options (rule, type, state, comment, etc.).</param>
|
||||
public void CreateRule(PveSession session, Dictionary<string, string> options)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.PostAsync("cluster/ha/rules", options).GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing HA rule. Requires PVE 9.0 or later.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="rule">The rule identifier.</param>
|
||||
/// <param name="options">Configuration options to update.</param>
|
||||
public void UpdateRule(PveSession session, string rule, Dictionary<string, string> options)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(rule)) throw new ArgumentNullException(nameof(rule));
|
||||
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.PutAsync($"cluster/ha/rules/{Uri.EscapeDataString(rule)}", options)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an HA rule. Requires PVE 9.0 or later.
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="rule">The rule identifier.</param>
|
||||
public void DeleteRule(PveSession session, string rule)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(rule)) throw new ArgumentNullException(nameof(rule));
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
client.DeleteAsync($"cluster/ha/rules/{Uri.EscapeDataString(rule)}")
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_injectedClient == null) client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user