diff --git a/src/PSProxmoxVE.Core/Client/PveHandlerCache.cs b/src/PSProxmoxVE.Core/Client/PveHandlerCache.cs new file mode 100644 index 0000000..8d1cfa6 --- /dev/null +++ b/src/PSProxmoxVE.Core/Client/PveHandlerCache.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace PSProxmoxVE.Core.Client +{ + /// + /// Process-wide cache of instances, one per + /// (host, port, skipCertificateCheck). The handler owns the connection pool, so sharing + /// it lets every for the same endpoint reuse established TLS + /// connections instead of handshaking per request. Cached handlers are never disposed + /// and are immutable once published: never set a property on a handler this returns. + /// + internal sealed class PveHandlerCache + { + /// The cache production clients share. + internal static readonly PveHandlerCache Shared = new PveHandlerCache(CreateHandler); + + private readonly object _gate = new object(); + private readonly Dictionary _handlers = + new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Func _factory; + + /// + /// Test seam: a cache whose handlers come from , which + /// receives the skipCertificateCheck flag for the key being populated. + /// + internal PveHandlerCache(Func factory) + { + _factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + /// Number of handlers built so far. + internal int Count + { + get { lock (_gate) return _handlers.Count; } + } + + /// Returns the handler for the endpoint, building it on first use. + internal HttpClientHandler Get(string host, int port, bool skipCertificateCheck) + { + if (string.IsNullOrWhiteSpace(host)) + throw new ArgumentException("Host cannot be null or empty.", nameof(host)); + + var key = host + ":" + port + ":" + (skipCertificateCheck ? "insecure" : "verify"); + lock (_gate) + { + if (!_handlers.TryGetValue(key, out var handler)) + { + handler = _factory(skipCertificateCheck); + _handlers[key] = handler; + } + return handler; + } + } + + private static HttpClientHandler CreateHandler(bool skipCertificateCheck) + { + var handler = new HttpClientHandler { UseCookies = false }; + if (skipCertificateCheck) + { + handler.ServerCertificateCustomValidationCallback = + (HttpRequestMessage _, X509Certificate2 _, X509Chain _, SslPolicyErrors _) => true; + } + return handler; + } + } +} diff --git a/src/PSProxmoxVE.Core/Client/PveHttpClient.cs b/src/PSProxmoxVE.Core/Client/PveHttpClient.cs index 2374b50..ffc530f 100644 --- a/src/PSProxmoxVE.Core/Client/PveHttpClient.cs +++ b/src/PSProxmoxVE.Core/Client/PveHttpClient.cs @@ -12,9 +12,6 @@ using PSProxmoxVE.Core.Authentication; using PSProxmoxVE.Core.Exceptions; using PSProxmoxVE.Core.Utilities; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; - namespace PSProxmoxVE.Core.Client { /// @@ -28,6 +25,7 @@ namespace PSProxmoxVE.Core.Client #pragma warning restore CS8625 private readonly string _baseUrl; private readonly HttpClient _httpClient; + private readonly HttpMessageHandler _handler; private bool _disposed; private readonly TimeSpan _guestLockRetryWindow; @@ -47,13 +45,26 @@ namespace PSProxmoxVE.Core.Client /// to disable the timeout entirely (useful for multi-GB uploads/downloads). /// public PveHttpClient(PveSession session, TimeSpan? timeoutOverride = null) - : this(session, timeoutOverride, guestLockRetryWindow: null, handler: null, guestLockRetryDelay: null) + : this(session, timeoutOverride, guestLockRetryWindow: null, handler: null, guestLockRetryDelay: null, handlerCache: null) + { + } + + /// + /// Test seam: same as the public constructor but drawing the transport handler from + /// instead of . + /// + /// The authenticated PVE session providing credentials and base URL. + /// The cache to take the handler from. + internal PveHttpClient(PveSession session, PveHandlerCache handlerCache) + : this(session, timeoutOverride: null, guestLockRetryWindow: null, handler: null, guestLockRetryDelay: null, + handlerCache: handlerCache ?? throw new ArgumentNullException(nameof(handlerCache))) { } /// /// Test seam: builds a client against an explicit handler, lock-retry window and/or /// inter-attempt delay. Production code always goes through the public constructor. + /// An explicit handler is owned by this client and bypasses the shared handler cache. /// /// The authenticated PVE session providing credentials and base URL. /// Optional per-instance timeout override. @@ -62,42 +73,42 @@ namespace PSProxmoxVE.Core.Client /// Null uses , the same as the public constructor. /// /// - /// Message handler to send requests through. Null builds the production - /// certificate-validation handler from . + /// Message handler to send requests through, owned and disposed by this client. Null + /// takes the shared pooled handler for the session's endpoint from the handler cache. /// /// /// Invoked before each guest-lock reissue instead of sleeping. Null uses /// , the same as the public constructor. /// + /// + /// Cache to take the handler from when is null. Null uses + /// , the same as the public constructor. + /// internal PveHttpClient( PveSession session, TimeSpan? timeoutOverride, TimeSpan? guestLockRetryWindow, HttpMessageHandler? handler, - Func? guestLockRetryDelay = null) + Func? guestLockRetryDelay = null, + PveHandlerCache? handlerCache = null) { _session = session ?? throw new ArgumentNullException(nameof(session)); _baseUrl = session.BaseUrl; _guestLockRetryWindow = guestLockRetryWindow ?? GuestLockRetry.DefaultWindow; _guestLockRetryDelay = guestLockRetryDelay ?? Task.Delay; - _httpClient = new HttpClient(handler ?? CreateHandler(session.SkipCertificateCheck)); + var ownsHandler = handler != null; + _handler = handler ?? (handlerCache ?? PveHandlerCache.Shared) + .Get(session.Hostname, session.Port, session.SkipCertificateCheck); + _httpClient = new HttpClient(_handler, disposeHandler: ownsHandler); _httpClient.Timeout = timeoutOverride ?? session.Timeout; _httpClient.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json")); } - private static HttpClientHandler CreateHandler(bool skipCertificateCheck) - { - var handler = new HttpClientHandler(); - if (skipCertificateCheck) - { - handler.ServerCertificateCustomValidationCallback = - (HttpRequestMessage _, X509Certificate2 _, X509Chain _, SslPolicyErrors _) => true; - } - return handler; - } + /// The transport handler requests go through. + internal HttpMessageHandler Handler => _handler; /// /// Creates a bare HTTP client for pre-session use (e.g. initial authentication). @@ -113,7 +124,8 @@ namespace PSProxmoxVE.Core.Client _guestLockRetryWindow = GuestLockRetry.DefaultWindow; _guestLockRetryDelay = Task.Delay; - _httpClient = new HttpClient(CreateHandler(skipCertificateCheck)); + _handler = PveHandlerCache.Shared.Get(hostname, port, skipCertificateCheck); + _httpClient = new HttpClient(_handler, disposeHandler: false); if (timeout.HasValue) _httpClient.Timeout = timeout.Value; @@ -506,7 +518,10 @@ namespace PSProxmoxVE.Core.Client return sb.ToString(); } - /// + /// + /// Releases this client. A handler taken from the shared cache stays alive for the + /// other clients on the same endpoint; only an explicitly supplied handler is disposed. + /// public void Dispose() { if (!_disposed) diff --git a/src/PSProxmoxVE.Core/Services/BackupService.cs b/src/PSProxmoxVE.Core/Services/BackupService.cs index c7033a5..4c5ba9b 100644 --- a/src/PSProxmoxVE.Core/Services/BackupService.cs +++ b/src/PSProxmoxVE.Core/Services/BackupService.cs @@ -12,10 +12,8 @@ namespace PSProxmoxVE.Core.Services /// /// Service for Proxmox VE Backup (vzdump) and backup job API operations. /// - public class BackupService + public class BackupService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - /// /// Initializes a new instance of with no injected client. /// Each method will create and dispose its own . @@ -27,10 +25,7 @@ namespace PSProxmoxVE.Core.Services /// The caller owns the client's lifetime; this service will not dispose it. /// /// The HTTP client to use for all requests. - public BackupService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public BackupService(IPveHttpClient client) : base(client) { } // ------------------------------------------------------------------------- // Ad-hoc backup (vzdump) @@ -48,17 +43,12 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/vzdump", config) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -72,17 +62,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/backup").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -93,18 +78,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"cluster/backup/{Uri.EscapeDataString(id)}") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -115,15 +95,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("cluster/backup", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -135,16 +110,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"cluster/backup/{Uri.EscapeDataString(id)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -155,16 +125,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/backup/{Uri.EscapeDataString(id)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -178,18 +143,13 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/backup-info/not-backed-up") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return JsonHelper.ToListOfDictionaries(data as JArray); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- diff --git a/src/PSProxmoxVE.Core/Services/CloudInitService.cs b/src/PSProxmoxVE.Core/Services/CloudInitService.cs index 443b5df..9252b7a 100644 --- a/src/PSProxmoxVE.Core/Services/CloudInitService.cs +++ b/src/PSProxmoxVE.Core/Services/CloudInitService.cs @@ -12,10 +12,8 @@ namespace PSProxmoxVE.Core.Services /// Service for managing Cloud-Init configuration on Proxmox VE QEMU/KVM VMs. /// All operations target the /nodes/{node}/qemu/{vmid}/config endpoint. /// - public class CloudInitService + public class CloudInitService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - // Cloud-Init field names as used in the PVE API private static readonly string[] CloudInitFields = { @@ -33,10 +31,7 @@ namespace PSProxmoxVE.Core.Services /// 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 CloudInitService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public CloudInitService(IPveHttpClient client) : base(client) { } /// /// Retrieves the Cloud-Init specific configuration fields for a VM. @@ -47,8 +42,7 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config") .GetAwaiter().GetResult(); @@ -64,11 +58,7 @@ namespace PSProxmoxVE.Core.Services } return ciObj.ToObject() ?? new PveCloudInitConfig(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -89,19 +79,14 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config", formData) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -114,18 +99,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/cloudinit", null) .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToString() ?? string.Empty; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } } } diff --git a/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs b/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs index 095b9f2..560a9ae 100644 --- a/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs +++ b/src/PSProxmoxVE.Core/Services/ClusterConfigService.cs @@ -14,10 +14,8 @@ namespace PSProxmoxVE.Core.Services /// Service for Proxmox VE cluster configuration API operations /// (/cluster/config, /cluster/options, /cluster/nextid). /// - public class ClusterConfigService + public class ClusterConfigService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - private static readonly TimeSpan DefaultQuorumTimeout = TimeSpan.FromSeconds(60); private static readonly TimeSpan QuorumPollInterval = TimeSpan.FromSeconds(2); @@ -30,10 +28,7 @@ namespace PSProxmoxVE.Core.Services /// 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)); - } + public ClusterConfigService(IPveHttpClient client) : base(client) { } /// /// Returns the cluster configuration directory (GET /cluster/config). @@ -43,17 +38,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/config").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data is JObject obj ? JsonHelper.ToDictionary(obj) : new Dictionary(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -84,17 +74,12 @@ namespace PSProxmoxVE.Core.Services if (votes.HasValue) data["votes"] = votes.Value.ToString(); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -104,17 +89,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -151,17 +131,12 @@ namespace PSProxmoxVE.Core.Services if (apiversion.HasValue) data["apiversion"] = apiversion.Value.ToString(); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -174,15 +149,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrEmpty(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/config/nodes/{Uri.EscapeDataString(node)}").GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -198,17 +168,12 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(node)) resource += $"?node={Uri.EscapeDataString(node!)}"; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveClusterJoinInfo(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -248,17 +213,12 @@ namespace PSProxmoxVE.Core.Services if (force.HasValue) data["force"] = force.Value ? "1" : "0"; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -268,17 +228,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/config/totem").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return JsonHelper.ToDictionary(data as JObject); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -288,17 +243,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/config/qdevice").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return JsonHelper.ToDictionary(data as JObject); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -308,17 +258,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -328,17 +273,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -351,15 +291,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (options == null) throw new ArgumentNullException(nameof(options)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync("cluster/options", options).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -370,17 +305,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -439,8 +369,7 @@ namespace PSProxmoxVE.Core.Services if (vmid.HasValue) resource += $"?vmid={vmid.Value}"; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; @@ -449,11 +378,7 @@ namespace PSProxmoxVE.Core.Services if (int.TryParse(data.ToString(), out var id)) return id; throw new InvalidOperationException($"API returned unexpected next VMID value: '{data}'"); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } } } diff --git a/src/PSProxmoxVE.Core/Services/ClusterService.cs b/src/PSProxmoxVE.Core/Services/ClusterService.cs index 0dcd632..fce46b5 100644 --- a/src/PSProxmoxVE.Core/Services/ClusterService.cs +++ b/src/PSProxmoxVE.Core/Services/ClusterService.cs @@ -9,10 +9,8 @@ namespace PSProxmoxVE.Core.Services /// /// Service for Proxmox VE cluster-level API operations. /// - public class ClusterService + public class ClusterService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - /// /// Initializes a new instance of the class. /// @@ -22,10 +20,7 @@ namespace PSProxmoxVE.Core.Services /// 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 ClusterService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public ClusterService(IPveHttpClient client) : base(client) { } /// /// Returns the current cluster status. The response is a mixed array of @@ -35,17 +30,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -57,8 +47,7 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var resource = "cluster/resources"; if (!string.IsNullOrEmpty(type)) @@ -67,11 +56,7 @@ namespace PSProxmoxVE.Core.Services var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } } } diff --git a/src/PSProxmoxVE.Core/Services/ContainerService.cs b/src/PSProxmoxVE.Core/Services/ContainerService.cs index 67a5be3..dd43769 100644 --- a/src/PSProxmoxVE.Core/Services/ContainerService.cs +++ b/src/PSProxmoxVE.Core/Services/ContainerService.cs @@ -12,19 +12,21 @@ namespace PSProxmoxVE.Core.Services /// /// Service for Proxmox VE Linux Container (LXC) API operations. /// - public class ContainerService + public class ContainerService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - private readonly NodeService _nodeService = new NodeService(); + private readonly NodeService _nodeService; /// Initializes a new instance that creates its own HTTP clients. - public ContainerService() { } + public ContainerService() + { + _nodeService = new NodeService(); + } /// 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) + public ContainerService(IPveHttpClient client) : base(client) { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); + _nodeService = new NodeService(client); } // ------------------------------------------------------------------------- @@ -85,17 +87,12 @@ namespace PSProxmoxVE.Core.Services private PveContainer[] GetContainersOnNode(PveSession session, string node) { - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -122,18 +119,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } // ------------------------------------------------------------------------- @@ -153,19 +145,14 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { 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(); - } + }); } // ------------------------------------------------------------------------- @@ -180,18 +167,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -215,17 +197,12 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(description)) formData["description"] = description!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -241,17 +218,12 @@ namespace PSProxmoxVE.Core.Services 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 + return Invoke(session, client => { 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(); - } + }); } /// @@ -267,17 +239,12 @@ namespace PSProxmoxVE.Core.Services 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 + return Invoke(session, client => { 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(); - } + }); } // ------------------------------------------------------------------------- @@ -296,8 +263,7 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var formData = config.ToDictionary( kvp => kvp.Key, @@ -305,11 +271,7 @@ namespace PSProxmoxVE.Core.Services 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. @@ -334,17 +296,12 @@ namespace PSProxmoxVE.Core.Services if (timeoutSeconds.HasValue) formData["timeout"] = timeoutSeconds.Value.ToString(); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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. @@ -364,17 +321,12 @@ namespace PSProxmoxVE.Core.Services queryParams.Add("force=1"); var queryString = "?" + string.Join("&", queryParams); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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. @@ -400,17 +352,12 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(targetNode)) formData["target"] = targetNode!; if (!string.IsNullOrEmpty(storage)) formData["storage"] = storage!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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. @@ -431,17 +378,12 @@ namespace PSProxmoxVE.Core.Services ["online"] = online ? "1" : "0" }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/migrate", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -476,17 +418,12 @@ namespace PSProxmoxVE.Core.Services ["size"] = size }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/resize", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -506,17 +443,12 @@ namespace PSProxmoxVE.Core.Services ["delete"] = delete ? "1" : "0" }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } // ------------------------------------------------------------------------- @@ -531,17 +463,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/template") .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -556,18 +483,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } // ------------------------------------------------------------------------- @@ -579,17 +501,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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) diff --git a/src/PSProxmoxVE.Core/Services/FirewallService.cs b/src/PSProxmoxVE.Core/Services/FirewallService.cs index e458d04..f22593f 100644 --- a/src/PSProxmoxVE.Core/Services/FirewallService.cs +++ b/src/PSProxmoxVE.Core/Services/FirewallService.cs @@ -11,19 +11,14 @@ namespace PSProxmoxVE.Core.Services /// Service for Proxmox VE Firewall API operations. /// Supports firewall management at cluster, node, VM, and container levels. /// - public class FirewallService + public class FirewallService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - /// Initializes a new instance that creates its own HTTP clients. public FirewallService() { } /// Initializes a new instance that uses the supplied HTTP client for all requests. /// The HTTP client to use. The caller owns its lifetime. - public FirewallService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public FirewallService(IPveHttpClient client) : base(client) { } // ------------------------------------------------------------------------- // Rules @@ -37,17 +32,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"{basePath}/rules").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -60,15 +50,10 @@ namespace PSProxmoxVE.Core.Services if (config == null) throw new ArgumentNullException(nameof(config)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync($"{basePath}/rules", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -81,15 +66,10 @@ namespace PSProxmoxVE.Core.Services if (config == null) throw new ArgumentNullException(nameof(config)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"{basePath}/rules/{pos}", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -101,15 +81,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"{basePath}/rules/{pos}").GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -123,17 +98,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/firewall/groups").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -148,15 +118,10 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(comment)) formData["comment"] = comment!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("cluster/firewall/groups", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -167,16 +132,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/firewall/groups/{Uri.EscapeDataString(name)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -187,18 +147,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -210,16 +165,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -231,16 +181,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}/{pos}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -251,16 +196,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}/{pos}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -275,17 +215,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"{basePath}/aliases").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -307,15 +242,10 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(comment)) formData["comment"] = comment!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync($"{basePath}/aliases", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -334,16 +264,11 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(comment)) formData["comment"] = comment!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"{basePath}/aliases/{Uri.EscapeDataString(name)}", formData) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -356,16 +281,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"{basePath}/aliases/{Uri.EscapeDataString(name)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -380,17 +300,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"{basePath}/ipset").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -407,15 +322,10 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(comment)) formData["comment"] = comment!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync($"{basePath}/ipset", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -428,16 +338,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -454,18 +359,13 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -485,16 +385,11 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(comment)) formData["comment"] = comment!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}", formData) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -514,17 +409,12 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(comment)) formData["comment"] = comment!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync( $"{basePath}/ipset/{Uri.EscapeDataString(name)}/{Uri.EscapeDataString(cidr)}", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -538,17 +428,12 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(cidr)) throw new ArgumentNullException(nameof(cidr)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync( $"{basePath}/ipset/{Uri.EscapeDataString(name)}/{Uri.EscapeDataString(cidr)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -564,17 +449,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"{basePath}/options").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveFirewallOptions(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -587,15 +467,10 @@ namespace PSProxmoxVE.Core.Services if (config == null) throw new ArgumentNullException(nameof(config)); var basePath = BuildBasePath(level, node, vmid); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"{basePath}/options", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -615,17 +490,12 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(type)) resource += $"?type={Uri.EscapeDataString(type!)}"; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- diff --git a/src/PSProxmoxVE.Core/Services/HaService.cs b/src/PSProxmoxVE.Core/Services/HaService.cs index e97bc41..83606d0 100644 --- a/src/PSProxmoxVE.Core/Services/HaService.cs +++ b/src/PSProxmoxVE.Core/Services/HaService.cs @@ -11,10 +11,8 @@ namespace PSProxmoxVE.Core.Services /// /// Service for Proxmox VE High Availability (HA) API operations. /// - public class HaService + public class HaService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - /// /// Initializes a new instance of the class. /// @@ -24,10 +22,7 @@ namespace PSProxmoxVE.Core.Services /// Initializes a new instance of the class with an injected HTTP client. /// /// The HTTP client to use for API calls. The caller owns its lifetime. - public HaService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public HaService(IPveHttpClient client) : base(client) { } // ------------------------------------------------------------------------- // Resources @@ -40,17 +35,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/ha/resources").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -63,18 +53,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(sid)) throw new ArgumentNullException(nameof(sid)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"cluster/ha/resources/{Uri.EscapeDataString(sid)}") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveHaResource(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -91,15 +76,10 @@ namespace PSProxmoxVE.Core.Services var formData = new Dictionary(options) { ["sid"] = sid }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("cluster/ha/resources", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -114,16 +94,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(sid)) throw new ArgumentNullException(nameof(sid)); if (options == null) throw new ArgumentNullException(nameof(options)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"cluster/ha/resources/{Uri.EscapeDataString(sid)}", options) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -136,16 +111,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(sid)) throw new ArgumentNullException(nameof(sid)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/ha/resources/{Uri.EscapeDataString(sid)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -162,19 +132,14 @@ namespace PSProxmoxVE.Core.Services var formData = new Dictionary { ["node"] = node }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } /// @@ -191,19 +156,14 @@ namespace PSProxmoxVE.Core.Services var formData = new Dictionary { ["node"] = node }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { 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(); - } + }); } // ------------------------------------------------------------------------- @@ -217,17 +177,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/ha/groups").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -240,18 +195,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"cluster/ha/groups/{Uri.EscapeDataString(group)}") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveHaGroup(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -274,15 +224,10 @@ namespace PSProxmoxVE.Core.Services ["nodes"] = nodes }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("cluster/ha/groups", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -297,16 +242,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group)); if (options == null) throw new ArgumentNullException(nameof(options)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"cluster/ha/groups/{Uri.EscapeDataString(group)}", options) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -319,16 +259,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/ha/groups/{Uri.EscapeDataString(group)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -342,17 +277,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/ha/status/current").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -362,17 +292,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/ha/status/manager_status").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return JsonHelper.ToDictionary(data as JObject); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -386,17 +311,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/ha/rules").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -409,18 +329,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(rule)) throw new ArgumentNullException(nameof(rule)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"cluster/ha/rules/{Uri.EscapeDataString(rule)}") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveHaRule(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -433,15 +348,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (options == null) throw new ArgumentNullException(nameof(options)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("cluster/ha/rules", options).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -456,16 +366,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(rule)) throw new ArgumentNullException(nameof(rule)); if (options == null) throw new ArgumentNullException(nameof(options)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"cluster/ha/rules/{Uri.EscapeDataString(rule)}", options) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -478,16 +383,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(rule)) throw new ArgumentNullException(nameof(rule)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/ha/rules/{Uri.EscapeDataString(rule)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } } } diff --git a/src/PSProxmoxVE.Core/Services/NetworkService.cs b/src/PSProxmoxVE.Core/Services/NetworkService.cs index 21e22e4..5aded9e 100644 --- a/src/PSProxmoxVE.Core/Services/NetworkService.cs +++ b/src/PSProxmoxVE.Core/Services/NetworkService.cs @@ -13,19 +13,14 @@ namespace PSProxmoxVE.Core.Services /// Service for Proxmox VE node network and SDN (Software-Defined Networking) API operations. /// SDN methods require PVE 8.0 or later. Version checks are performed at the cmdlet layer. /// - public class NetworkService + public class NetworkService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - /// Initializes a new instance that creates its own HTTP clients. public NetworkService() { } /// Initializes a new instance that uses the supplied HTTP client for all requests. /// The HTTP client to use. The caller owns its lifetime. - public NetworkService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public NetworkService(IPveHttpClient client) : base(client) { } // ------------------------------------------------------------------------- // Node network interfaces @@ -48,17 +43,12 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(type)) resource += $"?type={Uri.EscapeDataString(type!)}"; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -77,8 +67,7 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var formData = config.ToDictionary( kvp => kvp.Key, @@ -87,11 +76,7 @@ namespace PSProxmoxVE.Core.Services .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveNetwork(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -113,19 +98,14 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(iface)) throw new ArgumentNullException(nameof(iface)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/network/{Uri.EscapeDataString(iface)}", formData) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -141,15 +121,10 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(iface)) throw new ArgumentNullException(nameof(iface)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/network/{Uri.EscapeDataString(iface)}").GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -162,17 +137,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/network") .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -187,17 +157,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/sdn/zones").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -208,17 +173,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/sdn/vnets").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -233,8 +193,7 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var formData = config.ToDictionary( kvp => kvp.Key, @@ -243,11 +202,7 @@ namespace PSProxmoxVE.Core.Services .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveSdnZone(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -260,15 +215,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(zone)) throw new ArgumentNullException(nameof(zone)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/sdn/zones/{Uri.EscapeDataString(zone)}").GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -283,8 +233,7 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var formData = config.ToDictionary( kvp => kvp.Key, @@ -293,11 +242,7 @@ namespace PSProxmoxVE.Core.Services .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveSdnVnet(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -310,15 +255,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}").GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -335,18 +275,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -364,19 +299,14 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); client.PostAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets", formData) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -391,17 +321,12 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); if (string.IsNullOrWhiteSpace(subnet)) throw new ArgumentNullException(nameof(subnet)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync( $"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets/{Uri.EscapeDataString(subnet)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -415,17 +340,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/sdn/ipams").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -436,15 +356,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("cluster/sdn/ipams", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -455,16 +370,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(ipam)) throw new ArgumentNullException(nameof(ipam)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/sdn/ipams/{Uri.EscapeDataString(ipam)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -478,17 +388,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/sdn/dns").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -499,15 +404,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("cluster/sdn/dns", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -518,16 +418,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(dns)) throw new ArgumentNullException(nameof(dns)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/sdn/dns/{Uri.EscapeDataString(dns)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -541,17 +436,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("cluster/sdn/controllers").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -562,15 +452,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("cluster/sdn/controllers", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -581,16 +466,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException(nameof(controller)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"cluster/sdn/controllers/{Uri.EscapeDataString(controller)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -604,16 +484,11 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync("cluster/sdn", new Dictionary()) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -625,16 +500,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(zone)) throw new ArgumentNullException(nameof(zone)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"cluster/sdn/zones/{Uri.EscapeDataString(zone)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -646,16 +516,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -668,17 +533,12 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(subnet)) throw new ArgumentNullException(nameof(subnet)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync( $"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets/{Uri.EscapeDataString(subnet)}", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -690,16 +550,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException(nameof(controller)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"cluster/sdn/controllers/{Uri.EscapeDataString(controller)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -711,16 +566,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(ipam)) throw new ArgumentNullException(nameof(ipam)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"cluster/sdn/ipams/{Uri.EscapeDataString(ipam)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -732,16 +582,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(dns)) throw new ArgumentNullException(nameof(dns)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"cluster/sdn/dns/{Uri.EscapeDataString(dns)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- diff --git a/src/PSProxmoxVE.Core/Services/NodeService.cs b/src/PSProxmoxVE.Core/Services/NodeService.cs index e9c4436..a8c64e7 100644 --- a/src/PSProxmoxVE.Core/Services/NodeService.cs +++ b/src/PSProxmoxVE.Core/Services/NodeService.cs @@ -13,10 +13,8 @@ namespace PSProxmoxVE.Core.Services /// /// Service for node-level and cluster-version Proxmox VE API operations. /// - public class NodeService + public class NodeService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - /// /// Initializes a new instance of with no injected client. /// Each method will create and dispose its own . @@ -28,10 +26,7 @@ namespace PSProxmoxVE.Core.Services /// The caller owns the client's lifetime; this service will not dispose it. /// /// The HTTP client to use for all requests. - public NodeService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public NodeService(IPveHttpClient client) : base(client) { } /// /// Returns all cluster nodes. @@ -40,17 +35,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("nodes").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -61,17 +51,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/status").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveNodeStatus(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -84,17 +69,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/config").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return JsonHelper.ToDictionary(data as JObject); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -109,15 +89,10 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/config", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -130,17 +105,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/dns").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return JsonHelper.ToDictionary(data as JObject); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -155,15 +125,10 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/dns", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -178,16 +143,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); var formData = config ?? new Dictionary(); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/startall", formData).GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -202,16 +162,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); var formData = config ?? new Dictionary(); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/stopall", formData).GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -221,8 +176,7 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("version").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; @@ -230,11 +184,7 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrEmpty(versionStr)) throw new InvalidOperationException("Failed to retrieve PVE version from API response."); return PveVersion.Parse(versionStr!); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- diff --git a/src/PSProxmoxVE.Core/Services/PoolService.cs b/src/PSProxmoxVE.Core/Services/PoolService.cs index e76d964..c8bbb23 100644 --- a/src/PSProxmoxVE.Core/Services/PoolService.cs +++ b/src/PSProxmoxVE.Core/Services/PoolService.cs @@ -10,10 +10,8 @@ namespace PSProxmoxVE.Core.Services /// /// Service for Proxmox VE resource pool API operations. /// - public class PoolService + public class PoolService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - /// /// Initializes a new instance of with no injected client. /// Each method will create and dispose its own . @@ -25,10 +23,7 @@ namespace PSProxmoxVE.Core.Services /// The caller owns the client's lifetime; this service will not dispose it. /// /// The HTTP client to use for all requests. - public PoolService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public PoolService(IPveHttpClient client) : base(client) { } /// /// Returns all resource pools. @@ -37,17 +32,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("pools").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -58,18 +48,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"pools/{Uri.EscapeDataString(poolId)}") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -80,19 +65,14 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var config = new Dictionary { { "poolid", poolId } }; if (!string.IsNullOrEmpty(comment)) config["comment"] = comment!; client.PostAsync("pools", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -104,16 +84,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"pools/{Uri.EscapeDataString(poolId)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -124,16 +99,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"pools/{Uri.EscapeDataString(poolId)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } } } diff --git a/src/PSProxmoxVE.Core/Services/PveServiceBase.cs b/src/PSProxmoxVE.Core/Services/PveServiceBase.cs new file mode 100644 index 0000000..230fae8 --- /dev/null +++ b/src/PSProxmoxVE.Core/Services/PveServiceBase.cs @@ -0,0 +1,86 @@ +using System; +using PSProxmoxVE.Core.Authentication; +using PSProxmoxVE.Core.Client; + +namespace PSProxmoxVE.Core.Services +{ + /// + /// Common base for the API services: owns the optional injected + /// and the client lifetime around each API operation. A service built without a client + /// opens one per operation and disposes it afterwards; a service built with one uses it + /// for every operation and never disposes it, since the caller owns its lifetime. + /// + public abstract class PveServiceBase + { + private readonly IPveHttpClient? _injectedClient; + + /// Initializes a service that opens its own HTTP client per call. + private protected PveServiceBase() { } + + /// Initializes a service that uses the supplied HTTP client for every call. + /// The HTTP client to use. The caller owns its lifetime. + private protected PveServiceBase(IPveHttpClient client) + { + _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); + } + + /// + /// Runs against the injected client, or against a client + /// opened for this call and disposed when it returns. + /// + /// The result type. + /// The authenticated PVE session. + /// The work to run against the client. + private protected T Invoke(PveSession session, Func action) => + Invoke(session, timeoutOverride: null, action); + + /// + /// Runs against the injected client, or against a client + /// opened for this call and disposed when it returns. + /// + /// The authenticated PVE session. + /// The work to run against the client. + private protected void Invoke(PveSession session, Action action) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + Invoke(session, timeoutOverride: null, client => { action(client); return null; }); + } + + /// + /// Runs against the injected client, or against a client + /// opened for this call with applied and disposed + /// when it returns. The override is ignored for an injected client. + /// + /// The result type. + /// The authenticated PVE session. + /// Per-call request timeout for a client opened here. + /// The work to run against the client. + private protected T Invoke(PveSession session, TimeSpan? timeoutOverride, Func action) + { + if (session == null) throw new ArgumentNullException(nameof(session)); + if (action == null) throw new ArgumentNullException(nameof(action)); + + if (_injectedClient != null) + return action(_injectedClient); + + var client = CreateClient(session, timeoutOverride); + try + { + return action(client); + } + finally + { + client.Dispose(); + } + } + + /// + /// Test seam: builds the per-call client. Production services always get a + /// for . + /// + /// The authenticated PVE session. + /// Per-call request timeout, or null for the session's. + internal virtual IPveHttpClient CreateClient(PveSession session, TimeSpan? timeoutOverride) => + new PveHttpClient(session, timeoutOverride); + } +} diff --git a/src/PSProxmoxVE.Core/Services/SnapshotService.cs b/src/PSProxmoxVE.Core/Services/SnapshotService.cs index ca6a867..932c743 100644 --- a/src/PSProxmoxVE.Core/Services/SnapshotService.cs +++ b/src/PSProxmoxVE.Core/Services/SnapshotService.cs @@ -11,10 +11,8 @@ namespace PSProxmoxVE.Core.Services /// Service for Proxmox VE VM snapshot API operations. /// All operations apply to QEMU/KVM VMs via the /nodes/{node}/qemu/{vmid}/snapshot endpoints. /// - public class SnapshotService + public class SnapshotService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - /// /// Initializes a new instance of the class. /// @@ -24,10 +22,7 @@ namespace PSProxmoxVE.Core.Services /// 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 SnapshotService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public SnapshotService(IPveHttpClient client) : base(client) { } /// /// Returns all snapshots for a VM. @@ -40,18 +35,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -83,17 +73,12 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(description)) formData["description"] = description!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -113,17 +98,12 @@ namespace PSProxmoxVE.Core.Services 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 + return Invoke(session, client => { var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}") .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -143,17 +123,12 @@ namespace PSProxmoxVE.Core.Services 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 + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}/rollback") .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- diff --git a/src/PSProxmoxVE.Core/Services/StorageService.cs b/src/PSProxmoxVE.Core/Services/StorageService.cs index 5b4162e..e537c53 100644 --- a/src/PSProxmoxVE.Core/Services/StorageService.cs +++ b/src/PSProxmoxVE.Core/Services/StorageService.cs @@ -12,10 +12,8 @@ namespace PSProxmoxVE.Core.Services /// /// Service for Proxmox VE storage API operations. /// - public class StorageService + public class StorageService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - /// /// Initializes a new instance of with no injected client. /// Each method will create and dispose its own . @@ -27,10 +25,7 @@ namespace PSProxmoxVE.Core.Services /// The caller owns the client's lifetime; this service will not dispose it. /// /// The HTTP client to use for all requests. - public StorageService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public StorageService(IPveHttpClient client) : base(client) { } // ------------------------------------------------------------------------- // Read operations @@ -50,17 +45,12 @@ namespace PSProxmoxVE.Core.Services ? $"nodes/{Uri.EscapeDataString(node)}/storage" : "storage"; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -86,17 +76,12 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(contentType)) resource += $"?content={Uri.EscapeDataString(contentType!)}"; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -137,8 +122,7 @@ namespace PSProxmoxVE.Core.Services ["content"] = "iso" }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session, timeout ?? TimeSpan.FromMinutes(30)); - try + return Invoke(session, timeout ?? TimeSpan.FromMinutes(30), client => { var response = client.UploadFileAsync( $"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/upload", @@ -149,11 +133,7 @@ namespace PSProxmoxVE.Core.Services progressCallback) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -187,17 +167,12 @@ namespace PSProxmoxVE.Core.Services ["content"] = contentType }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/download-url", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -215,8 +190,7 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var formData = config.ToDictionary( kvp => kvp.Key, @@ -224,11 +198,7 @@ namespace PSProxmoxVE.Core.Services var response = client.PostAsync("storage", formData).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveStorage(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -241,15 +211,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"storage/{Uri.EscapeDataString(storage)}").GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -264,16 +229,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"storage/{Uri.EscapeDataString(storage)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -292,17 +252,12 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/status").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveStorageStatus(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -319,16 +274,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage)); if (string.IsNullOrWhiteSpace(volume)) throw new ArgumentNullException(nameof(volume)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/content/{Uri.EscapeDataString(volume)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -347,16 +297,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(volume)) throw new ArgumentNullException(nameof(volume)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/content/{Uri.EscapeDataString(volume)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -373,17 +318,12 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/content", config) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- diff --git a/src/PSProxmoxVE.Core/Services/TaskService.cs b/src/PSProxmoxVE.Core/Services/TaskService.cs index 512a36b..e9a49d8 100644 --- a/src/PSProxmoxVE.Core/Services/TaskService.cs +++ b/src/PSProxmoxVE.Core/Services/TaskService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; using Newtonsoft.Json.Linq; using PSProxmoxVE.Core.Authentication; using PSProxmoxVE.Core.Client; @@ -12,24 +13,50 @@ namespace PSProxmoxVE.Core.Services /// /// Service for querying and waiting on Proxmox VE asynchronous tasks (UPIDs). /// - public class TaskService + public class TaskService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(10); - private static readonly TimeSpan DefaultPollInterval = TimeSpan.FromSeconds(2); private static readonly TimeSpan MinPollInterval = TimeSpan.FromSeconds(1); + private static readonly TimeSpan MaxBackoffInterval = TimeSpan.FromSeconds(10); + private static readonly TimeSpan BackoffStep = TimeSpan.FromSeconds(1); + + private readonly Func _pollDelay; /// Initializes a new instance that creates its own HTTP clients. - public TaskService() { } + public TaskService() : this(Sleep) { } /// Initializes a new instance that uses the supplied HTTP client for all requests. /// The HTTP client to use. The caller owns its lifetime. - public TaskService(IPveHttpClient client) + public TaskService(IPveHttpClient client) : this(client, Sleep) { } + + /// + /// Test seam: same as but with the wait between polls + /// replaceable, so a test can assert the poll schedule without sleeping for it. + /// + /// Invoked with each computed poll interval instead of sleeping. + internal TaskService(Func pollDelay) { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); + _pollDelay = pollDelay ?? throw new ArgumentNullException(nameof(pollDelay)); } + /// + /// Test seam: same as but with the wait between + /// polls replaceable, so a test can assert the poll schedule without sleeping for it. + /// + /// The HTTP client to use. The caller owns its lifetime. + /// Invoked with each computed poll interval instead of sleeping. + internal TaskService(IPveHttpClient client, Func pollDelay) : base(client) + { + _pollDelay = pollDelay ?? throw new ArgumentNullException(nameof(pollDelay)); + } + + private static Task Sleep(TimeSpan duration) + { + Thread.Sleep(duration); + return Task.CompletedTask; + } + + /// /// Returns the current status of a task identified by its UPID. /// @@ -39,21 +66,21 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var encodedUpid = Uri.EscapeDataString(upid); var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/tasks/{encodedUpid}/status") .GetAwaiter().GetResult(); - var data = JObject.Parse(response)["data"]; - var task = data?.ToObject() ?? new PveTask { Upid = upid }; - task.Node = node; - return task; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + return ParseTaskStatus(response, node, upid); + }); + } + + private static PveTask ParseTaskStatus(string response, string node, string upid) + { + var data = JObject.Parse(response)["data"]; + var task = data?.ToObject() ?? new PveTask { Upid = upid }; + task.Node = node; + return task; } /// @@ -65,29 +92,29 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var encodedUpid = Uri.EscapeDataString(upid); var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/tasks/{encodedUpid}/log") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// - /// Polls the task status until it completes, throws on timeout or failure. + /// Polls the task status until it completes, throws on timeout or failure. One HTTP + /// client is held open for the whole wait. /// /// Active PVE session. /// Node name where the task is running. /// Task UPID. /// Maximum time to wait. Defaults to 10 minutes. - /// Interval between status polls. Defaults to 2 seconds. Minimum 1 second. + /// + /// Fixed interval between status polls, minimum 1 second. When omitted the interval + /// starts at 1 second and grows by 1 second per poll up to a 10 second cap. A wait + /// never sleeps past . + /// /// Optional callback invoked on each poll with the current task. /// The completed . /// Thrown when the task does not complete within . @@ -105,29 +132,44 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid)); var effectiveTimeout = timeout ?? DefaultTimeout; - var effectivePoll = pollInterval ?? DefaultPollInterval; - if (effectivePoll < MinPollInterval) - effectivePoll = MinPollInterval; + var fixedInterval = pollInterval.HasValue + ? (pollInterval.Value < MinPollInterval ? MinPollInterval : pollInterval.Value) + : (TimeSpan?)null; var deadline = DateTime.UtcNow.Add(effectiveTimeout); + var statusResource = $"nodes/{Uri.EscapeDataString(node)}/tasks/{Uri.EscapeDataString(upid)}/status"; - while (true) + return Invoke(session, client => { - var task = GetTask(session, node, upid); - progressCallback?.Invoke(task); - - if (string.Equals(task.Status, "stopped", StringComparison.OrdinalIgnoreCase)) + var interval = fixedInterval ?? MinPollInterval; + while (true) { - if (!string.Equals(task.ExitStatus, "OK", StringComparison.OrdinalIgnoreCase)) - throw new PveTaskFailedException(upid, task.ExitStatus ?? "(no exit status)"); - return task; + var response = client.GetAsync(statusResource).GetAwaiter().GetResult(); + var task = ParseTaskStatus(response, node, upid); + progressCallback?.Invoke(task); + + if (string.Equals(task.Status, "stopped", StringComparison.OrdinalIgnoreCase)) + { + if (!string.Equals(task.ExitStatus, "OK", StringComparison.OrdinalIgnoreCase)) + throw new PveTaskFailedException(upid, task.ExitStatus ?? "(no exit status)"); + return task; + } + + var now = DateTime.UtcNow; + if (now >= deadline) + throw new PveTaskTimeoutException(upid, effectiveTimeout); + + var remaining = deadline - now; + _pollDelay(interval < remaining ? interval : remaining).GetAwaiter().GetResult(); + + if (!fixedInterval.HasValue && interval < MaxBackoffInterval) + { + interval += BackoffStep; + if (interval > MaxBackoffInterval) + interval = MaxBackoffInterval; + } } - - if (DateTime.UtcNow >= deadline) - throw new PveTaskTimeoutException(upid, effectiveTimeout); - - Thread.Sleep(effectivePoll); - } + }); } /// @@ -145,8 +187,7 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var queryParts = new List { $"limit={limit}" }; if (vmid.HasValue) @@ -164,11 +205,7 @@ namespace PSProxmoxVE.Core.Services foreach (var t in tasks) t.Node ??= node; return tasks; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -183,17 +220,12 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var encodedUpid = Uri.EscapeDataString(upid); client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/tasks/{encodedUpid}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } } } diff --git a/src/PSProxmoxVE.Core/Services/TemplateService.cs b/src/PSProxmoxVE.Core/Services/TemplateService.cs index 3fa53a2..6f05627 100644 --- a/src/PSProxmoxVE.Core/Services/TemplateService.cs +++ b/src/PSProxmoxVE.Core/Services/TemplateService.cs @@ -11,23 +11,25 @@ namespace PSProxmoxVE.Core.Services /// Service for Proxmox VE VM template operations. /// Templates are VMs with the "template" flag set to 1. /// - public class TemplateService + public class TemplateService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - private readonly VmService _vmService = new VmService(); + private readonly VmService _vmService; /// /// Initializes a new instance of the class. /// - public TemplateService() { } + public TemplateService() + { + _vmService = new VmService(); + } /// /// 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 TemplateService(IPveHttpClient client) + public TemplateService(IPveHttpClient client) : base(client) { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); + _vmService = new VmService(client); } /// @@ -58,17 +60,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/template") .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// diff --git a/src/PSProxmoxVE.Core/Services/UserService.cs b/src/PSProxmoxVE.Core/Services/UserService.cs index 5c07f32..e0295ba 100644 --- a/src/PSProxmoxVE.Core/Services/UserService.cs +++ b/src/PSProxmoxVE.Core/Services/UserService.cs @@ -11,10 +11,8 @@ namespace PSProxmoxVE.Core.Services /// /// Service for Proxmox VE access control — users, roles, and permissions (ACLs). /// - public class UserService + public class UserService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - /// /// Initializes a new instance of with no injected client. /// Each method will create and dispose its own . @@ -26,10 +24,7 @@ namespace PSProxmoxVE.Core.Services /// The caller owns the client's lifetime; this service will not dispose it. /// /// The HTTP client to use for all requests. - public UserService(IPveHttpClient client) - { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); - } + public UserService(IPveHttpClient client) : base(client) { } // ------------------------------------------------------------------------- // Users @@ -41,17 +36,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("access/users").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Returns a single user by their user ID (e.g. "admin@pam"). @@ -62,8 +52,7 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var encodedId = Uri.EscapeDataString(userId); var response = client.GetAsync($"access/users/{encodedId}").GetAwaiter().GetResult(); @@ -73,11 +62,7 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrEmpty(user.UserId)) user.UserId = userId; return user; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -101,15 +86,10 @@ namespace PSProxmoxVE.Core.Services formData[kvp.Key] = kvp.Value?.ToString() ?? string.Empty; } - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("access/users", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Removes a user account. @@ -120,16 +100,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var encodedId = Uri.EscapeDataString(userId); client.DeleteAsync($"access/users/{encodedId}").GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Updates one or more properties of an existing user. @@ -145,19 +120,14 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var encodedId = Uri.EscapeDataString(userId); var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); client.PutAsync($"access/users/{encodedId}", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -172,8 +142,7 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var encodedId = Uri.EscapeDataString(userId); var response = client.GetAsync($"access/users/{encodedId}/token").GetAwaiter().GetResult(); @@ -182,11 +151,7 @@ namespace PSProxmoxVE.Core.Services foreach (var t in tokens) t.UserId = userId; return tokens; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -218,8 +183,7 @@ namespace PSProxmoxVE.Core.Services if (expire.HasValue) formData["expire"] = expire.Value.ToString(); if (privilegeSeparation.HasValue) formData["privsep"] = privilegeSeparation.Value ? "1" : "0"; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var encodedUser = Uri.EscapeDataString(userId); var encodedToken = Uri.EscapeDataString(tokenId); @@ -232,11 +196,7 @@ namespace PSProxmoxVE.Core.Services token.UserId = userId; token.TokenId = tokenId; return token; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Removes an API token. @@ -249,18 +209,13 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId)); if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var encodedUser = Uri.EscapeDataString(userId); var encodedToken = Uri.EscapeDataString(tokenId); client.DeleteAsync($"access/users/{encodedUser}/token/{encodedToken}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -273,18 +228,13 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var encodedUser = Uri.EscapeDataString(userId); var encodedToken = Uri.EscapeDataString(tokenId); client.PutAsync($"access/users/{encodedUser}/token/{encodedToken}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -297,17 +247,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("access/roles").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Creates a new role. @@ -323,15 +268,10 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(privileges)) formData["privs"] = privileges!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("access/roles", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Removes a role. @@ -342,16 +282,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(roleId)) throw new ArgumentNullException(nameof(roleId)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"access/roles/{Uri.EscapeDataString(roleId)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -367,16 +302,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(privileges)) throw new ArgumentNullException(nameof(privileges)); var formData = new Dictionary { ["privs"] = privileges }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"access/roles/{Uri.EscapeDataString(roleId)}", formData) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -389,17 +319,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("access/groups").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Creates a new group. @@ -415,15 +340,10 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(comment)) formData["comment"] = comment!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("access/groups", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Updates a group's properties. @@ -436,16 +356,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(groupId)) throw new ArgumentNullException(nameof(groupId)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"access/groups/{Uri.EscapeDataString(groupId)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Removes a group. @@ -456,16 +371,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(groupId)) throw new ArgumentNullException(nameof(groupId)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"access/groups/{Uri.EscapeDataString(groupId)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -478,17 +388,12 @@ namespace PSProxmoxVE.Core.Services { if (session == null) throw new ArgumentNullException(nameof(session)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync("access/domains").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Creates a new authentication domain/realm. @@ -499,15 +404,10 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync("access/domains", config).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Updates an authentication domain/realm. @@ -520,16 +420,11 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(realm)) throw new ArgumentNullException(nameof(realm)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"access/domains/{Uri.EscapeDataString(realm)}", config) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Removes an authentication domain/realm. @@ -540,16 +435,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(realm)) throw new ArgumentNullException(nameof(realm)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.DeleteAsync($"access/domains/{Uri.EscapeDataString(realm)}") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -572,15 +462,10 @@ namespace PSProxmoxVE.Core.Services ["password"] = password }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync("access/password", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -609,8 +494,7 @@ namespace PSProxmoxVE.Core.Services if (queryParts.Count > 0) resource += "?" + string.Join("&", queryParts); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; @@ -635,11 +519,7 @@ namespace PSProxmoxVE.Core.Services result.Add(perm); } return result.ToArray(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -678,15 +558,10 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(groups)) formData["groups"] = groups!; if (!string.IsNullOrEmpty(tokens)) formData["tokens"] = tokens!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync("access/acl", formData).GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } } } diff --git a/src/PSProxmoxVE.Core/Services/VmService.cs b/src/PSProxmoxVE.Core/Services/VmService.cs index 347acfc..1de2033 100644 --- a/src/PSProxmoxVE.Core/Services/VmService.cs +++ b/src/PSProxmoxVE.Core/Services/VmService.cs @@ -13,19 +13,21 @@ namespace PSProxmoxVE.Core.Services /// /// Service for Proxmox VE QEMU/KVM virtual machine API operations. /// - public class VmService + public class VmService : PveServiceBase { - private readonly IPveHttpClient? _injectedClient; - private readonly NodeService _nodeService = new NodeService(); + private readonly NodeService _nodeService; /// Initializes a new instance that creates its own HTTP clients. - public VmService() { } + public VmService() + { + _nodeService = new NodeService(); + } /// Initializes a new instance that uses the supplied HTTP client for all requests. /// The HTTP client to use. The caller owns its lifetime. - public VmService(IPveHttpClient client) + public VmService(IPveHttpClient client) : base(client) { - _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); + _nodeService = new NodeService(client); } // ------------------------------------------------------------------------- @@ -90,17 +92,12 @@ namespace PSProxmoxVE.Core.Services private PveVm[] GetVmsOnNode(PveSession session, string node) { - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -135,8 +132,7 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (vm == null) throw new ArgumentNullException(nameof(vm)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vm.VmId}/status/current") .GetAwaiter().GetResult(); @@ -150,11 +146,7 @@ namespace PSProxmoxVE.Core.Services vm.CpuCount = data["cpus"]?.ToObject() ?? vm.CpuCount; vm.MaxMem = data["maxmem"]?.ToObject() ?? vm.MaxMem; vm.MaxDisk = data["maxdisk"]?.ToObject() ?? vm.MaxDisk; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -168,18 +160,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveVmConfig(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -199,19 +186,14 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config", formData) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -262,18 +244,13 @@ namespace PSProxmoxVE.Core.Services [disk] = diskValue }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { // POST (not PUT) because import-from triggers a background task var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -292,8 +269,7 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var formData = config.ToDictionary( kvp => kvp.Key, @@ -301,11 +277,7 @@ namespace PSProxmoxVE.Core.Services var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Starts a VM. Returns the task UPID. @@ -336,17 +308,12 @@ namespace PSProxmoxVE.Core.Services if (timeoutSeconds.HasValue) formData["timeout"] = timeoutSeconds.Value.ToString(); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/status/shutdown", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -372,17 +339,12 @@ namespace PSProxmoxVE.Core.Services if (timeoutSeconds.HasValue) formData["timeout"] = timeoutSeconds.Value.ToString(); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/status/reboot", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// Resets a VM (hard reset). Returns the task UPID. @@ -429,17 +391,12 @@ namespace PSProxmoxVE.Core.Services queryParams.Add("skiplock=1"); var queryString = "?" + string.Join("&", queryParams); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}{queryString}") .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -474,17 +431,12 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(targetNode)) formData["target"] = targetNode!; if (!string.IsNullOrEmpty(storage)) formData["storage"] = storage!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/clone", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -512,17 +464,12 @@ namespace PSProxmoxVE.Core.Services ["online"] = online ? "1" : "0" }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/migrate", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -553,17 +500,12 @@ namespace PSProxmoxVE.Core.Services ["size"] = size }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/resize", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -575,17 +517,12 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/status/{action}") .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } private static PveTask ParseTask(string response, string node) @@ -612,21 +549,19 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { - client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/ping").GetAwaiter().GetResult(); - return true; - } - catch (PSProxmoxVE.Core.Exceptions.PveApiException) - { - // Treat API-level failures for this endpoint as "guest agent not responding". - return false; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + try + { + client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/ping").GetAwaiter().GetResult(); + return true; + } + catch (PSProxmoxVE.Core.Exceptions.PveApiException) + { + // Treat API-level failures for this endpoint as "guest agent not responding". + return false; + } + }); } /// @@ -637,19 +572,14 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/network-get-interfaces") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; var result = data?["result"]; return result?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -663,8 +593,7 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(command)) throw new ArgumentNullException(nameof(command)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { // PVE's agent/exec "command" is an array: element 0 is the executable and // each subsequent element is one argv entry. It is sent as repeated form @@ -688,11 +617,7 @@ namespace PSProxmoxVE.Core.Services .GetAwaiter().GetResult(); var pid = JObject.Parse(response)["data"]?["pid"]?.ToObject() ?? 0; return pid; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -703,18 +628,13 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/exec-status?pid={pid}") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return JsonHelper.ToDictionary(data as JObject); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -740,17 +660,12 @@ namespace PSProxmoxVE.Core.Services if (!string.IsNullOrEmpty(format)) formData["format"] = format!; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/move_disk", formData) .GetAwaiter().GetResult(); return ParseTask(response, node); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -769,16 +684,11 @@ namespace PSProxmoxVE.Core.Services if (force) formData["force"] = "1"; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/unlink", formData) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -793,19 +703,14 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/get-osinfo") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; var result = data?["result"]; return result?.ToObject(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -816,19 +721,14 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/get-fsinfo") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; var result = data?["result"]; return result?.ToObject() ?? Array.Empty(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -840,18 +740,13 @@ namespace PSProxmoxVE.Core.Services if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(file)) throw new ArgumentNullException(nameof(file)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + return Invoke(session, client => { var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/file-read?file={Uri.EscapeDataString(file)}") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?["content"]?.ToString() ?? string.Empty; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -869,16 +764,11 @@ namespace PSProxmoxVE.Core.Services ["content"] = content }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/file-write", formData) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -898,16 +788,11 @@ namespace PSProxmoxVE.Core.Services if (crypted) formData["crypted"] = "1"; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/set-user-password", formData) .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } /// @@ -918,16 +803,11 @@ namespace PSProxmoxVE.Core.Services if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); - try + Invoke(session, client => { client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/fstrim") .GetAwaiter().GetResult(); - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } // ------------------------------------------------------------------------- @@ -967,8 +847,7 @@ namespace PSProxmoxVE.Core.Services ["content"] = "import" }; - IPveHttpClient client = _injectedClient ?? new PveHttpClient(session, timeout ?? TimeSpan.FromMinutes(30)); - try + return Invoke(session, timeout ?? TimeSpan.FromMinutes(30), client => { var response = client.UploadFileAsync( $"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/upload", @@ -980,11 +859,7 @@ namespace PSProxmoxVE.Core.Services var root = JObject.Parse(response); var upid = root["data"]?.ToString() ?? string.Empty; return new PveTask { Upid = upid, Node = node, Status = "running" }; - } - finally - { - if (_injectedClient == null) client.Dispose(); - } + }); } } } diff --git a/src/PSProxmoxVE/Cmdlets/Tasks/WaitPveTaskCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Tasks/WaitPveTaskCmdlet.cs index 1fc70e1..37c4570 100644 --- a/src/PSProxmoxVE/Cmdlets/Tasks/WaitPveTaskCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Tasks/WaitPveTaskCmdlet.cs @@ -45,7 +45,8 @@ namespace PSProxmoxVE.Cmdlets.Tasks public TimeSpan? Timeout { get; set; } /// - /// How frequently to poll the task status. Defaults to 2 seconds. + /// Fixed interval between status polls, minimum 1 second. When omitted, polling + /// starts at 1 second and backs off to a 10 second cap. /// Example: -PollInterval (New-TimeSpan -Seconds 5) /// [Parameter(Mandatory = false, HelpMessage = "How often to poll task status.")] diff --git a/tests/PSProxmoxVE.Core.Tests/Client/PveHandlerCacheTests.cs b/tests/PSProxmoxVE.Core.Tests/Client/PveHandlerCacheTests.cs new file mode 100644 index 0000000..ce71486 --- /dev/null +++ b/tests/PSProxmoxVE.Core.Tests/Client/PveHandlerCacheTests.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using PSProxmoxVE.Core.Authentication; +using PSProxmoxVE.Core.Client; +using Xunit; + +namespace PSProxmoxVE.Core.Tests.Client +{ + public class PveHandlerCacheTests + { + private static PveSession NewSession(bool skipCertificateCheck = true, string host = "pve.example.com") => + new PveSession(host, 8006, skipCertificateCheck, + "root@pam!token=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); + + private sealed class CannedHandler : HttpClientHandler + { + public int Sends; + public bool IsDisposed; + + protected override void Dispose(bool disposing) + { + IsDisposed = true; + base.Dispose(disposing); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (IsDisposed) throw new ObjectDisposedException(nameof(CannedHandler)); + Interlocked.Increment(ref Sends); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"data\":{\"version\":\"8.2\"}}") + }); + } + } + + private static (PveHandlerCache cache, List built) NewCache() + { + var built = new List(); + var cache = new PveHandlerCache(_ => + { + var h = new CannedHandler(); + built.Add(h); + return h; + }); + return (cache, built); + } + + [Fact] + public void TwoClientsForTheSameEndpointShareOneHandler() + { + var (cache, built) = NewCache(); + + using var first = new PveHttpClient(NewSession(), cache); + using var second = new PveHttpClient(NewSession(), cache); + + Assert.Single(built); + Assert.Same(first.Handler, second.Handler); + Assert.Equal(1, cache.Count); + } + + [Fact] + public void DifferentSkipCertificateCheckGetsADifferentHandler() + { + var (cache, built) = NewCache(); + + using var insecure = new PveHttpClient(NewSession(skipCertificateCheck: true), cache); + using var verified = new PveHttpClient(NewSession(skipCertificateCheck: false), cache); + + Assert.Equal(2, built.Count); + Assert.NotSame(insecure.Handler, verified.Handler); + } + + [Fact] + public void DifferentHostGetsADifferentHandler() + { + var (cache, _) = NewCache(); + + using var a = new PveHttpClient(NewSession(host: "pve-a.example.com"), cache); + using var b = new PveHttpClient(NewSession(host: "pve-b.example.com"), cache); + + Assert.NotSame(a.Handler, b.Handler); + } + + [Fact] + public async Task DisposingOneClientLeavesTheSharedHandlerUsableByAnother() + { + var (cache, built) = NewCache(); + + var first = new PveHttpClient(NewSession(), cache); + using var second = new PveHttpClient(NewSession(), cache); + first.Dispose(); + + var body = await second.GetAsync("version"); + + Assert.Contains("8.2", body); + Assert.Equal(1, built[0].Sends); + Assert.Single(built); + Assert.False(built[0].IsDisposed); + } + + [Fact] + public void AnExplicitHandlerBypassesTheCacheAndIsOwnedByTheClient() + { + var (cache, built) = NewCache(); + var own = new CannedHandler(); + + var client = new PveHttpClient(NewSession(), timeoutOverride: null, + guestLockRetryWindow: null, handler: own, guestLockRetryDelay: null, handlerCache: cache); + + Assert.Same(own, client.Handler); + Assert.Empty(built); + + client.Dispose(); + + Assert.True(own.IsDisposed); + } + + [Fact] + public void SharedHandlersDoNotCarryACookieContainer() + { + var handler = PveHandlerCache.Shared.Get("cookies.example.invalid", 8006, skipCertificateCheck: false); + + Assert.False(handler.UseCookies); + } + + [Fact] + public void GetBuildsEachKeyOnceUnderConcurrentCallers() + { + var (cache, built) = NewCache(); + var handlers = new HttpClientHandler[32]; + + Parallel.For(0, handlers.Length, i => + handlers[i] = cache.Get("pve.example.com", 8006, skipCertificateCheck: true)); + + Assert.Single(built); + Assert.All(handlers, h => Assert.Same(built[0], h)); + } + } +} diff --git a/tests/PSProxmoxVE.Core.Tests/Services/ContainerServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/ContainerServiceTests.cs index 533a547..bb445fa 100644 --- a/tests/PSProxmoxVE.Core.Tests/Services/ContainerServiceTests.cs +++ b/tests/PSProxmoxVE.Core.Tests/Services/ContainerServiceTests.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Reflection; using Moq; using PSProxmoxVE.Core.Authentication; using PSProxmoxVE.Core.Client; @@ -123,20 +122,6 @@ namespace PSProxmoxVE.Core.Tests.Services // GetContainers multi-node aggregation: issue #142 // --------------------------------------------------------------------- - /// - /// Points the private NodeService field a ContainerService constructs at a mock - /// client, so the "list all nodes" call the multi-node overload issues is reachable - /// without a real HTTP connection. ContainerService(client) only injects the client - /// used for the per-node lxc calls; NodeService is never constructor-injectable from - /// ContainerService, so this is the only offline path to the aggregation loop. - /// - private static void InjectNodeServiceClient(ContainerService service, IPveHttpClient client) - { - var field = typeof(ContainerService).GetField("_nodeService", BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new InvalidOperationException("ContainerService._nodeService field not found."); - field.SetValue(service, new NodeService(client)); - } - private static Mock SetupTwoNodeCluster() { var mockClient = new Mock(); @@ -158,7 +143,6 @@ namespace PSProxmoxVE.Core.Tests.Services .ThrowsAsync(new PveApiException(HttpStatusCode.InternalServerError, "internal error", "nodes/pve2/lxc", "GET")); var service = new ContainerService(mockClient.Object); - InjectNodeServiceClient(service, mockClient.Object); var skipped = new List(); var containers = service.GetContainers(CreateSession(), onNodeSkipped: (node, ex) => skipped.Add(node)); @@ -180,7 +164,6 @@ namespace PSProxmoxVE.Core.Tests.Services .ThrowsAsync(new PveApiException(HttpStatusCode.Forbidden, "permission denied", "nodes/pve2/lxc", "GET")); var service = new ContainerService(mockClient.Object); - InjectNodeServiceClient(service, mockClient.Object); var ex = Assert.Throws(() => service.GetContainers(CreateSession())); Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode); @@ -203,7 +186,6 @@ namespace PSProxmoxVE.Core.Tests.Services "nodes/pve2/lxc", "GET", new HttpRequestException("connection refused"))); var service = new ContainerService(mockClient.Object); - InjectNodeServiceClient(service, mockClient.Object); var skipped = new List(); var containers = service.GetContainers(CreateSession(), onNodeSkipped: (node, ex) => skipped.Add(node)); @@ -230,7 +212,6 @@ namespace PSProxmoxVE.Core.Tests.Services "nodes/pve2/lxc", "GET")); var service = new ContainerService(mockClient.Object); - InjectNodeServiceClient(service, mockClient.Object); var skipped = new List(); var containers = service.GetContainers(CreateSession(), onNodeSkipped: (node, ex) => skipped.Add(node)); diff --git a/tests/PSProxmoxVE.Core.Tests/Services/PveServiceBaseTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/PveServiceBaseTests.cs new file mode 100644 index 0000000..5983c9f --- /dev/null +++ b/tests/PSProxmoxVE.Core.Tests/Services/PveServiceBaseTests.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PSProxmoxVE.Core.Authentication; +using PSProxmoxVE.Core.Client; +using PSProxmoxVE.Core.Services; +using Xunit; + +namespace PSProxmoxVE.Core.Tests.Services +{ + public class PveServiceBaseTests + { + private static PveSession CreateSession() => + new PveSession("pve1.example.com", 8006, true, "PVE:root@pam:TEST_TOKEN"); + + private sealed class RecordingClient : IPveHttpClient + { + public int DisposeCalls; + public int Gets; + + public Task GetAsync(string resource) { Gets++; return Task.FromResult("{\"data\":null}"); } + public Task PostAsync(string resource, Dictionary? data = null) => throw new NotSupportedException(); + public Task PostAsync(string resource, IEnumerable> data) => throw new NotSupportedException(); + public Task PutAsync(string resource, Dictionary? data = null) => throw new NotSupportedException(); + public Task DeleteAsync(string resource) => throw new NotSupportedException(); + public string Get(string resource) => GetAsync(resource).GetAwaiter().GetResult(); + public string Post(string resource, Dictionary? data = null) => throw new NotSupportedException(); + public string Put(string resource, Dictionary? data = null) => throw new NotSupportedException(); + public string Delete(string resource) => throw new NotSupportedException(); + public Task UploadFileAsync(string resource, string filePath, Dictionary? formFields = null, + string? checksum = null, string? checksumAlgorithm = null, Action? progressCallback = null) => + throw new NotSupportedException(); + public void Dispose() => DisposeCalls++; + } + + private sealed class ProbeService : PveServiceBase + { + private readonly RecordingClient? _built; + public TimeSpan? SeenTimeoutOverride; + + private ProbeService(RecordingClient built) { _built = built; } + private ProbeService(IPveHttpClient injected) : base(injected) { } + + public static ProbeService Building(RecordingClient built) => new ProbeService(built); + public static ProbeService Using(IPveHttpClient injected) => new ProbeService(injected); + + internal override IPveHttpClient CreateClient(PveSession session, TimeSpan? timeoutOverride) + { + SeenTimeoutOverride = timeoutOverride; + return _built ?? throw new InvalidOperationException("CreateClient reached with an injected client."); + } + + public string Fetch(PveSession session) => Invoke(session, c => c.Get("version")); + public string FetchWithTimeout(PveSession session, TimeSpan timeout) => Invoke(session, timeout, c => c.Get("version")); + public void Touch(PveSession session) => Invoke(session, c => { c.Get("version"); }); + public string Throw(PveSession session) => Invoke(session, c => throw new InvalidOperationException("boom")); + } + + [Fact] + public void WithNoInjectedClient_TheClientBuiltForTheCallIsDisposed() + { + var built = new RecordingClient(); + var service = ProbeService.Building(built); + + service.Fetch(CreateSession()); + + Assert.Equal(1, built.Gets); + Assert.Equal(1, built.DisposeCalls); + } + + [Fact] + public void WithNoInjectedClient_TheClientIsDisposedWhenTheActionThrows() + { + var built = new RecordingClient(); + var service = ProbeService.Building(built); + + Assert.Throws(() => service.Throw(CreateSession())); + + Assert.Equal(1, built.DisposeCalls); + } + + [Fact] + public void WithAnInjectedClient_ItIsUsedAndNeverDisposed() + { + var injected = new RecordingClient(); + var service = ProbeService.Using(injected); + + service.Fetch(CreateSession()); + service.Touch(CreateSession()); + + Assert.Equal(2, injected.Gets); + Assert.Equal(0, injected.DisposeCalls); + } + + [Fact] + public void TheVoidOverloadDisposesTheClientItBuilt() + { + var built = new RecordingClient(); + var service = ProbeService.Building(built); + + service.Touch(CreateSession()); + + Assert.Equal(1, built.DisposeCalls); + } + + [Fact] + public void TheTimeoutOverloadPassesTheOverrideToTheClientFactory() + { + var built = new RecordingClient(); + var service = ProbeService.Building(built); + + service.FetchWithTimeout(CreateSession(), TimeSpan.FromMinutes(30)); + + Assert.Equal(TimeSpan.FromMinutes(30), service.SeenTimeoutOverride); + } + + [Fact] + public void ANullSessionIsRejectedBeforeAnyClientIsBuilt() + { + var built = new RecordingClient(); + var service = ProbeService.Building(built); + + var ex = Assert.Throws(() => service.Fetch(null!)); + + Assert.Equal("session", ex.ParamName); + Assert.Equal(0, built.DisposeCalls); + } + + [Fact] + public void NestedServicesShareTheInjectedClient() + { + var injected = new RecordingClient(); + + new VmService(injected).GetVms(CreateSession()); + new ContainerService(injected).GetContainers(CreateSession()); + new TemplateService(injected).GetTemplates(CreateSession()); + + Assert.Equal(3, injected.Gets); + Assert.Equal(0, injected.DisposeCalls); + } + } +} diff --git a/tests/PSProxmoxVE.Core.Tests/Services/TaskServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/TaskServiceTests.cs index dd8f7d3..58b050f 100644 --- a/tests/PSProxmoxVE.Core.Tests/Services/TaskServiceTests.cs +++ b/tests/PSProxmoxVE.Core.Tests/Services/TaskServiceTests.cs @@ -336,6 +336,129 @@ namespace PSProxmoxVE.Core.Tests.Services Assert.Equal(new List { "running", "running", "stopped" }, seenStatuses); } + private const string RunningJson = @"{ ""data"": { ""status"": ""running"", ""user"": ""root@pam"" } }"; + private const string StoppedJson = @"{ ""data"": { ""status"": ""stopped"", ""exitstatus"": ""OK"", ""user"": ""root@pam"" } }"; + + private static (TaskService service, Mock client, List delays) ServiceWithRecordedDelays(int runningPolls) + { + var mockClient = new Mock(); + var sequence = mockClient.SetupSequence(c => c.GetAsync(It.IsAny())); + for (var i = 0; i < runningPolls; i++) + sequence = sequence.ReturnsAsync(RunningJson); + sequence.ReturnsAsync(StoppedJson); + + var delays = new List(); + var service = new TaskService(mockClient.Object, d => { delays.Add(d); return Task.CompletedTask; }); + return (service, mockClient, delays); + } + + [Fact] + public void WaitForTask_WithNoPollInterval_BacksOffFromOneSecondToATenSecondCap() + { + var (service, mockClient, delays) = ServiceWithRecordedDelays(runningPolls: 12); + + var task = service.WaitForTask(CreateSession(), TestNode, TestUpid, timeout: TimeSpan.FromMinutes(5)); + + Assert.True(task.IsSuccessful); + mockClient.Verify(c => c.GetAsync(It.IsAny()), Times.Exactly(13)); + Assert.Equal(12, delays.Count); + Assert.Equal(TimeSpan.FromSeconds(1), delays[0]); + Assert.Equal(TimeSpan.FromSeconds(2), delays[1]); + for (var i = 1; i < delays.Count; i++) + Assert.True(delays[i] >= delays[i - 1], $"delay {i} ({delays[i]}) shrank from {delays[i - 1]}"); + Assert.All(delays, d => Assert.True(d <= TimeSpan.FromSeconds(10), $"delay {d} exceeds the cap")); + Assert.Equal(TimeSpan.FromSeconds(10), delays[delays.Count - 1]); + Assert.Contains(delays, d => d > TimeSpan.FromSeconds(1) && d < TimeSpan.FromSeconds(10)); + } + + [Fact] + public void WaitForTask_NeverSleepsPastTheDeadline() + { + var mockClient = new Mock(); + mockClient.Setup(c => c.GetAsync(It.IsAny())).ReturnsAsync(RunningJson); + var delays = new List(); + var service = new TaskService(mockClient.Object, d => { delays.Add(d); return Task.CompletedTask; }); + var timeout = TimeSpan.FromMilliseconds(300); + + Assert.Throws(() => + service.WaitForTask(CreateSession(), TestNode, TestUpid, timeout: timeout)); + + Assert.NotEmpty(delays); + Assert.All(delays, d => Assert.True(d <= timeout, $"slept {d} against a {timeout} timeout")); + } + + [Fact] + public void WaitForTask_WithAnExplicitPollInterval_EveryDelayEqualsIt() + { + var (service, mockClient, delays) = ServiceWithRecordedDelays(runningPolls: 5); + + service.WaitForTask(CreateSession(), TestNode, TestUpid, + timeout: TimeSpan.FromMinutes(5), pollInterval: TimeSpan.FromSeconds(3)); + + mockClient.Verify(c => c.GetAsync(It.IsAny()), Times.Exactly(6)); + Assert.Equal(5, delays.Count); + Assert.All(delays, d => Assert.Equal(TimeSpan.FromSeconds(3), d)); + } + + [Fact] + public void WaitForTask_WithAnExplicitPollIntervalBelowTheMinimum_EveryDelayIsOneSecond() + { + var (service, _, delays) = ServiceWithRecordedDelays(runningPolls: 3); + + service.WaitForTask(CreateSession(), TestNode, TestUpid, + timeout: TimeSpan.FromMinutes(5), pollInterval: TimeSpan.FromMilliseconds(50)); + + Assert.Equal(3, delays.Count); + Assert.All(delays, d => Assert.Equal(TimeSpan.FromSeconds(1), d)); + } + + [Fact] + public void WaitForTask_PollsTheStatusEndpointOncePerPollThroughTheInjectedClient() + { + var (service, mockClient, _) = ServiceWithRecordedDelays(runningPolls: 2); + + service.WaitForTask(CreateSession(), TestNode, TestUpid, timeout: TimeSpan.FromMinutes(5)); + + var expected = $"nodes/{TestNode}/tasks/{Uri.EscapeDataString(TestUpid)}/status"; + mockClient.Verify(c => c.GetAsync(expected), Times.Exactly(3)); + mockClient.Verify(c => c.Dispose(), Times.Never); + } + + private sealed class ClientCountingTaskService : TaskService + { + public readonly List> Built = new List>(); + private readonly int _runningPolls; + + public ClientCountingTaskService(int runningPolls) : base(_ => Task.CompletedTask) + { + _runningPolls = runningPolls; + } + + internal override IPveHttpClient CreateClient(PveSession session, TimeSpan? timeoutOverride) + { + var mock = new Mock(); + var sequence = mock.SetupSequence(c => c.GetAsync(It.IsAny())); + for (var i = 0; i < _runningPolls; i++) + sequence = sequence.ReturnsAsync(RunningJson); + sequence.ReturnsAsync(StoppedJson); + Built.Add(mock); + return mock.Object; + } + } + + [Fact] + public void WaitForTask_WithNoInjectedClient_OpensOneClientForTheWholeWaitAndDisposesItAfter() + { + var service = new ClientCountingTaskService(runningPolls: 4); + + var task = service.WaitForTask(CreateSession(), TestNode, TestUpid, timeout: TimeSpan.FromMinutes(5)); + + Assert.True(task.IsSuccessful); + var client = Assert.Single(service.Built); + client.Verify(c => c.GetAsync(It.IsAny()), Times.Exactly(5)); + client.Verify(c => c.Dispose(), Times.Once); + } + [Fact] public void StopTask_CallsDeleteAsyncWithCorrectPath() { diff --git a/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs b/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs index 01ef2e8..bca3048 100644 --- a/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs +++ b/tests/PSProxmoxVE.Core.Tests/Services/VmServiceTests.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; -using System.Reflection; using Moq; using PSProxmoxVE.Core.Authentication; using PSProxmoxVE.Core.Client; @@ -296,20 +295,6 @@ namespace PSProxmoxVE.Core.Tests.Services // GetVms multi-node aggregation: issue #142 // --------------------------------------------------------------------- - /// - /// Points the private NodeService field a VmService constructs at a mock client, - /// so the "list all nodes" call the multi-node overload issues is reachable - /// without a real HTTP connection. VmService(client) only injects the client used - /// for the per-node qemu calls; NodeService is never constructor-injectable from - /// VmService, so this is the only offline path to the aggregation loop. - /// - private static void InjectNodeServiceClient(VmService service, IPveHttpClient client) - { - var field = typeof(VmService).GetField("_nodeService", BindingFlags.NonPublic | BindingFlags.Instance) - ?? throw new InvalidOperationException("VmService._nodeService field not found."); - field.SetValue(service, new NodeService(client)); - } - private static Mock SetupTwoNodeCluster() { var mockClient = new Mock(); @@ -331,7 +316,6 @@ namespace PSProxmoxVE.Core.Tests.Services .ThrowsAsync(new PveApiException(HttpStatusCode.InternalServerError, "internal error", "nodes/pve2/qemu", "GET")); var service = new VmService(mockClient.Object); - InjectNodeServiceClient(service, mockClient.Object); var skipped = new List(); var vms = service.GetVms(CreateSession(), onNodeSkipped: (node, ex) => skipped.Add(node)); @@ -353,7 +337,6 @@ namespace PSProxmoxVE.Core.Tests.Services .ThrowsAsync(new PveApiException(HttpStatusCode.Forbidden, "permission denied", "nodes/pve2/qemu", "GET")); var service = new VmService(mockClient.Object); - InjectNodeServiceClient(service, mockClient.Object); var ex = Assert.Throws(() => service.GetVms(CreateSession())); Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode); @@ -376,7 +359,6 @@ namespace PSProxmoxVE.Core.Tests.Services "nodes/pve2/qemu", "GET", new HttpRequestException("connection refused"))); var service = new VmService(mockClient.Object); - InjectNodeServiceClient(service, mockClient.Object); var skipped = new List(); var vms = service.GetVms(CreateSession(), onNodeSkipped: (node, ex) => skipped.Add(node)); @@ -403,7 +385,6 @@ namespace PSProxmoxVE.Core.Tests.Services "nodes/pve2/qemu", "GET")); var service = new VmService(mockClient.Object); - InjectNodeServiceClient(service, mockClient.Object); var skipped = new List(); var vms = service.GetVms(CreateSession(), onNodeSkipped: (node, ex) => skipped.Add(node));