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