using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; using PSProxmoxVE.Core.Authentication; using PSProxmoxVE.Core.Client; using PSProxmoxVE.Core.Models.Containers; using PSProxmoxVE.Core.Models.Vms; namespace PSProxmoxVE.Core.Services { /// /// Service for Proxmox VE Linux Container (LXC) API operations. /// public class ContainerService { private readonly IPveHttpClient? _injectedClient; private readonly NodeService _nodeService = new NodeService(); /// Initializes a new instance that creates its own HTTP clients. public ContainerService() { } /// Initializes a new instance that uses the supplied HTTP client for all requests. /// The HTTP client to use. The caller owns its lifetime. public ContainerService(IPveHttpClient client) { _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); } // ------------------------------------------------------------------------- // Read operations // ------------------------------------------------------------------------- /// /// Returns containers. If is null, queries every cluster node. /// public PveContainer[] GetContainers(PveSession session, string? node = null) { if (session == null) throw new ArgumentNullException(nameof(session)); if (node != null) return GetContainersOnNode(session, node); var nodes = _nodeService.GetNodes(session); var all = new List(); foreach (var n in nodes) { try { var containers = GetContainersOnNode(session, n.Name); foreach (var ct in containers) ct.Node ??= n.Name; all.AddRange(containers); } catch (Exception ex) when (ex is PSProxmoxVE.Core.Exceptions.PveApiException or System.Net.Http.HttpRequestException) { // Skip offline/inaccessible nodes } } return all.ToArray(); } private PveContainer[] GetContainersOnNode(PveSession session, string node) { IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Returns a single container by its ID on the specified node. /// public PveContainer GetContainer(PveSession session, string node, int vmid) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); var containers = GetContainersOnNode(session, node); var ct = containers.FirstOrDefault(c => c.VmId == vmid); if (ct == null) throw new InvalidOperationException($"Container {vmid} not found on node '{node}'."); ct.Node ??= node; return ct; } /// /// Returns the full configuration of a container. /// public PveContainerConfig GetContainerConfig(PveSession session, string node, int vmid) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/config") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveContainerConfig(); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // Configuration mutation // ------------------------------------------------------------------------- /// /// Updates one or more container configuration settings. /// public void SetContainerConfig( PveSession session, string node, int vmid, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/config", formData) .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // Snapshots // ------------------------------------------------------------------------- /// /// Returns all snapshots for a container. /// public PveSnapshot[] GetContainerSnapshots(PveSession session, string node, int vmid) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Creates a snapshot of a container. Returns the task UPID. /// public PveTask CreateContainerSnapshot( PveSession session, string node, int vmid, string snapname, string? description = null) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname)); var formData = new Dictionary { ["snapname"] = snapname }; if (!string.IsNullOrEmpty(description)) formData["description"] = description!; IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Removes a snapshot from a container. Returns the task UPID. /// public PveTask RemoveContainerSnapshot( PveSession session, string node, int vmid, string snapname) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}") .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Rolls a container back to a snapshot. Returns the task UPID. /// public PveTask RollbackContainerSnapshot( PveSession session, string node, int vmid, string snapname) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}/rollback") .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // Lifecycle // ------------------------------------------------------------------------- /// /// Creates a new container. Returns the task UPID. /// public PveTask CreateContainer( PveSession session, string node, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } /// Starts a container. Returns the task UPID. public PveTask StartContainer(PveSession session, string node, int vmid) => PostStatus(session, node, vmid, "start"); /// Stops a container (hard stop). Returns the task UPID. public PveTask StopContainer(PveSession session, string node, int vmid) => PostStatus(session, node, vmid, "stop"); /// Gracefully shuts down a container. Returns the task UPID. public PveTask ShutdownContainer( PveSession session, string node, int vmid, int? timeoutSeconds = null) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); var formData = new Dictionary(); if (timeoutSeconds.HasValue) formData["timeout"] = timeoutSeconds.Value.ToString(); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/status/shutdown", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } /// Removes a container. Returns the task UPID. public PveTask RemoveContainer( PveSession session, string node, int vmid, bool purge = false, bool force = false) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); var queryParams = new List(); queryParams.Add(purge ? "purge=1" : "purge=0"); if (force) queryParams.Add("force=1"); var queryString = "?" + string.Join("&", queryParams); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}{queryString}") .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } /// Clones a container. Returns the task UPID. public PveTask CloneContainer( PveSession session, string node, int vmid, int newid, string? hostname = null, string? targetNode = null, bool full = true) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); var formData = new Dictionary { ["newid"] = newid.ToString(), ["full"] = full ? "1" : "0" }; if (!string.IsNullOrEmpty(hostname)) formData["hostname"] = hostname!; if (!string.IsNullOrEmpty(targetNode)) formData["target"] = targetNode!; IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/clone", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } /// Migrates a container to another node. Returns the task UPID. public PveTask MigrateContainer( PveSession session, string node, int vmid, string targetNode, bool online = false) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(targetNode)) throw new ArgumentNullException(nameof(targetNode)); var formData = new Dictionary { ["target"] = targetNode, ["online"] = online ? "1" : "0" }; IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/migrate", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // Suspend / Resume // ------------------------------------------------------------------------- /// Suspends a container. Returns the task UPID. public PveTask SuspendContainer(PveSession session, string node, int vmid) => PostStatus(session, node, vmid, "suspend"); /// Resumes a suspended container. Returns the task UPID. public PveTask ResumeContainer(PveSession session, string node, int vmid) => PostStatus(session, node, vmid, "resume"); // ------------------------------------------------------------------------- // Disk / volume operations // ------------------------------------------------------------------------- /// /// Resizes a container disk/volume. Returns the task UPID. /// public PveTask ResizeContainerDisk(PveSession session, string node, int vmid, string disk, string size) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(disk)) throw new ArgumentNullException(nameof(disk)); if (string.IsNullOrWhiteSpace(size)) throw new ArgumentNullException(nameof(size)); var formData = new Dictionary { ["disk"] = disk, ["size"] = size }; IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/resize", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Moves a container volume to a different storage. Returns the task UPID. /// public PveTask MoveVolume(PveSession session, string node, int vmid, string volume, string storage, bool delete = true) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(volume)) throw new ArgumentNullException(nameof(volume)); if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage)); var formData = new Dictionary { ["volume"] = volume, ["storage"] = storage, ["delete"] = delete ? "1" : "0" }; IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/move_volume", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // Template // ------------------------------------------------------------------------- /// /// Converts a container to a template. This is irreversible. Returns the task UPID. /// public PveTask ConvertToTemplate(PveSession session, string node, int vmid) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/template") .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // Interfaces // ------------------------------------------------------------------------- /// /// Returns network interface information for a container. /// public PveContainerInterface[] GetInterfaces(PveSession session, string node, int vmid) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/interfaces") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- private PveTask PostStatus(PveSession session, string node, int vmid, string action) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/status/{action}") .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } private static PveTask ParseTask(string response, string node) { var data = JObject.Parse(response)["data"]; if (data?.Type == JTokenType.String) return new PveTask { Upid = data.ToString(), Node = node }; var task = data?.ToObject() ?? new PveTask(); task.Node = node; return task; } } }