diff --git a/src/PSProxmoxVE.Core/Models/Cluster/PveClusterConfigNode.cs b/src/PSProxmoxVE.Core/Models/Cluster/PveClusterConfigNode.cs
new file mode 100644
index 0000000..fa58a40
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Cluster/PveClusterConfigNode.cs
@@ -0,0 +1,46 @@
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Cluster;
+
+///
+/// Represents a node entry from the GET /cluster/config/nodes endpoint.
+///
+public class PveClusterConfigNode
+{
+ ///
+ /// The node name.
+ ///
+ [JsonProperty("node")]
+ public string? Name { get; set; }
+
+ ///
+ /// The numeric node ID within the cluster Corosync configuration.
+ ///
+ [JsonProperty("nodeid")]
+ public int? NodeId { get; set; }
+
+ ///
+ /// The address used for Corosync ring 0 communication.
+ ///
+ [JsonProperty("ring0_addr")]
+ public string? Ring0Addr { get; set; }
+
+ ///
+ /// The address used for Corosync ring 1 communication.
+ ///
+ [JsonProperty("ring1_addr")]
+ public string? Ring1Addr { get; set; }
+
+ ///
+ /// The number of quorum votes assigned to this node.
+ ///
+ [JsonProperty("quorum_votes")]
+ public int? QuorumVotes { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ var nodeIdStr = NodeId.HasValue ? $" (ID {NodeId})" : string.Empty;
+ return $"Node: {Name ?? "N/A"}{nodeIdStr} | Ring0: {Ring0Addr ?? "N/A"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Cluster/PveClusterJoinInfo.cs b/src/PSProxmoxVE.Core/Models/Cluster/PveClusterJoinInfo.cs
new file mode 100644
index 0000000..0292527
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Cluster/PveClusterJoinInfo.cs
@@ -0,0 +1,43 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace PSProxmoxVE.Core.Models.Cluster;
+
+///
+/// Represents the response from GET /cluster/config/join, containing
+/// the information needed to join this cluster.
+///
+public class PveClusterJoinInfo
+{
+ ///
+ /// The SHA digest of the current cluster configuration.
+ ///
+ [JsonProperty("config_digest")]
+ public string? ConfigDigest { get; set; }
+
+ ///
+ /// The list of nodes in the cluster with their connection details.
+ /// This is a complex nested structure returned as a JArray.
+ ///
+ [JsonProperty("nodelist")]
+ public JArray? Nodelist { get; set; }
+
+ ///
+ /// The preferred node to connect to when joining.
+ ///
+ [JsonProperty("preferred_node")]
+ public string? PreferredNode { get; set; }
+
+ ///
+ /// The Corosync totem configuration as a raw JSON object.
+ ///
+ [JsonProperty("totem")]
+ public JObject? Totem { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ var nodeCount = Nodelist?.Count ?? 0;
+ return $"JoinInfo: PreferredNode={PreferredNode ?? "N/A"} | Nodes={nodeCount}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/Cluster/PveClusterOptions.cs b/src/PSProxmoxVE.Core/Models/Cluster/PveClusterOptions.cs
new file mode 100644
index 0000000..c23440b
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/Cluster/PveClusterOptions.cs
@@ -0,0 +1,129 @@
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.Cluster;
+
+///
+/// Represents the cluster-wide options returned by GET /cluster/options.
+///
+public class PveClusterOptions
+{
+ ///
+ /// The keyboard layout for the web console (e.g., "en-us", "de").
+ ///
+ [JsonProperty("keyboard")]
+ public string? Keyboard { get; set; }
+
+ ///
+ /// The default language for the web UI.
+ ///
+ [JsonProperty("language")]
+ public string? Language { get; set; }
+
+ ///
+ /// The HTTP proxy for outgoing connections (e.g., for downloading templates).
+ ///
+ [JsonProperty("http_proxy")]
+ public string? HttpProxy { get; set; }
+
+ ///
+ /// The sender email address for cluster notification emails.
+ ///
+ [JsonProperty("email_from")]
+ public string? EmailFrom { get; set; }
+
+ ///
+ /// The default console viewer (e.g., "applet", "vv", "html5").
+ ///
+ [JsonProperty("console")]
+ public string? Console { get; set; }
+
+ ///
+ /// The cluster fencing mode (e.g., "watchdog", "hardware", "both").
+ ///
+ [JsonProperty("fencing")]
+ public string? Fencing { get; set; }
+
+ ///
+ /// The default migration settings (type, network).
+ ///
+ [JsonProperty("migration")]
+ public string? Migration { get; set; }
+
+ ///
+ /// The MAC address prefix used for auto-generated MAC addresses.
+ ///
+ [JsonProperty("mac_prefix")]
+ public string? MacPrefix { get; set; }
+
+ ///
+ /// A description or comment for the cluster.
+ ///
+ [JsonProperty("description")]
+ public string? Description { get; set; }
+
+ ///
+ /// The maximum number of parallel worker processes for bulk operations.
+ ///
+ [JsonProperty("max_workers")]
+ public int? MaxWorkers { get; set; }
+
+ ///
+ /// HA manager settings.
+ ///
+ [JsonProperty("ha")]
+ public string? Ha { get; set; }
+
+ ///
+ /// Bandwidth limit settings for various operations (clone, migration, etc.).
+ ///
+ [JsonProperty("bwlimit")]
+ public string? BwLimit { get; set; }
+
+ ///
+ /// Settings controlling next VM/CT ID allocation.
+ ///
+ [JsonProperty("next-id")]
+ public string? NextId { get; set; }
+
+ ///
+ /// Cluster resource scheduling settings.
+ ///
+ [JsonProperty("crs")]
+ public string? Crs { get; set; }
+
+ ///
+ /// U2F configuration settings.
+ ///
+ [JsonProperty("u2f")]
+ public string? U2f { get; set; }
+
+ ///
+ /// WebAuthn configuration settings.
+ ///
+ [JsonProperty("webauthn")]
+ public string? Webauthn { get; set; }
+
+ ///
+ /// Tag style configuration for the web UI.
+ ///
+ [JsonProperty("tag-style")]
+ public string? TagStyle { get; set; }
+
+ ///
+ /// Notification system configuration.
+ ///
+ [JsonProperty("notify")]
+ public string? Notify { get; set; }
+
+ ///
+ /// Custom consent text displayed at login.
+ ///
+ [JsonProperty("consent-text")]
+ public string? ConsentText { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return $"ClusterOptions: Language={Language ?? "default"} | Console={Console ?? "default"} | Fencing={Fencing ?? "default"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/HA/PveHaGroup.cs b/src/PSProxmoxVE.Core/Models/HA/PveHaGroup.cs
new file mode 100644
index 0000000..a2a863d
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/HA/PveHaGroup.cs
@@ -0,0 +1,59 @@
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.HA;
+
+///
+/// Represents a Proxmox VE HA group from the /cluster/ha/groups endpoint.
+///
+public class PveHaGroup
+{
+ ///
+ /// The HA group name.
+ ///
+ [JsonProperty("group")]
+ public string Group { get; set; } = string.Empty;
+
+ ///
+ /// Comma-separated list of nodes with optional priorities, e.g. "node1:2,node2:1".
+ ///
+ [JsonProperty("nodes")]
+ public string? Nodes { get; set; }
+
+ ///
+ /// Whether the group is restricted (1) or not (0). When restricted, resources can
+ /// only run on group members.
+ ///
+ [JsonProperty("restricted")]
+ public int? Restricted { get; set; }
+
+ ///
+ /// 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.
+ ///
+ [JsonProperty("nofailback")]
+ public int? NoFailback { get; set; }
+
+ ///
+ /// An optional comment describing this HA group.
+ ///
+ [JsonProperty("comment")]
+ public string? Comment { get; set; }
+
+ ///
+ /// The object type.
+ ///
+ [JsonProperty("type")]
+ public string? Type { get; set; }
+
+ ///
+ /// The configuration digest, used for conflict detection on updates.
+ ///
+ [JsonProperty("digest")]
+ public string? Digest { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return $"HA Group: {Group} | Nodes: {Nodes ?? "N/A"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/HA/PveHaResource.cs b/src/PSProxmoxVE.Core/Models/HA/PveHaResource.cs
new file mode 100644
index 0000000..e10a365
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/HA/PveHaResource.cs
@@ -0,0 +1,63 @@
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.HA;
+
+///
+/// Represents a Proxmox VE HA resource from the /cluster/ha/resources endpoint.
+///
+public class PveHaResource
+{
+ ///
+ /// The HA resource ID, e.g. "vm:100" or "ct:200".
+ ///
+ [JsonProperty("sid")]
+ public string Sid { get; set; } = string.Empty;
+
+ ///
+ /// The requested state: "started", "stopped", "disabled", or "ignored".
+ ///
+ [JsonProperty("state")]
+ public string? State { get; set; }
+
+ ///
+ /// The HA group this resource is assigned to.
+ ///
+ [JsonProperty("group")]
+ public string? Group { get; set; }
+
+ ///
+ /// Maximum number of relocations before the resource is placed in an error state.
+ ///
+ [JsonProperty("max_relocate")]
+ public int? MaxRelocate { get; set; }
+
+ ///
+ /// Maximum number of restart attempts before the resource is placed in an error state.
+ ///
+ [JsonProperty("max_restart")]
+ public int? MaxRestart { get; set; }
+
+ ///
+ /// An optional comment describing this HA resource.
+ ///
+ [JsonProperty("comment")]
+ public string? Comment { get; set; }
+
+ ///
+ /// The resource type: "vm" or "ct".
+ ///
+ [JsonProperty("type")]
+ public string? Type { get; set; }
+
+ ///
+ /// The configuration digest, used for conflict detection on updates.
+ ///
+ [JsonProperty("digest")]
+ public string? Digest { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return $"HA Resource: {Sid} | State: {State ?? "N/A"} | Group: {Group ?? "N/A"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/HA/PveHaRule.cs b/src/PSProxmoxVE.Core/Models/HA/PveHaRule.cs
new file mode 100644
index 0000000..8205d75
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/HA/PveHaRule.cs
@@ -0,0 +1,53 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace PSProxmoxVE.Core.Models.HA;
+
+///
+/// Represents a Proxmox VE HA rule from the /cluster/ha/rules endpoint.
+/// Available in PVE 9.0 and later.
+///
+public class PveHaRule
+{
+ ///
+ /// The rule identifier.
+ ///
+ [JsonProperty("rule")]
+ public string Rule { get; set; } = string.Empty;
+
+ ///
+ /// The rule type.
+ ///
+ [JsonProperty("type")]
+ public string? Type { get; set; }
+
+ ///
+ /// An optional comment describing this HA rule.
+ ///
+ [JsonProperty("comment")]
+ public string? Comment { get; set; }
+
+ ///
+ /// The rule state: "enabled" or "disabled".
+ ///
+ [JsonProperty("state")]
+ public string? State { get; set; }
+
+ ///
+ /// The configuration digest, used for conflict detection on updates.
+ ///
+ [JsonProperty("digest")]
+ public string? Digest { get; set; }
+
+ ///
+ /// Rule-specific properties that vary by rule type.
+ ///
+ [JsonProperty("properties")]
+ public JObject? Properties { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return $"HA Rule: {Rule} | Type: {Type ?? "N/A"} | State: {State ?? "N/A"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Models/HA/PveHaStatus.cs b/src/PSProxmoxVE.Core/Models/HA/PveHaStatus.cs
new file mode 100644
index 0000000..c1da9d3
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Models/HA/PveHaStatus.cs
@@ -0,0 +1,58 @@
+using Newtonsoft.Json;
+
+namespace PSProxmoxVE.Core.Models.HA;
+
+///
+/// Represents an entry from the /cluster/ha/status/current endpoint.
+/// Entries may be of type "quorum", "manager", or "service".
+///
+public class PveHaStatus
+{
+ ///
+ /// The status entry identifier.
+ ///
+ [JsonProperty("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// The entry type: "quorum", "manager", or "service".
+ ///
+ [JsonProperty("type")]
+ public string? Type { get; set; }
+
+ ///
+ /// The cluster node name associated with this entry.
+ ///
+ [JsonProperty("node")]
+ public string? Node { get; set; }
+
+ ///
+ /// The current status text.
+ ///
+ [JsonProperty("status")]
+ public string? Status { get; set; }
+
+ ///
+ /// The Unix timestamp of the status entry.
+ ///
+ [JsonProperty("timestamp")]
+ public long? Timestamp { get; set; }
+
+ ///
+ /// The CRM (Cluster Resource Manager) state.
+ ///
+ [JsonProperty("crm_state")]
+ public string? CrmState { get; set; }
+
+ ///
+ /// The requested state for a service entry.
+ ///
+ [JsonProperty("request_state")]
+ public string? RequestState { get; set; }
+
+ ///
+ public override string ToString()
+ {
+ return $"HA Status: {Id ?? "N/A"} | Node: {Node ?? "N/A"} | Status: {Status ?? "N/A"}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs b/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs
new file mode 100644
index 0000000..a135b68
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs
@@ -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
+{
+ ///
+ /// Service for Proxmox VE cluster configuration API operations
+ /// (/cluster/config, /cluster/options, /cluster/nextid).
+ ///
+ public class ClusterConfigService
+ {
+ private readonly IPveHttpClient? _injectedClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ClusterConfigService() { }
+
+ ///
+ /// Initializes a new instance of the class with an injected HTTP client.
+ ///
+ /// The HTTP client to use for API calls. The caller owns its lifetime.
+ public ClusterConfigService(IPveHttpClient client)
+ {
+ _injectedClient = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ ///
+ /// Returns the cluster configuration directory (GET /cluster/config).
+ /// The response is a mixed structure returned as a raw JObject.
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Creates a new cluster (POST /cluster/config).
+ ///
+ /// The authenticated PVE session.
+ /// The name for the new cluster.
+ /// Optional Corosync link addresses (e.g., "0=10.0.0.1,1=10.0.1.1").
+ /// Optional node ID for this node.
+ /// Optional number of quorum votes for this node.
+ /// The UPID of the cluster creation task.
+ public string CreateCluster(PveSession session, string clusterName, Dictionary? 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
+ {
+ ["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();
+ }
+ }
+
+ ///
+ /// Returns the list of nodes in the cluster configuration (GET /cluster/config/nodes).
+ ///
+ 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() ?? Array.Empty();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Adds a node to the cluster configuration (POST /cluster/config/nodes/{node}).
+ ///
+ /// The authenticated PVE session.
+ /// The node name to add.
+ /// The IP address of the new node.
+ /// Optional Corosync link addresses.
+ /// Optional node ID for the new node.
+ /// Optional number of quorum votes.
+ /// Optional flag to force the operation.
+ /// Optional API version override.
+ /// The UPID of the add-node task.
+ public string AddConfigNode(PveSession session, string node, string? newNodeIp = null, Dictionary? 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();
+ 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();
+ }
+ }
+
+ ///
+ /// Removes a node from the cluster configuration (DELETE /cluster/config/nodes/{node}).
+ ///
+ /// The authenticated PVE session.
+ /// The node name to remove.
+ 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();
+ }
+ }
+
+ ///
+ /// Returns the cluster join information (GET /cluster/config/join).
+ ///
+ /// The authenticated PVE session.
+ /// Optional node name to get join info for a specific node.
+ 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() ?? new PveClusterJoinInfo();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Joins the current node to an existing cluster (POST /cluster/config/join).
+ ///
+ /// The authenticated PVE session.
+ /// The hostname or IP of an existing cluster node.
+ /// The TLS certificate fingerprint of the cluster node.
+ /// The root password for the cluster node (plain string; cmdlet layer handles SecureString conversion per D002).
+ /// Optional Corosync link addresses.
+ /// Optional node ID for this node.
+ /// Optional number of quorum votes.
+ /// Optional flag to force the join.
+ /// The UPID of the join task.
+ public string JoinCluster(PveSession session, string hostname, string fingerprint, string password, Dictionary? 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
+ {
+ ["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();
+ }
+ }
+
+ ///
+ /// Returns the Corosync totem configuration (GET /cluster/config/totem).
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Returns the external quorum device (qdevice) status (GET /cluster/config/qdevice).
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Returns the cluster API version (GET /cluster/config/apiversion).
+ ///
+ 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() ?? 0;
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Returns the cluster-wide options (GET /cluster/options).
+ ///
+ 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() ?? new PveClusterOptions();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Sets cluster-wide options (PUT /cluster/options).
+ ///
+ /// The authenticated PVE session.
+ /// A dictionary of option names and values to set.
+ public void SetClusterOptions(PveSession session, Dictionary 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();
+ }
+ }
+
+ ///
+ /// Returns the current cluster status (GET /cluster/status).
+ /// Delegates to the same endpoint as .
+ ///
+ 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() ?? Array.Empty();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Returns the next available VM/CT ID (GET /cluster/nextid).
+ ///
+ /// The authenticated PVE session.
+ /// Optional specific VMID to check availability for.
+ /// The next available VMID as an integer.
+ 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();
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Services/HaService.cs b/src/PSProxmoxVE.Core/Services/HaService.cs
new file mode 100644
index 0000000..5e9e4e0
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Services/HaService.cs
@@ -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
+{
+ ///
+ /// Service for Proxmox VE High Availability (HA) API operations.
+ ///
+ public class HaService
+ {
+ private readonly IPveHttpClient? _injectedClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public HaService() { }
+
+ ///
+ /// Initializes a new instance of the class with an injected HTTP client.
+ ///
+ /// The HTTP client to use for API calls. The caller owns its lifetime.
+ public HaService(IPveHttpClient client)
+ {
+ _injectedClient = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ // -------------------------------------------------------------------------
+ // Resources
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Returns all HA resources.
+ ///
+ 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() ?? Array.Empty();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Returns a single HA resource by its SID.
+ ///
+ /// The authenticated PVE session.
+ /// The resource ID, e.g. "vm:100" or "ct:200".
+ 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() ?? new PveHaResource();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Creates a new HA resource.
+ ///
+ /// The authenticated PVE session.
+ /// The resource ID, e.g. "vm:100" or "ct:200".
+ /// Additional configuration options (state, group, max_relocate, etc.).
+ public void CreateResource(PveSession session, string sid, Dictionary 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(options) { ["sid"] = sid };
+
+ IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
+ try
+ {
+ client.PostAsync("cluster/ha/resources", formData).GetAwaiter().GetResult();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Updates an existing HA resource.
+ ///
+ /// The authenticated PVE session.
+ /// The resource ID, e.g. "vm:100" or "ct:200".
+ /// Configuration options to update.
+ public void UpdateResource(PveSession session, string sid, Dictionary 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();
+ }
+ }
+
+ ///
+ /// Deletes an HA resource.
+ ///
+ /// The authenticated PVE session.
+ /// The resource ID, e.g. "vm:100" or "ct:200".
+ 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();
+ }
+ }
+
+ ///
+ /// Requests migration of an HA resource to another node. Returns the task UPID.
+ ///
+ /// The authenticated PVE session.
+ /// The resource ID, e.g. "vm:100" or "ct:200".
+ /// The target node to migrate to.
+ 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 { ["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();
+ }
+ }
+
+ ///
+ /// Requests relocation of an HA resource to another node. Returns the task UPID.
+ ///
+ /// The authenticated PVE session.
+ /// The resource ID, e.g. "vm:100" or "ct:200".
+ /// The target node to relocate to.
+ 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 { ["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
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Returns all HA groups.
+ ///
+ 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() ?? Array.Empty();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Returns a single HA group by name.
+ ///
+ /// The authenticated PVE session.
+ /// The HA group name.
+ 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() ?? new PveHaGroup();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Creates a new HA group.
+ ///
+ /// The authenticated PVE session.
+ /// The group name.
+ /// Comma-separated list of nodes with optional priorities, e.g. "node1:2,node2:1".
+ /// Additional configuration options (restricted, nofailback, comment).
+ public void CreateGroup(PveSession session, string group, string nodes, Dictionary 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(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();
+ }
+ }
+
+ ///
+ /// Updates an existing HA group.
+ ///
+ /// The authenticated PVE session.
+ /// The group name.
+ /// Configuration options to update.
+ public void UpdateGroup(PveSession session, string group, Dictionary 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();
+ }
+ }
+
+ ///
+ /// Deletes an HA group.
+ ///
+ /// The authenticated PVE session.
+ /// The group name.
+ 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
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Returns the current HA status from /cluster/ha/status/current.
+ ///
+ 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() ?? Array.Empty();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Returns the full HA manager status as a raw JSON object.
+ ///
+ 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+)
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Returns all HA rules. Requires PVE 9.0 or later.
+ ///
+ 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() ?? Array.Empty();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Returns a single HA rule by its ID. Requires PVE 9.0 or later.
+ ///
+ /// The authenticated PVE session.
+ /// The rule identifier.
+ 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() ?? new PveHaRule();
+ }
+ finally
+ {
+ if (_injectedClient == null) client.Dispose();
+ }
+ }
+
+ ///
+ /// Creates a new HA rule. Requires PVE 9.0 or later.
+ ///
+ /// The authenticated PVE session.
+ /// Rule configuration options (rule, type, state, comment, etc.).
+ public void CreateRule(PveSession session, Dictionary 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();
+ }
+ }
+
+ ///
+ /// Updates an existing HA rule. Requires PVE 9.0 or later.
+ ///
+ /// The authenticated PVE session.
+ /// The rule identifier.
+ /// Configuration options to update.
+ public void UpdateRule(PveSession session, string rule, Dictionary 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();
+ }
+ }
+
+ ///
+ /// Deletes an HA rule. Requires PVE 9.0 or later.
+ ///
+ /// The authenticated PVE session.
+ /// The rule identifier.
+ 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();
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterConfigNodeCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterConfigNodeCmdlet.cs
new file mode 100644
index 0000000..6d15a29
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterConfigNodeCmdlet.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Adds a node to the cluster configuration.
+ ///
+ /// Registers a new node in the Corosync cluster configuration.
+ /// This is the server-side counterpart of joining — run this on an existing
+ /// cluster member to prepare for a new node.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Add, "PveClusterConfigNode", SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ [OutputType(typeof(string))]
+ public sealed class AddPveClusterConfigNodeCmdlet : PveCmdletBase
+ {
+ /// The name of the node to add.
+ [Parameter(Mandatory = true, Position = 0, HelpMessage = "The cluster node name to add.")]
+ public string Node { get; set; } = string.Empty;
+
+ /// IP address of the new node (fallback if no links given).
+ [Parameter(Mandatory = false, HelpMessage = "IP address of the new node.")]
+ public string? NewNodeIp { get; set; }
+
+ /// Node ID for the new node.
+ [Parameter(Mandatory = false, HelpMessage = "Corosync node ID.")]
+ [ValidateRange(1, int.MaxValue)]
+ public int? NodeId { get; set; }
+
+ /// Number of votes for the new node.
+ [Parameter(Mandatory = false, HelpMessage = "Number of quorum votes.")]
+ [ValidateRange(0, int.MaxValue)]
+ public int? Votes { get; set; }
+
+ /// Do not throw error if node already exists.
+ [Parameter(Mandatory = false, HelpMessage = "Force add even if node already exists.")]
+ public SwitchParameter Force { get; set; }
+
+ /// The JOIN_API_VERSION of the new node.
+ [Parameter(Mandatory = false, HelpMessage = "JOIN_API_VERSION of the new node.")]
+ public int? ApiVersion { get; set; }
+
+ /// Corosync link addresses (link0..link7).
+ [Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
+ public string[]? Links { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"node '{Node}'", "Add to cluster configuration"))
+ return;
+
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ Dictionary? linkDict = null;
+ if (Links != null)
+ {
+ linkDict = new Dictionary();
+ foreach (var link in Links)
+ {
+ var parts = link.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ linkDict[parts[0]] = parts[1];
+ }
+ }
+
+ WriteVerbose($"Adding node '{Node}' to cluster configuration...");
+ var upid = service.AddConfigNode(session, Node, NewNodeIp, linkDict, NodeId, Votes,
+ Force.IsPresent ? true : (bool?)null, ApiVersion);
+ WriteObject(upid);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterMemberCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterMemberCmdlet.cs
new file mode 100644
index 0000000..820dbdb
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterMemberCmdlet.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Management.Automation;
+using System.Runtime.InteropServices;
+using System.Security;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Joins this node to an existing cluster.
+ ///
+ /// Joins the current node to an existing Proxmox VE cluster. Requires the
+ /// cluster's certificate fingerprint, the hostname of an existing member,
+ /// and the root password of that member. Run this on the node that should join.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Add, "PveClusterMember", SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ [OutputType(typeof(string))]
+ public sealed class AddPveClusterMemberCmdlet : PveCmdletBase
+ {
+ /// Hostname or IP of an existing cluster member.
+ [Parameter(Mandatory = true, Position = 0, HelpMessage = "Hostname or IP of an existing cluster member.")]
+ public string Hostname { get; set; } = string.Empty;
+
+ /// Certificate SHA-256 fingerprint of the cluster.
+ [Parameter(Mandatory = true, Position = 1, HelpMessage = "Certificate SHA-256 fingerprint of the cluster.")]
+ public string Fingerprint { get; set; } = string.Empty;
+
+ /// Root password of the existing cluster member.
+ [Parameter(Mandatory = true, HelpMessage = "Root password of the cluster member.")]
+ public SecureString Password { get; set; } = new SecureString();
+
+ /// Node ID for this node.
+ [Parameter(Mandatory = false, HelpMessage = "Corosync node ID for this node.")]
+ [ValidateRange(1, int.MaxValue)]
+ public int? NodeId { get; set; }
+
+ /// Number of quorum votes for this node.
+ [Parameter(Mandatory = false, HelpMessage = "Number of quorum votes.")]
+ [ValidateRange(0, int.MaxValue)]
+ public int? Votes { get; set; }
+
+ /// Force join even if already part of a cluster.
+ [Parameter(Mandatory = false, HelpMessage = "Force join even if already part of a cluster.")]
+ public SwitchParameter Force { get; set; }
+
+ /// Corosync link addresses (link0..link7).
+ [Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
+ public string[]? Links { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"this node to cluster via '{Hostname}'", "Join cluster"))
+ return;
+
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ IntPtr ptr = IntPtr.Zero;
+ try
+ {
+ ptr = Marshal.SecureStringToGlobalAllocUnicode(Password);
+ var plainPassword = Marshal.PtrToStringUni(ptr)!;
+
+ Dictionary? linkDict = null;
+ if (Links != null)
+ {
+ linkDict = new Dictionary();
+ foreach (var link in Links)
+ {
+ var parts = link.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ linkDict[parts[0]] = parts[1];
+ }
+ }
+
+ WriteVerbose($"Joining cluster via '{Hostname}'...");
+ var upid = service.JoinCluster(session, Hostname, Fingerprint, plainPassword,
+ linkDict, NodeId, Votes, Force.IsPresent ? true : (bool?)null);
+ WriteObject(upid);
+ }
+ finally
+ {
+ if (ptr != IntPtr.Zero)
+ Marshal.ZeroFreeGlobalAllocUnicode(ptr);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigCmdlet.cs
new file mode 100644
index 0000000..88cf7cd
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigCmdlet.cs
@@ -0,0 +1,28 @@
+using System.Management.Automation;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Gets the cluster configuration.
+ ///
+ /// Returns the raw cluster configuration including nodes, totem settings,
+ /// and cluster version information.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveClusterConfig")]
+ [OutputType(typeof(PSObject))]
+ public sealed class GetPveClusterConfigCmdlet : PveCmdletBase
+ {
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ WriteVerbose("Getting cluster configuration...");
+ var config = service.GetClusterConfig(session);
+ WriteObject(config);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigNodeCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigNodeCmdlet.cs
new file mode 100644
index 0000000..b89be61
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigNodeCmdlet.cs
@@ -0,0 +1,30 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Cluster;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Lists nodes in the cluster configuration.
+ ///
+ /// Returns all nodes registered in the Corosync cluster configuration,
+ /// including their node IDs and ring addresses.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveClusterConfigNode")]
+ [OutputType(typeof(PveClusterConfigNode))]
+ public sealed class GetPveClusterConfigNodeCmdlet : PveCmdletBase
+ {
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ WriteVerbose("Getting cluster config nodes...");
+ var nodes = service.GetConfigNodes(session);
+
+ foreach (var node in nodes)
+ WriteObject(node);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterJoinInfoCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterJoinInfoCmdlet.cs
new file mode 100644
index 0000000..a4a1dcb
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterJoinInfoCmdlet.cs
@@ -0,0 +1,33 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Cluster;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Gets cluster join information.
+ ///
+ /// Returns the information needed to join this cluster, including the
+ /// certificate fingerprint, node list, and totem configuration.
+ /// Run this on an existing cluster member to get join credentials.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveClusterJoinInfo")]
+ [OutputType(typeof(PveClusterJoinInfo))]
+ public sealed class GetPveClusterJoinInfoCmdlet : PveCmdletBase
+ {
+ /// Node to get join info for (defaults to current node).
+ [Parameter(Mandatory = false, Position = 0, HelpMessage = "Node to get join info for (defaults to current).")]
+ public string? Node { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ WriteVerbose("Getting cluster join information...");
+ var joinInfo = service.GetJoinInfo(session, Node);
+ WriteObject(joinInfo);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterNextIdCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterNextIdCmdlet.cs
new file mode 100644
index 0000000..413da3f
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterNextIdCmdlet.cs
@@ -0,0 +1,32 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Gets the next available VM/CT ID in the cluster.
+ ///
+ /// Returns the next free VMID from the cluster's auto-allocation pool.
+ /// Optionally validates whether a specific VMID is available.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveClusterNextId")]
+ [OutputType(typeof(int))]
+ public sealed class GetPveClusterNextIdCmdlet : PveCmdletBase
+ {
+ /// Optional VMID to check availability for.
+ [Parameter(Mandatory = false, Position = 0, HelpMessage = "Optional VMID to check availability for.")]
+ [ValidateRange(100, 999999999)]
+ public int? VmId { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ WriteVerbose("Getting next available cluster VMID...");
+ var nextId = service.GetNextId(session, VmId);
+ WriteObject(nextId);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterOptionCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterOptionCmdlet.cs
new file mode 100644
index 0000000..66e4f48
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterOptionCmdlet.cs
@@ -0,0 +1,28 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Cluster;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Gets cluster-wide datacenter options.
+ ///
+ /// Returns the datacenter-level cluster options including keyboard layout,
+ /// language, migration settings, HA settings, and more.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveClusterOption")]
+ [OutputType(typeof(PveClusterOptions))]
+ public sealed class GetPveClusterOptionCmdlet : PveCmdletBase
+ {
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ WriteVerbose("Getting cluster options...");
+ var options = service.GetClusterOptions(session);
+ WriteObject(options);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterStatusCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterStatusCmdlet.cs
new file mode 100644
index 0000000..50a8a45
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterStatusCmdlet.cs
@@ -0,0 +1,30 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.Cluster;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Gets the current cluster status.
+ ///
+ /// Returns the cluster status including cluster-wide info and per-node entries.
+ /// Filter by Type to distinguish cluster vs node records.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveClusterStatus")]
+ [OutputType(typeof(PveClusterStatus))]
+ public sealed class GetPveClusterStatusCmdlet : PveCmdletBase
+ {
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ WriteVerbose("Getting cluster status...");
+ var statuses = service.GetClusterStatus(session);
+
+ foreach (var status in statuses)
+ WriteObject(status);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/NewPveClusterCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/NewPveClusterCmdlet.cs
new file mode 100644
index 0000000..503c2f3
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/NewPveClusterCmdlet.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Creates a new Proxmox VE cluster.
+ ///
+ /// Initializes a new Corosync cluster on the current node. This is a
+ /// destructive operation that should only be performed once per cluster.
+ /// Additional nodes can then join using Add-PveClusterMember.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveCluster", SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ [OutputType(typeof(string))]
+ public sealed class NewPveClusterCmdlet : PveCmdletBase
+ {
+ /// The name for the new cluster.
+ [Parameter(Mandatory = true, Position = 0, HelpMessage = "The name for the new cluster (max 15 chars).")]
+ [ValidateLength(1, 15)]
+ public string ClusterName { get; set; } = string.Empty;
+
+ /// Node ID for this node.
+ [Parameter(Mandatory = false, HelpMessage = "Corosync node ID for this node.")]
+ [ValidateRange(1, int.MaxValue)]
+ public int? NodeId { get; set; }
+
+ /// Number of quorum votes for this node.
+ [Parameter(Mandatory = false, HelpMessage = "Number of quorum votes.")]
+ [ValidateRange(1, int.MaxValue)]
+ public int? Votes { get; set; }
+
+ /// Corosync link addresses (link0..link7).
+ [Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
+ public string[]? Links { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"cluster '{ClusterName}'", "Create new cluster"))
+ return;
+
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ Dictionary? linkDict = null;
+ if (Links != null)
+ {
+ linkDict = new Dictionary();
+ foreach (var link in Links)
+ {
+ var parts = link.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ linkDict[parts[0]] = parts[1];
+ }
+ }
+
+ WriteVerbose($"Creating cluster '{ClusterName}'...");
+ var upid = service.CreateCluster(session, ClusterName, linkDict, NodeId, Votes);
+ WriteObject(upid);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/RemovePveClusterConfigNodeCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/RemovePveClusterConfigNodeCmdlet.cs
new file mode 100644
index 0000000..675f86e
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/RemovePveClusterConfigNodeCmdlet.cs
@@ -0,0 +1,35 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Removes a node from the cluster configuration.
+ ///
+ /// Removes a node from the Corosync cluster configuration. The node must be
+ /// offline or already evacuated before removal. This is a destructive operation
+ /// that changes cluster topology.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveClusterConfigNode", SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ [OutputType(typeof(void))]
+ public sealed class RemovePveClusterConfigNodeCmdlet : PveCmdletBase
+ {
+ /// The name of the node to remove.
+ [Parameter(Mandatory = true, Position = 0, HelpMessage = "The cluster node name to remove.")]
+ public string Node { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"node '{Node}'", "Remove from cluster configuration"))
+ return;
+
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ WriteVerbose($"Removing node '{Node}' from cluster configuration...");
+ service.RemoveConfigNode(session, Node);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/Cluster/SetPveClusterOptionCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Cluster/SetPveClusterOptionCmdlet.cs
new file mode 100644
index 0000000..5d47e7b
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/Cluster/SetPveClusterOptionCmdlet.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.Cluster
+{
+ ///
+ /// Sets cluster-wide datacenter options.
+ ///
+ /// Updates datacenter-level cluster options such as keyboard layout, language,
+ /// migration settings, console preference, and other global settings.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Set, "PveClusterOption", SupportsShouldProcess = true)]
+ [OutputType(typeof(void))]
+ public sealed class SetPveClusterOptionCmdlet : PveCmdletBase
+ {
+ /// Default keyboard layout for VNC.
+ [Parameter(Mandatory = false, HelpMessage = "Default keyboard layout for VNC.")]
+ [ValidateSet("de", "de-ch", "da", "en-gb", "en-us", "es", "fi", "fr", "fr-be",
+ "fr-ca", "fr-ch", "hu", "is", "it", "ja", "lt", "mk", "nl", "no", "pl",
+ "pt", "pt-br", "sv", "sl", "tr")]
+ public string? Keyboard { get; set; }
+
+ /// Default GUI language.
+ [Parameter(Mandatory = false, HelpMessage = "Default GUI language.")]
+ public string? Language { get; set; }
+
+ /// Default console viewer.
+ [Parameter(Mandatory = false, HelpMessage = "Default console viewer.")]
+ [ValidateSet("applet", "vv", "html5", "xtermjs")]
+ public string? Console { get; set; }
+
+ /// Email address for notifications.
+ [Parameter(Mandatory = false, HelpMessage = "Email address to send notifications from.")]
+ public string? EmailFrom { get; set; }
+
+ /// External HTTP proxy URL.
+ [Parameter(Mandatory = false, HelpMessage = "External HTTP proxy URL for downloads.")]
+ public string? HttpProxy { get; set; }
+
+ /// MAC address prefix for auto-generated guest MACs.
+ [Parameter(Mandatory = false, HelpMessage = "MAC address prefix for virtual guests.")]
+ public string? MacPrefix { get; set; }
+
+ /// Max workers per node for bulk operations.
+ [Parameter(Mandatory = false, HelpMessage = "Max workers per node for bulk operations like stopall.")]
+ [ValidateRange(1, int.MaxValue)]
+ public int? MaxWorkers { get; set; }
+
+ /// Fencing mode.
+ [Parameter(Mandatory = false, HelpMessage = "HA fencing mode.")]
+ [ValidateSet("watchdog", "hardware", "both")]
+ public string? Fencing { get; set; }
+
+ /// Migration settings.
+ [Parameter(Mandatory = false, HelpMessage = "Cluster-wide migration settings string.")]
+ public string? Migration { get; set; }
+
+ /// Datacenter description.
+ [Parameter(Mandatory = false, HelpMessage = "Datacenter description shown in the web UI.")]
+ public string? Description { get; set; }
+
+ /// Settings to delete.
+ [Parameter(Mandatory = false, HelpMessage = "Comma-separated list of settings to delete/reset.")]
+ public string? Delete { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess("cluster options", "Set"))
+ return;
+
+ var session = GetSession();
+ var service = new ClusterConfigService();
+
+ var data = new Dictionary();
+ if (Keyboard != null) data["keyboard"] = Keyboard;
+ if (Language != null) data["language"] = Language;
+ if (Console != null) data["console"] = Console;
+ if (EmailFrom != null) data["email_from"] = EmailFrom;
+ if (HttpProxy != null) data["http_proxy"] = HttpProxy;
+ if (MacPrefix != null) data["mac_prefix"] = MacPrefix;
+ if (MaxWorkers.HasValue) data["max_workers"] = MaxWorkers.Value.ToString();
+ if (Fencing != null) data["fencing"] = Fencing;
+ if (Migration != null) data["migration"] = Migration;
+ if (Description != null) data["description"] = Description;
+ if (Delete != null) data["delete"] = Delete;
+
+ WriteVerbose("Setting cluster options...");
+ service.SetClusterOptions(session, data);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/GetPveHaGroupCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/GetPveHaGroupCmdlet.cs
new file mode 100644
index 0000000..eab7fab
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/GetPveHaGroupCmdlet.cs
@@ -0,0 +1,43 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.HA;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Gets HA groups.
+ ///
+ /// Lists all HA groups or retrieves a specific group by name. HA groups
+ /// define which nodes a resource can run on and their priorities.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveHaGroup")]
+ [OutputType(typeof(PveHaGroup))]
+ public sealed class GetPveHaGroupCmdlet : PveCmdletBase
+ {
+ /// Optional group name to retrieve.
+ [Parameter(Mandatory = false, Position = 0, ValueFromPipelineByPropertyName = true,
+ HelpMessage = "HA group name. Omit to list all groups.")]
+ public string? Group { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new HaService();
+
+ if (!string.IsNullOrEmpty(Group))
+ {
+ WriteVerbose($"Getting HA group '{Group}'...");
+ var group = service.GetGroup(session, Group!);
+ WriteObject(group);
+ }
+ else
+ {
+ WriteVerbose("Listing all HA groups...");
+ var groups = service.GetGroups(session);
+ foreach (var g in groups)
+ WriteObject(g);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/GetPveHaResourceCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/GetPveHaResourceCmdlet.cs
new file mode 100644
index 0000000..9318125
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/GetPveHaResourceCmdlet.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.HA;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Gets HA managed resources.
+ ///
+ /// Lists all resources managed by the HA manager, or retrieves a specific
+ /// resource by its service ID (e.g. "vm:100", "ct:200").
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveHaResource")]
+ [OutputType(typeof(PveHaResource))]
+ public sealed class GetPveHaResourceCmdlet : PveCmdletBase
+ {
+ /// Optional service ID to retrieve a specific resource.
+ [Parameter(Mandatory = false, Position = 0, ValueFromPipelineByPropertyName = true,
+ HelpMessage = "Service ID (e.g. 'vm:100', 'ct:200'). Omit to list all.")]
+ public string? Sid { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new HaService();
+
+ if (!string.IsNullOrEmpty(Sid))
+ {
+ WriteVerbose($"Getting HA resource '{Sid}'...");
+ var resource = service.GetResource(session, Sid!);
+ WriteObject(resource);
+ }
+ else
+ {
+ WriteVerbose("Listing all HA resources...");
+ var resources = service.GetResources(session);
+ foreach (var r in resources)
+ WriteObject(r);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/GetPveHaRuleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/GetPveHaRuleCmdlet.cs
new file mode 100644
index 0000000..a7395ed
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/GetPveHaRuleCmdlet.cs
@@ -0,0 +1,45 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.HA;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Gets HA rules.
+ ///
+ /// Lists all HA rules or retrieves a specific rule by ID.
+ /// Requires Proxmox VE 9.0 or later.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveHaRule")]
+ [OutputType(typeof(PveHaRule))]
+ public sealed class GetPveHaRuleCmdlet : PveCmdletBase
+ {
+ /// Optional rule ID.
+ [Parameter(Mandatory = false, Position = 0, ValueFromPipelineByPropertyName = true,
+ HelpMessage = "HA rule ID. Omit to list all rules.")]
+ public string? Rule { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ RequireVersion(session, "HA Rules", 9, 0);
+
+ var service = new HaService();
+
+ if (!string.IsNullOrEmpty(Rule))
+ {
+ WriteVerbose($"Getting HA rule '{Rule}'...");
+ var rule = service.GetRule(session, Rule!);
+ WriteObject(rule);
+ }
+ else
+ {
+ WriteVerbose("Listing all HA rules...");
+ var rules = service.GetRules(session);
+ foreach (var r in rules)
+ WriteObject(r);
+ }
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/GetPveHaStatusCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/GetPveHaStatusCmdlet.cs
new file mode 100644
index 0000000..586b302
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/GetPveHaStatusCmdlet.cs
@@ -0,0 +1,30 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Models.HA;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Gets the current HA manager status.
+ ///
+ /// Returns the current status of the HA manager including quorum state,
+ /// manager status, and service status entries.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Get, "PveHaStatus")]
+ [OutputType(typeof(PveHaStatus))]
+ public sealed class GetPveHaStatusCmdlet : PveCmdletBase
+ {
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ var service = new HaService();
+
+ WriteVerbose("Getting HA status...");
+ var statuses = service.GetStatus(session);
+
+ foreach (var status in statuses)
+ WriteObject(status);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/MovePveHaResourceCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/MovePveHaResourceCmdlet.cs
new file mode 100644
index 0000000..21bc4b0
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/MovePveHaResourceCmdlet.cs
@@ -0,0 +1,48 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Requests HA migration or relocation of a resource.
+ ///
+ /// Requests the HA manager to migrate or relocate a managed resource to a
+ /// different node. Use -Mode Migrate for online migration or -Mode Relocate
+ /// for offline relocation.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Move, "PveHaResource", SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ [OutputType(typeof(void))]
+ public sealed class MovePveHaResourceCmdlet : PveCmdletBase
+ {
+ /// Service ID of the resource to move.
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
+ HelpMessage = "Service ID (e.g. 'vm:100', 'ct:200').")]
+ public string Sid { get; set; } = string.Empty;
+
+ /// Target node name.
+ [Parameter(Mandatory = true, Position = 1, HelpMessage = "Target node name.")]
+ public string Node { get; set; } = string.Empty;
+
+ /// Migration mode: Migrate (online) or Relocate (offline).
+ [Parameter(Mandatory = false, HelpMessage = "Migration mode: Migrate (online) or Relocate (offline).")]
+ [ValidateSet("Migrate", "Relocate")]
+ public string Mode { get; set; } = "Migrate";
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"HA resource '{Sid}' to node '{Node}'", $"{Mode}"))
+ return;
+
+ var session = GetSession();
+ var service = new HaService();
+
+ WriteVerbose($"{Mode} HA resource '{Sid}' to node '{Node}'...");
+ if (Mode == "Relocate")
+ service.RelocateResource(session, Sid, Node);
+ else
+ service.MigrateResource(session, Sid, Node);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/NewPveHaGroupCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/NewPveHaGroupCmdlet.cs
new file mode 100644
index 0000000..21dd425
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/NewPveHaGroupCmdlet.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Creates a new HA group.
+ ///
+ /// Creates an HA group that defines which nodes a managed resource can run on
+ /// and their priorities. Format for Nodes: "node1:priority,node2:priority".
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveHaGroup", SupportsShouldProcess = true)]
+ [OutputType(typeof(void))]
+ public sealed class NewPveHaGroupCmdlet : PveCmdletBase
+ {
+ /// Group name.
+ [Parameter(Mandatory = true, Position = 0, HelpMessage = "HA group name.")]
+ public string Group { get; set; } = string.Empty;
+
+ /// Node list with priorities.
+ [Parameter(Mandatory = true, Position = 1, HelpMessage = "Node list: 'node1:pri,node2:pri'.")]
+ public string Nodes { get; set; } = string.Empty;
+
+ /// Restrict resources to this group's nodes.
+ [Parameter(Mandatory = false, HelpMessage = "If set, resources can only run on this group's nodes.")]
+ public SwitchParameter Restricted { get; set; }
+
+ /// Disable failback to higher-priority nodes.
+ [Parameter(Mandatory = false, HelpMessage = "Disable automatic failback to higher-priority nodes.")]
+ public SwitchParameter NoFailback { get; set; }
+
+ /// Description/comment.
+ [Parameter(Mandatory = false, HelpMessage = "Description or comment.")]
+ public string? Comment { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"HA group '{Group}'", "Create"))
+ return;
+
+ var session = GetSession();
+ var service = new HaService();
+
+ var options = new Dictionary();
+ if (Restricted.IsPresent) options["restricted"] = "1";
+ if (NoFailback.IsPresent) options["nofailback"] = "1";
+ if (Comment != null) options["comment"] = Comment;
+
+ WriteVerbose($"Creating HA group '{Group}'...");
+ service.CreateGroup(session, Group, Nodes, options);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/NewPveHaResourceCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/NewPveHaResourceCmdlet.cs
new file mode 100644
index 0000000..a20781d
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/NewPveHaResourceCmdlet.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Creates a new HA managed resource.
+ ///
+ /// Adds a VM or container to HA management. The resource will be monitored
+ /// and automatically restarted or migrated on node failure.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveHaResource", SupportsShouldProcess = true)]
+ [OutputType(typeof(void))]
+ public sealed class NewPveHaResourceCmdlet : PveCmdletBase
+ {
+ /// Service ID (e.g. "vm:100" or "ct:200").
+ [Parameter(Mandatory = true, Position = 0, HelpMessage = "Service ID (e.g. 'vm:100', 'ct:200').")]
+ public string Sid { get; set; } = string.Empty;
+
+ /// Desired state for the resource.
+ [Parameter(Mandatory = false, HelpMessage = "Desired state: started, stopped, disabled, ignored.")]
+ [ValidateSet("started", "stopped", "disabled", "ignored")]
+ public string? State { get; set; }
+
+ /// HA group to assign this resource to.
+ [Parameter(Mandatory = false, HelpMessage = "HA group name.")]
+ public string? Group { get; set; }
+
+ /// Maximum number of relocations before giving up.
+ [Parameter(Mandatory = false, HelpMessage = "Maximum relocations before giving up.")]
+ [ValidateRange(0, 10)]
+ public int? MaxRelocate { get; set; }
+
+ /// Maximum number of restart attempts.
+ [Parameter(Mandatory = false, HelpMessage = "Maximum restart attempts.")]
+ [ValidateRange(0, 10)]
+ public int? MaxRestart { get; set; }
+
+ /// Description/comment.
+ [Parameter(Mandatory = false, HelpMessage = "Description or comment.")]
+ public string? Comment { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"HA resource '{Sid}'", "Create"))
+ return;
+
+ var session = GetSession();
+ var service = new HaService();
+
+ var options = new Dictionary();
+ if (State != null) options["state"] = State;
+ if (Group != null) options["group"] = Group;
+ if (MaxRelocate.HasValue) options["max_relocate"] = MaxRelocate.Value.ToString();
+ if (MaxRestart.HasValue) options["max_restart"] = MaxRestart.Value.ToString();
+ if (Comment != null) options["comment"] = Comment;
+
+ WriteVerbose($"Creating HA resource '{Sid}'...");
+ service.CreateResource(session, Sid, options);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/NewPveHaRuleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/NewPveHaRuleCmdlet.cs
new file mode 100644
index 0000000..a46f3e4
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/NewPveHaRuleCmdlet.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Creates a new HA rule.
+ ///
+ /// Creates a new HA rule for the cluster. Requires Proxmox VE 9.0 or later.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.New, "PveHaRule", SupportsShouldProcess = true)]
+ [OutputType(typeof(void))]
+ public sealed class NewPveHaRuleCmdlet : PveCmdletBase
+ {
+ /// Rule type.
+ [Parameter(Mandatory = true, Position = 0, HelpMessage = "Rule type.")]
+ public string Type { get; set; } = string.Empty;
+
+ /// Rule state.
+ [Parameter(Mandatory = false, HelpMessage = "Rule state: enabled or disabled.")]
+ [ValidateSet("enabled", "disabled")]
+ public string? State { get; set; }
+
+ /// Description/comment.
+ [Parameter(Mandatory = false, HelpMessage = "Description or comment.")]
+ public string? Comment { get; set; }
+
+ /// Additional rule properties as a hashtable.
+ [Parameter(Mandatory = false, HelpMessage = "Additional rule properties.")]
+ public System.Collections.Hashtable? Properties { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ RequireVersion(session, "HA Rules", 9, 0);
+
+ if (!ShouldProcess($"HA rule of type '{Type}'", "Create"))
+ return;
+
+ var service = new HaService();
+
+ var data = new Dictionary { ["type"] = Type };
+ if (State != null) data["state"] = State;
+ if (Comment != null) data["comment"] = Comment;
+ if (Properties != null)
+ {
+ foreach (var key in Properties.Keys)
+ data[key.ToString()!] = Properties[key]!.ToString()!;
+ }
+
+ WriteVerbose($"Creating HA rule of type '{Type}'...");
+ service.CreateRule(session, data);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/RemovePveHaGroupCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/RemovePveHaGroupCmdlet.cs
new file mode 100644
index 0000000..121b70b
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/RemovePveHaGroupCmdlet.cs
@@ -0,0 +1,35 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Deletes an HA group.
+ ///
+ /// Removes an HA group definition. Resources assigned to this group will
+ /// need to be reassigned before deletion if the group is in use.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveHaGroup", SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ [OutputType(typeof(void))]
+ public sealed class RemovePveHaGroupCmdlet : PveCmdletBase
+ {
+ /// Group name to delete.
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
+ HelpMessage = "HA group name to delete.")]
+ public string Group { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"HA group '{Group}'", "Delete"))
+ return;
+
+ var session = GetSession();
+ var service = new HaService();
+
+ WriteVerbose($"Deleting HA group '{Group}'...");
+ service.DeleteGroup(session, Group);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/RemovePveHaResourceCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/RemovePveHaResourceCmdlet.cs
new file mode 100644
index 0000000..c6897de
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/RemovePveHaResourceCmdlet.cs
@@ -0,0 +1,35 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Removes a resource from HA management.
+ ///
+ /// Removes a VM or container from HA management. The resource will no longer
+ /// be monitored for failover.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveHaResource", SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ [OutputType(typeof(void))]
+ public sealed class RemovePveHaResourceCmdlet : PveCmdletBase
+ {
+ /// Service ID of the resource to remove.
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
+ HelpMessage = "Service ID (e.g. 'vm:100', 'ct:200').")]
+ public string Sid { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"HA resource '{Sid}'", "Remove from HA management"))
+ return;
+
+ var session = GetSession();
+ var service = new HaService();
+
+ WriteVerbose($"Removing HA resource '{Sid}'...");
+ service.DeleteResource(session, Sid);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/RemovePveHaRuleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/RemovePveHaRuleCmdlet.cs
new file mode 100644
index 0000000..c722297
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/RemovePveHaRuleCmdlet.cs
@@ -0,0 +1,36 @@
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Deletes an HA rule.
+ ///
+ /// Removes an HA rule from the cluster. Requires Proxmox VE 9.0 or later.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Remove, "PveHaRule", SupportsShouldProcess = true,
+ ConfirmImpact = ConfirmImpact.High)]
+ [OutputType(typeof(void))]
+ public sealed class RemovePveHaRuleCmdlet : PveCmdletBase
+ {
+ /// Rule ID to delete.
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
+ HelpMessage = "HA rule ID to delete.")]
+ public string Rule { get; set; } = string.Empty;
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ RequireVersion(session, "HA Rules", 9, 0);
+
+ if (!ShouldProcess($"HA rule '{Rule}'", "Delete"))
+ return;
+
+ var service = new HaService();
+
+ WriteVerbose($"Deleting HA rule '{Rule}'...");
+ service.DeleteRule(session, Rule);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/SetPveHaGroupCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/SetPveHaGroupCmdlet.cs
new file mode 100644
index 0000000..69ac363
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/SetPveHaGroupCmdlet.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Updates an HA group.
+ ///
+ /// Modifies an existing HA group's node list, restriction settings, or comment.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Set, "PveHaGroup", SupportsShouldProcess = true)]
+ [OutputType(typeof(void))]
+ public sealed class SetPveHaGroupCmdlet : PveCmdletBase
+ {
+ /// Group name to update.
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
+ HelpMessage = "HA group name.")]
+ public string Group { get; set; } = string.Empty;
+
+ /// Updated node list.
+ [Parameter(Mandatory = false, HelpMessage = "Node list: 'node1:pri,node2:pri'.")]
+ public string? Nodes { get; set; }
+
+ /// Restrict resources to this group's nodes.
+ [Parameter(Mandatory = false, HelpMessage = "Restrict resources to this group's nodes (0 or 1).")]
+ [ValidateRange(0, 1)]
+ public int? Restricted { get; set; }
+
+ /// Disable failback.
+ [Parameter(Mandatory = false, HelpMessage = "Disable automatic failback (0 or 1).")]
+ [ValidateRange(0, 1)]
+ public int? NoFailback { get; set; }
+
+ /// Description/comment.
+ [Parameter(Mandatory = false, HelpMessage = "Description or comment.")]
+ public string? Comment { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"HA group '{Group}'", "Update"))
+ return;
+
+ var session = GetSession();
+ var service = new HaService();
+
+ var data = new Dictionary();
+ if (Nodes != null) data["nodes"] = Nodes;
+ if (Restricted.HasValue) data["restricted"] = Restricted.Value.ToString();
+ if (NoFailback.HasValue) data["nofailback"] = NoFailback.Value.ToString();
+ if (Comment != null) data["comment"] = Comment;
+
+ WriteVerbose($"Updating HA group '{Group}'...");
+ service.UpdateGroup(session, Group, data);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/SetPveHaResourceCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/SetPveHaResourceCmdlet.cs
new file mode 100644
index 0000000..9c3b0a5
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/SetPveHaResourceCmdlet.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Updates an HA managed resource.
+ ///
+ /// Modifies the configuration of an existing HA managed resource,
+ /// such as changing its state, group, or restart limits.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Set, "PveHaResource", SupportsShouldProcess = true)]
+ [OutputType(typeof(void))]
+ public sealed class SetPveHaResourceCmdlet : PveCmdletBase
+ {
+ /// Service ID of the resource to update.
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
+ HelpMessage = "Service ID (e.g. 'vm:100', 'ct:200').")]
+ public string Sid { get; set; } = string.Empty;
+
+ /// Desired state.
+ [Parameter(Mandatory = false, HelpMessage = "Desired state: started, stopped, disabled, ignored.")]
+ [ValidateSet("started", "stopped", "disabled", "ignored")]
+ public string? State { get; set; }
+
+ /// HA group name.
+ [Parameter(Mandatory = false, HelpMessage = "HA group name.")]
+ public string? Group { get; set; }
+
+ /// Maximum relocations.
+ [Parameter(Mandatory = false, HelpMessage = "Maximum relocations before giving up.")]
+ [ValidateRange(0, 10)]
+ public int? MaxRelocate { get; set; }
+
+ /// Maximum restart attempts.
+ [Parameter(Mandatory = false, HelpMessage = "Maximum restart attempts.")]
+ [ValidateRange(0, 10)]
+ public int? MaxRestart { get; set; }
+
+ /// Description/comment.
+ [Parameter(Mandatory = false, HelpMessage = "Description or comment.")]
+ public string? Comment { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess($"HA resource '{Sid}'", "Update"))
+ return;
+
+ var session = GetSession();
+ var service = new HaService();
+
+ var data = new Dictionary();
+ if (State != null) data["state"] = State;
+ if (Group != null) data["group"] = Group;
+ if (MaxRelocate.HasValue) data["max_relocate"] = MaxRelocate.Value.ToString();
+ if (MaxRestart.HasValue) data["max_restart"] = MaxRestart.Value.ToString();
+ if (Comment != null) data["comment"] = Comment;
+
+ WriteVerbose($"Updating HA resource '{Sid}'...");
+ service.UpdateResource(session, Sid, data);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/Cmdlets/HA/SetPveHaRuleCmdlet.cs b/src/PSProxmoxVE/Cmdlets/HA/SetPveHaRuleCmdlet.cs
new file mode 100644
index 0000000..8c43535
--- /dev/null
+++ b/src/PSProxmoxVE/Cmdlets/HA/SetPveHaRuleCmdlet.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using System.Management.Automation;
+using PSProxmoxVE.Core.Services;
+
+namespace PSProxmoxVE.Cmdlets.HA
+{
+ ///
+ /// Updates an HA rule.
+ ///
+ /// Modifies an existing HA rule. Requires Proxmox VE 9.0 or later.
+ ///
+ ///
+ [Cmdlet(VerbsCommon.Set, "PveHaRule", SupportsShouldProcess = true)]
+ [OutputType(typeof(void))]
+ public sealed class SetPveHaRuleCmdlet : PveCmdletBase
+ {
+ /// Rule ID to update.
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
+ HelpMessage = "HA rule ID.")]
+ public string Rule { get; set; } = string.Empty;
+
+ /// Rule state.
+ [Parameter(Mandatory = false, HelpMessage = "Rule state: enabled or disabled.")]
+ [ValidateSet("enabled", "disabled")]
+ public string? State { get; set; }
+
+ /// Description/comment.
+ [Parameter(Mandatory = false, HelpMessage = "Description or comment.")]
+ public string? Comment { get; set; }
+
+ /// Additional rule properties.
+ [Parameter(Mandatory = false, HelpMessage = "Additional rule properties.")]
+ public System.Collections.Hashtable? Properties { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var session = GetSession();
+ RequireVersion(session, "HA Rules", 9, 0);
+
+ if (!ShouldProcess($"HA rule '{Rule}'", "Update"))
+ return;
+
+ var service = new HaService();
+
+ var data = new Dictionary();
+ if (State != null) data["state"] = State;
+ if (Comment != null) data["comment"] = Comment;
+ if (Properties != null)
+ {
+ foreach (var key in Properties.Keys)
+ data[key.ToString()!] = Properties[key]!.ToString()!;
+ }
+
+ WriteVerbose($"Updating HA rule '{Rule}'...");
+ service.UpdateRule(session, Rule, data);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE/PSProxmoxVE.psd1 b/src/PSProxmoxVE/PSProxmoxVE.psd1
index 9a25937..adb30bc 100644
--- a/src/PSProxmoxVE/PSProxmoxVE.psd1
+++ b/src/PSProxmoxVE/PSProxmoxVE.psd1
@@ -233,6 +233,39 @@
# Cluster
'Get-PveClusterResource',
+ 'Get-PveClusterStatus',
+ 'Get-PveClusterNextId',
+ 'Get-PveClusterOption',
+ 'Set-PveClusterOption',
+ 'Get-PveClusterConfig',
+ 'Get-PveClusterConfigNode',
+ 'Add-PveClusterConfigNode',
+ 'Remove-PveClusterConfigNode',
+ 'Get-PveClusterJoinInfo',
+ 'Add-PveClusterMember',
+ 'New-PveCluster',
+
+ # HA — Resources
+ 'Get-PveHaResource',
+ 'New-PveHaResource',
+ 'Set-PveHaResource',
+ 'Remove-PveHaResource',
+ 'Move-PveHaResource',
+
+ # HA — Groups
+ 'Get-PveHaGroup',
+ 'New-PveHaGroup',
+ 'Set-PveHaGroup',
+ 'Remove-PveHaGroup',
+
+ # HA — Status
+ 'Get-PveHaStatus',
+
+ # HA — Rules (PVE 9.0+)
+ 'Get-PveHaRule',
+ 'New-PveHaRule',
+ 'Set-PveHaRule',
+ 'Remove-PveHaRule',
# Tasks
'Get-PveTaskList',