using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; using PSProxmoxVE.Core.Authentication; using PSProxmoxVE.Core.Client; using PSProxmoxVE.Core.Models.Cluster; namespace PSProxmoxVE.Core.Services { /// /// Service for Proxmox VE cluster configuration API operations /// (/cluster/config, /cluster/options, /cluster/nextid). /// public class ClusterConfigService { private readonly IPveHttpClient? _injectedClient; /// /// Initializes a new instance of the class. /// public ClusterConfigService() { } /// /// Initializes a new instance of the class with an injected HTTP client. /// /// The HTTP client to use for API calls. The caller owns its lifetime. public ClusterConfigService(IPveHttpClient client) { _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); } /// /// Returns the cluster configuration directory (GET /cluster/config). /// The response is a mixed structure returned as a raw JObject. /// public JObject GetClusterConfig(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/config").GetAwaiter().GetResult(); return JObject.Parse(response); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Creates a new cluster (POST /cluster/config). /// /// The authenticated PVE session. /// The name for the new cluster. /// Optional Corosync link addresses (e.g., "0=10.0.0.1,1=10.0.1.1"). /// Optional node ID for this node. /// Optional number of quorum votes for this node. /// The UPID of the cluster creation task. public string CreateCluster(PveSession session, string clusterName, Dictionary? links = null, int? nodeid = null, int? votes = null) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrEmpty(clusterName)) throw new ArgumentNullException(nameof(clusterName)); var data = new Dictionary { ["clustername"] = clusterName }; if (links != null) { foreach (var kvp in links) data[kvp.Key] = kvp.Value; } if (nodeid.HasValue) data["nodeid"] = nodeid.Value.ToString(); if (votes.HasValue) data["votes"] = votes.Value.ToString(); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync("cluster/config", data).GetAwaiter().GetResult(); var result = JObject.Parse(response)["data"]; return result?.ToString() ?? string.Empty; } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Returns the list of nodes in the cluster configuration (GET /cluster/config/nodes). /// public PveClusterConfigNode[] GetConfigNodes(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/config/nodes").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Adds a node to the cluster configuration (POST /cluster/config/nodes/{node}). /// /// The authenticated PVE session. /// The node name to add. /// The IP address of the new node. /// Optional Corosync link addresses. /// Optional node ID for the new node. /// Optional number of quorum votes. /// Optional flag to force the operation. /// Optional API version override. /// The UPID of the add-node task. public string AddConfigNode(PveSession session, string node, string? newNodeIp = null, Dictionary? links = null, int? nodeid = null, int? votes = null, bool? force = null, int? apiversion = null) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrEmpty(node)) throw new ArgumentNullException(nameof(node)); var data = new Dictionary(); if (!string.IsNullOrEmpty(newNodeIp)) data["new_node_ip"] = newNodeIp!; if (links != null) { foreach (var kvp in links) data[kvp.Key] = kvp.Value; } if (nodeid.HasValue) data["nodeid"] = nodeid.Value.ToString(); if (votes.HasValue) data["votes"] = votes.Value.ToString(); if (force.HasValue) data["force"] = force.Value ? "1" : "0"; if (apiversion.HasValue) data["apiversion"] = apiversion.Value.ToString(); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync($"cluster/config/nodes/{Uri.EscapeDataString(node)}", data).GetAwaiter().GetResult(); var result = JObject.Parse(response)["data"]; return result?.ToString() ?? string.Empty; } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Removes a node from the cluster configuration (DELETE /cluster/config/nodes/{node}). /// /// The authenticated PVE session. /// The node name to remove. public void RemoveConfigNode(PveSession session, string node) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrEmpty(node)) throw new ArgumentNullException(nameof(node)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.DeleteAsync($"cluster/config/nodes/{Uri.EscapeDataString(node)}").GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Returns the cluster join information (GET /cluster/config/join). /// /// The authenticated PVE session. /// Optional node name to get join info for a specific node. public PveClusterJoinInfo GetJoinInfo(PveSession session, string? node = null) { if (session == null) throw new ArgumentNullException(nameof(session)); var resource = "cluster/config/join"; if (!string.IsNullOrEmpty(node)) resource += $"?node={Uri.EscapeDataString(node!)}"; IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveClusterJoinInfo(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Joins the current node to an existing cluster (POST /cluster/config/join). /// /// The authenticated PVE session. /// The hostname or IP of an existing cluster node. /// The TLS certificate fingerprint of the cluster node. /// The root password for the cluster node (plain string; cmdlet layer handles SecureString conversion per D002). /// Optional Corosync link addresses. /// Optional node ID for this node. /// Optional number of quorum votes. /// Optional flag to force the join. /// The UPID of the join task. public string JoinCluster(PveSession session, string hostname, string fingerprint, string password, Dictionary? links = null, int? nodeid = null, int? votes = null, bool? force = null) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrEmpty(hostname)) throw new ArgumentNullException(nameof(hostname)); if (string.IsNullOrEmpty(fingerprint)) throw new ArgumentNullException(nameof(fingerprint)); if (string.IsNullOrEmpty(password)) throw new ArgumentNullException(nameof(password)); var data = new Dictionary { ["hostname"] = hostname, ["fingerprint"] = fingerprint, ["password"] = password }; if (links != null) { foreach (var kvp in links) data[kvp.Key] = kvp.Value; } if (nodeid.HasValue) data["nodeid"] = nodeid.Value.ToString(); if (votes.HasValue) data["votes"] = votes.Value.ToString(); if (force.HasValue) data["force"] = force.Value ? "1" : "0"; IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync("cluster/config/join", data).GetAwaiter().GetResult(); var result = JObject.Parse(response)["data"]; return result?.ToString() ?? string.Empty; } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Returns the Corosync totem configuration (GET /cluster/config/totem). /// public JObject GetTotem(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/config/totem").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data as JObject ?? new JObject(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Returns the external quorum device (qdevice) status (GET /cluster/config/qdevice). /// public JObject GetQdevice(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/config/qdevice").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data as JObject ?? new JObject(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Returns the cluster API version (GET /cluster/config/apiversion). /// public int GetApiVersion(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/config/apiversion").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? 0; } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Returns the cluster-wide options (GET /cluster/options). /// public PveClusterOptions GetClusterOptions(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/options").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveClusterOptions(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Sets cluster-wide options (PUT /cluster/options). /// /// The authenticated PVE session. /// A dictionary of option names and values to set. public void SetClusterOptions(PveSession session, Dictionary options) { if (session == null) throw new ArgumentNullException(nameof(session)); if (options == null) throw new ArgumentNullException(nameof(options)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PutAsync("cluster/options", options).GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Returns the current cluster status (GET /cluster/status). /// Delegates to the same endpoint as . /// public PveClusterStatus[] GetClusterStatus(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/status").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Returns the next available VM/CT ID (GET /cluster/nextid). /// /// The authenticated PVE session. /// Optional specific VMID to check availability for. /// The next available VMID as an integer. public int GetNextId(PveSession session, int? vmid = null) { if (session == null) throw new ArgumentNullException(nameof(session)); var resource = "cluster/nextid"; if (vmid.HasValue) resource += $"?vmid={vmid.Value}"; IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; // The API returns the ID as a string, parse it to int if (data != null && int.TryParse(data.ToString(), out var id)) return id; return 0; } finally { if (_injectedClient == null) client.Dispose(); } } } }