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:
Clint Branham
2026-03-24 18:28:11 -05:00
parent 0789601e33
commit da2037d0c5
35 changed files with 2598 additions and 0 deletions
@@ -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();
}
}
}
}
+492
View File
@@ -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();
}
}
}
}
@@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Adds a node to the cluster configuration.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Add, "PveClusterConfigNode", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(string))]
public sealed class AddPveClusterConfigNodeCmdlet : PveCmdletBase
{
/// <summary>The name of the node to add.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The cluster node name to add.")]
public string Node { get; set; } = string.Empty;
/// <summary>IP address of the new node (fallback if no links given).</summary>
[Parameter(Mandatory = false, HelpMessage = "IP address of the new node.")]
public string? NewNodeIp { get; set; }
/// <summary>Node ID for the new node.</summary>
[Parameter(Mandatory = false, HelpMessage = "Corosync node ID.")]
[ValidateRange(1, int.MaxValue)]
public int? NodeId { get; set; }
/// <summary>Number of votes for the new node.</summary>
[Parameter(Mandatory = false, HelpMessage = "Number of quorum votes.")]
[ValidateRange(0, int.MaxValue)]
public int? Votes { get; set; }
/// <summary>Do not throw error if node already exists.</summary>
[Parameter(Mandatory = false, HelpMessage = "Force add even if node already exists.")]
public SwitchParameter Force { get; set; }
/// <summary>The JOIN_API_VERSION of the new node.</summary>
[Parameter(Mandatory = false, HelpMessage = "JOIN_API_VERSION of the new node.")]
public int? ApiVersion { get; set; }
/// <summary>Corosync link addresses (link0..link7).</summary>
[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<string, string>? linkDict = null;
if (Links != null)
{
linkDict = new Dictionary<string, string>();
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);
}
}
}
@@ -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
{
/// <summary>
/// <para type="synopsis">Joins this node to an existing cluster.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Add, "PveClusterMember", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(string))]
public sealed class AddPveClusterMemberCmdlet : PveCmdletBase
{
/// <summary>Hostname or IP of an existing cluster member.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "Hostname or IP of an existing cluster member.")]
public string Hostname { get; set; } = string.Empty;
/// <summary>Certificate SHA-256 fingerprint of the cluster.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "Certificate SHA-256 fingerprint of the cluster.")]
public string Fingerprint { get; set; } = string.Empty;
/// <summary>Root password of the existing cluster member.</summary>
[Parameter(Mandatory = true, HelpMessage = "Root password of the cluster member.")]
public SecureString Password { get; set; } = new SecureString();
/// <summary>Node ID for this node.</summary>
[Parameter(Mandatory = false, HelpMessage = "Corosync node ID for this node.")]
[ValidateRange(1, int.MaxValue)]
public int? NodeId { get; set; }
/// <summary>Number of quorum votes for this node.</summary>
[Parameter(Mandatory = false, HelpMessage = "Number of quorum votes.")]
[ValidateRange(0, int.MaxValue)]
public int? Votes { get; set; }
/// <summary>Force join even if already part of a cluster.</summary>
[Parameter(Mandatory = false, HelpMessage = "Force join even if already part of a cluster.")]
public SwitchParameter Force { get; set; }
/// <summary>Corosync link addresses (link0..link7).</summary>
[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<string, string>? linkDict = null;
if (Links != null)
{
linkDict = new Dictionary<string, string>();
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);
}
}
}
}
@@ -0,0 +1,28 @@
using System.Management.Automation;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Gets the cluster configuration.</para>
/// <para type="description">
/// Returns the raw cluster configuration including nodes, totem settings,
/// and cluster version information.
/// </para>
/// </summary>
[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);
}
}
}
@@ -0,0 +1,30 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Cluster;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Lists nodes in the cluster configuration.</para>
/// <para type="description">
/// Returns all nodes registered in the Corosync cluster configuration,
/// including their node IDs and ring addresses.
/// </para>
/// </summary>
[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);
}
}
}
@@ -0,0 +1,33 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Cluster;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Gets cluster join information.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveClusterJoinInfo")]
[OutputType(typeof(PveClusterJoinInfo))]
public sealed class GetPveClusterJoinInfoCmdlet : PveCmdletBase
{
/// <summary>Node to get join info for (defaults to current node).</summary>
[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);
}
}
}
@@ -0,0 +1,32 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Gets the next available VM/CT ID in the cluster.</para>
/// <para type="description">
/// Returns the next free VMID from the cluster's auto-allocation pool.
/// Optionally validates whether a specific VMID is available.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveClusterNextId")]
[OutputType(typeof(int))]
public sealed class GetPveClusterNextIdCmdlet : PveCmdletBase
{
/// <summary>Optional VMID to check availability for.</summary>
[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);
}
}
}
@@ -0,0 +1,28 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Cluster;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Gets cluster-wide datacenter options.</para>
/// <para type="description">
/// Returns the datacenter-level cluster options including keyboard layout,
/// language, migration settings, HA settings, and more.
/// </para>
/// </summary>
[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);
}
}
}
@@ -0,0 +1,30 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Cluster;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Gets the current cluster status.</para>
/// <para type="description">
/// Returns the cluster status including cluster-wide info and per-node entries.
/// Filter by Type to distinguish cluster vs node records.
/// </para>
/// </summary>
[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);
}
}
}
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Creates a new Proxmox VE cluster.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveCluster", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(string))]
public sealed class NewPveClusterCmdlet : PveCmdletBase
{
/// <summary>The name for the new cluster.</summary>
[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;
/// <summary>Node ID for this node.</summary>
[Parameter(Mandatory = false, HelpMessage = "Corosync node ID for this node.")]
[ValidateRange(1, int.MaxValue)]
public int? NodeId { get; set; }
/// <summary>Number of quorum votes for this node.</summary>
[Parameter(Mandatory = false, HelpMessage = "Number of quorum votes.")]
[ValidateRange(1, int.MaxValue)]
public int? Votes { get; set; }
/// <summary>Corosync link addresses (link0..link7).</summary>
[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<string, string>? linkDict = null;
if (Links != null)
{
linkDict = new Dictionary<string, string>();
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);
}
}
}
@@ -0,0 +1,35 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Removes a node from the cluster configuration.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveClusterConfigNode", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(void))]
public sealed class RemovePveClusterConfigNodeCmdlet : PveCmdletBase
{
/// <summary>The name of the node to remove.</summary>
[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);
}
}
}
@@ -0,0 +1,93 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Cluster
{
/// <summary>
/// <para type="synopsis">Sets cluster-wide datacenter options.</para>
/// <para type="description">
/// Updates datacenter-level cluster options such as keyboard layout, language,
/// migration settings, console preference, and other global settings.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveClusterOption", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class SetPveClusterOptionCmdlet : PveCmdletBase
{
/// <summary>Default keyboard layout for VNC.</summary>
[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; }
/// <summary>Default GUI language.</summary>
[Parameter(Mandatory = false, HelpMessage = "Default GUI language.")]
public string? Language { get; set; }
/// <summary>Default console viewer.</summary>
[Parameter(Mandatory = false, HelpMessage = "Default console viewer.")]
[ValidateSet("applet", "vv", "html5", "xtermjs")]
public string? Console { get; set; }
/// <summary>Email address for notifications.</summary>
[Parameter(Mandatory = false, HelpMessage = "Email address to send notifications from.")]
public string? EmailFrom { get; set; }
/// <summary>External HTTP proxy URL.</summary>
[Parameter(Mandatory = false, HelpMessage = "External HTTP proxy URL for downloads.")]
public string? HttpProxy { get; set; }
/// <summary>MAC address prefix for auto-generated guest MACs.</summary>
[Parameter(Mandatory = false, HelpMessage = "MAC address prefix for virtual guests.")]
public string? MacPrefix { get; set; }
/// <summary>Max workers per node for bulk operations.</summary>
[Parameter(Mandatory = false, HelpMessage = "Max workers per node for bulk operations like stopall.")]
[ValidateRange(1, int.MaxValue)]
public int? MaxWorkers { get; set; }
/// <summary>Fencing mode.</summary>
[Parameter(Mandatory = false, HelpMessage = "HA fencing mode.")]
[ValidateSet("watchdog", "hardware", "both")]
public string? Fencing { get; set; }
/// <summary>Migration settings.</summary>
[Parameter(Mandatory = false, HelpMessage = "Cluster-wide migration settings string.")]
public string? Migration { get; set; }
/// <summary>Datacenter description.</summary>
[Parameter(Mandatory = false, HelpMessage = "Datacenter description shown in the web UI.")]
public string? Description { get; set; }
/// <summary>Settings to delete.</summary>
[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<string, string>();
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);
}
}
}
@@ -0,0 +1,43 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.HA;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Gets HA groups.</para>
/// <para type="description">
/// Lists all HA groups or retrieves a specific group by name. HA groups
/// define which nodes a resource can run on and their priorities.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveHaGroup")]
[OutputType(typeof(PveHaGroup))]
public sealed class GetPveHaGroupCmdlet : PveCmdletBase
{
/// <summary>Optional group name to retrieve.</summary>
[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);
}
}
}
}
@@ -0,0 +1,44 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.HA;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Gets HA managed resources.</para>
/// <para type="description">
/// Lists all resources managed by the HA manager, or retrieves a specific
/// resource by its service ID (e.g. "vm:100", "ct:200").
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveHaResource")]
[OutputType(typeof(PveHaResource))]
public sealed class GetPveHaResourceCmdlet : PveCmdletBase
{
/// <summary>Optional service ID to retrieve a specific resource.</summary>
[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);
}
}
}
}
@@ -0,0 +1,45 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.HA;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Gets HA rules.</para>
/// <para type="description">
/// Lists all HA rules or retrieves a specific rule by ID.
/// Requires Proxmox VE 9.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveHaRule")]
[OutputType(typeof(PveHaRule))]
public sealed class GetPveHaRuleCmdlet : PveCmdletBase
{
/// <summary>Optional rule ID.</summary>
[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);
}
}
}
}
@@ -0,0 +1,30 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.HA;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Gets the current HA manager status.</para>
/// <para type="description">
/// Returns the current status of the HA manager including quorum state,
/// manager status, and service status entries.
/// </para>
/// </summary>
[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);
}
}
}
@@ -0,0 +1,48 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Requests HA migration or relocation of a resource.</para>
/// <para type="description">
/// 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.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Move, "PveHaResource", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(void))]
public sealed class MovePveHaResourceCmdlet : PveCmdletBase
{
/// <summary>Service ID of the resource to move.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
HelpMessage = "Service ID (e.g. 'vm:100', 'ct:200').")]
public string Sid { get; set; } = string.Empty;
/// <summary>Target node name.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "Target node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>Migration mode: Migrate (online) or Relocate (offline).</summary>
[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);
}
}
}
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Creates a new HA group.</para>
/// <para type="description">
/// Creates an HA group that defines which nodes a managed resource can run on
/// and their priorities. Format for Nodes: "node1:priority,node2:priority".
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveHaGroup", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class NewPveHaGroupCmdlet : PveCmdletBase
{
/// <summary>Group name.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "HA group name.")]
public string Group { get; set; } = string.Empty;
/// <summary>Node list with priorities.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "Node list: 'node1:pri,node2:pri'.")]
public string Nodes { get; set; } = string.Empty;
/// <summary>Restrict resources to this group's nodes.</summary>
[Parameter(Mandatory = false, HelpMessage = "If set, resources can only run on this group's nodes.")]
public SwitchParameter Restricted { get; set; }
/// <summary>Disable failback to higher-priority nodes.</summary>
[Parameter(Mandatory = false, HelpMessage = "Disable automatic failback to higher-priority nodes.")]
public SwitchParameter NoFailback { get; set; }
/// <summary>Description/comment.</summary>
[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<string, string>();
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);
}
}
}
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Creates a new HA managed resource.</para>
/// <para type="description">
/// Adds a VM or container to HA management. The resource will be monitored
/// and automatically restarted or migrated on node failure.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveHaResource", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class NewPveHaResourceCmdlet : PveCmdletBase
{
/// <summary>Service ID (e.g. "vm:100" or "ct:200").</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "Service ID (e.g. 'vm:100', 'ct:200').")]
public string Sid { get; set; } = string.Empty;
/// <summary>Desired state for the resource.</summary>
[Parameter(Mandatory = false, HelpMessage = "Desired state: started, stopped, disabled, ignored.")]
[ValidateSet("started", "stopped", "disabled", "ignored")]
public string? State { get; set; }
/// <summary>HA group to assign this resource to.</summary>
[Parameter(Mandatory = false, HelpMessage = "HA group name.")]
public string? Group { get; set; }
/// <summary>Maximum number of relocations before giving up.</summary>
[Parameter(Mandatory = false, HelpMessage = "Maximum relocations before giving up.")]
[ValidateRange(0, 10)]
public int? MaxRelocate { get; set; }
/// <summary>Maximum number of restart attempts.</summary>
[Parameter(Mandatory = false, HelpMessage = "Maximum restart attempts.")]
[ValidateRange(0, 10)]
public int? MaxRestart { get; set; }
/// <summary>Description/comment.</summary>
[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<string, string>();
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);
}
}
}
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Creates a new HA rule.</para>
/// <para type="description">
/// Creates a new HA rule for the cluster. Requires Proxmox VE 9.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveHaRule", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class NewPveHaRuleCmdlet : PveCmdletBase
{
/// <summary>Rule type.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "Rule type.")]
public string Type { get; set; } = string.Empty;
/// <summary>Rule state.</summary>
[Parameter(Mandatory = false, HelpMessage = "Rule state: enabled or disabled.")]
[ValidateSet("enabled", "disabled")]
public string? State { get; set; }
/// <summary>Description/comment.</summary>
[Parameter(Mandatory = false, HelpMessage = "Description or comment.")]
public string? Comment { get; set; }
/// <summary>Additional rule properties as a hashtable.</summary>
[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<string, string> { ["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);
}
}
}
@@ -0,0 +1,35 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Deletes an HA group.</para>
/// <para type="description">
/// Removes an HA group definition. Resources assigned to this group will
/// need to be reassigned before deletion if the group is in use.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveHaGroup", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(void))]
public sealed class RemovePveHaGroupCmdlet : PveCmdletBase
{
/// <summary>Group name to delete.</summary>
[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);
}
}
}
@@ -0,0 +1,35 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Removes a resource from HA management.</para>
/// <para type="description">
/// Removes a VM or container from HA management. The resource will no longer
/// be monitored for failover.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveHaResource", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(void))]
public sealed class RemovePveHaResourceCmdlet : PveCmdletBase
{
/// <summary>Service ID of the resource to remove.</summary>
[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);
}
}
}
@@ -0,0 +1,36 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Deletes an HA rule.</para>
/// <para type="description">
/// Removes an HA rule from the cluster. Requires Proxmox VE 9.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveHaRule", SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(void))]
public sealed class RemovePveHaRuleCmdlet : PveCmdletBase
{
/// <summary>Rule ID to delete.</summary>
[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);
}
}
}
@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Updates an HA group.</para>
/// <para type="description">
/// Modifies an existing HA group's node list, restriction settings, or comment.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveHaGroup", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class SetPveHaGroupCmdlet : PveCmdletBase
{
/// <summary>Group name to update.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
HelpMessage = "HA group name.")]
public string Group { get; set; } = string.Empty;
/// <summary>Updated node list.</summary>
[Parameter(Mandatory = false, HelpMessage = "Node list: 'node1:pri,node2:pri'.")]
public string? Nodes { get; set; }
/// <summary>Restrict resources to this group's nodes.</summary>
[Parameter(Mandatory = false, HelpMessage = "Restrict resources to this group's nodes (0 or 1).")]
[ValidateRange(0, 1)]
public int? Restricted { get; set; }
/// <summary>Disable failback.</summary>
[Parameter(Mandatory = false, HelpMessage = "Disable automatic failback (0 or 1).")]
[ValidateRange(0, 1)]
public int? NoFailback { get; set; }
/// <summary>Description/comment.</summary>
[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<string, string>();
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);
}
}
}
@@ -0,0 +1,65 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Updates an HA managed resource.</para>
/// <para type="description">
/// Modifies the configuration of an existing HA managed resource,
/// such as changing its state, group, or restart limits.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveHaResource", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class SetPveHaResourceCmdlet : PveCmdletBase
{
/// <summary>Service ID of the resource to update.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
HelpMessage = "Service ID (e.g. 'vm:100', 'ct:200').")]
public string Sid { get; set; } = string.Empty;
/// <summary>Desired state.</summary>
[Parameter(Mandatory = false, HelpMessage = "Desired state: started, stopped, disabled, ignored.")]
[ValidateSet("started", "stopped", "disabled", "ignored")]
public string? State { get; set; }
/// <summary>HA group name.</summary>
[Parameter(Mandatory = false, HelpMessage = "HA group name.")]
public string? Group { get; set; }
/// <summary>Maximum relocations.</summary>
[Parameter(Mandatory = false, HelpMessage = "Maximum relocations before giving up.")]
[ValidateRange(0, 10)]
public int? MaxRelocate { get; set; }
/// <summary>Maximum restart attempts.</summary>
[Parameter(Mandatory = false, HelpMessage = "Maximum restart attempts.")]
[ValidateRange(0, 10)]
public int? MaxRestart { get; set; }
/// <summary>Description/comment.</summary>
[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<string, string>();
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);
}
}
}
@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.HA
{
/// <summary>
/// <para type="synopsis">Updates an HA rule.</para>
/// <para type="description">
/// Modifies an existing HA rule. Requires Proxmox VE 9.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveHaRule", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class SetPveHaRuleCmdlet : PveCmdletBase
{
/// <summary>Rule ID to update.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true,
HelpMessage = "HA rule ID.")]
public string Rule { get; set; } = string.Empty;
/// <summary>Rule state.</summary>
[Parameter(Mandatory = false, HelpMessage = "Rule state: enabled or disabled.")]
[ValidateSet("enabled", "disabled")]
public string? State { get; set; }
/// <summary>Description/comment.</summary>
[Parameter(Mandatory = false, HelpMessage = "Description or comment.")]
public string? Comment { get; set; }
/// <summary>Additional rule properties.</summary>
[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<string, string>();
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);
}
}
}
+33
View File
@@ -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',