From da2037d0c55a6e86678e8bb749b42d207dddda15 Mon Sep 17 00:00:00 2001 From: Clint Branham Date: Tue, 24 Mar 2026 18:28:11 -0500 Subject: [PATCH] feat: add cluster config and HA management cmdlets (F060, F053) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Models/Cluster/PveClusterConfigNode.cs | 46 ++ .../Models/Cluster/PveClusterJoinInfo.cs | 43 ++ .../Models/Cluster/PveClusterOptions.cs | 129 +++++ src/PSProxmoxVE.Core/Models/HA/PveHaGroup.cs | 59 +++ .../Models/HA/PveHaResource.cs | 63 +++ src/PSProxmoxVE.Core/Models/HA/PveHaRule.cs | 53 ++ src/PSProxmoxVE.Core/Models/HA/PveHaStatus.cs | 58 +++ .../Services/ClusterConfigService.cs | 409 +++++++++++++++ src/PSProxmoxVE.Core/Services/HaService.cs | 492 ++++++++++++++++++ .../Cluster/AddPveClusterConfigNodeCmdlet.cs | 76 +++ .../Cluster/AddPveClusterMemberCmdlet.cs | 91 ++++ .../Cluster/GetPveClusterConfigCmdlet.cs | 28 + .../Cluster/GetPveClusterConfigNodeCmdlet.cs | 30 ++ .../Cluster/GetPveClusterJoinInfoCmdlet.cs | 33 ++ .../Cluster/GetPveClusterNextIdCmdlet.cs | 32 ++ .../Cluster/GetPveClusterOptionCmdlet.cs | 28 + .../Cluster/GetPveClusterStatusCmdlet.cs | 30 ++ .../Cmdlets/Cluster/NewPveClusterCmdlet.cs | 64 +++ .../RemovePveClusterConfigNodeCmdlet.cs | 35 ++ .../Cluster/SetPveClusterOptionCmdlet.cs | 93 ++++ .../Cmdlets/HA/GetPveHaGroupCmdlet.cs | 43 ++ .../Cmdlets/HA/GetPveHaResourceCmdlet.cs | 44 ++ .../Cmdlets/HA/GetPveHaRuleCmdlet.cs | 45 ++ .../Cmdlets/HA/GetPveHaStatusCmdlet.cs | 30 ++ .../Cmdlets/HA/MovePveHaResourceCmdlet.cs | 48 ++ .../Cmdlets/HA/NewPveHaGroupCmdlet.cs | 55 ++ .../Cmdlets/HA/NewPveHaResourceCmdlet.cs | 64 +++ .../Cmdlets/HA/NewPveHaRuleCmdlet.cs | 57 ++ .../Cmdlets/HA/RemovePveHaGroupCmdlet.cs | 35 ++ .../Cmdlets/HA/RemovePveHaResourceCmdlet.cs | 35 ++ .../Cmdlets/HA/RemovePveHaRuleCmdlet.cs | 36 ++ .../Cmdlets/HA/SetPveHaGroupCmdlet.cs | 58 +++ .../Cmdlets/HA/SetPveHaResourceCmdlet.cs | 65 +++ .../Cmdlets/HA/SetPveHaRuleCmdlet.cs | 58 +++ src/PSProxmoxVE/PSProxmoxVE.psd1 | 33 ++ 35 files changed, 2598 insertions(+) create mode 100644 src/PSProxmoxVE.Core/Models/Cluster/PveClusterConfigNode.cs create mode 100644 src/PSProxmoxVE.Core/Models/Cluster/PveClusterJoinInfo.cs create mode 100644 src/PSProxmoxVE.Core/Models/Cluster/PveClusterOptions.cs create mode 100644 src/PSProxmoxVE.Core/Models/HA/PveHaGroup.cs create mode 100644 src/PSProxmoxVE.Core/Models/HA/PveHaResource.cs create mode 100644 src/PSProxmoxVE.Core/Models/HA/PveHaRule.cs create mode 100644 src/PSProxmoxVE.Core/Models/HA/PveHaStatus.cs create mode 100644 src/PSProxmoxVE.Core/Services/ClusterConfigService.cs create mode 100644 src/PSProxmoxVE.Core/Services/HaService.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterConfigNodeCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/AddPveClusterMemberCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterConfigNodeCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterJoinInfoCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterNextIdCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterOptionCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/GetPveClusterStatusCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/NewPveClusterCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/RemovePveClusterConfigNodeCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/Cluster/SetPveClusterOptionCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/GetPveHaGroupCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/GetPveHaResourceCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/GetPveHaRuleCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/GetPveHaStatusCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/MovePveHaResourceCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/NewPveHaGroupCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/NewPveHaResourceCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/NewPveHaRuleCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/RemovePveHaGroupCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/RemovePveHaResourceCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/RemovePveHaRuleCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/SetPveHaGroupCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/SetPveHaResourceCmdlet.cs create mode 100644 src/PSProxmoxVE/Cmdlets/HA/SetPveHaRuleCmdlet.cs 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',