From 907b2aa1f2394d439a1a663582d136b5dedf0f4d Mon Sep 17 00:00:00 2001 From: "goodolclint-claude[bot]" <323206664+goodolclint-claude[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:55:38 +0000 Subject: [PATCH] refactor: share one transport per host and poll tasks with backoff (#151) (#193) Every service method built and disposed its own PveHttpClient, and each client owned a fresh HttpClientHandler, so every API call was a TCP connect plus a TLS handshake and WaitForTask paid that 300 times over a ten-minute wait. PveServiceBase now owns the injected-or-fresh client lifetime behind one Invoke helper; the 201 hand-written try/finally blocks across the 16 services collapse to calls on it, and the nested NodeService/VmService instances receive the injected client. PveHttpClient takes its handler from a process-wide PveHandlerCache keyed on (host, port, skipCertificateCheck) and never disposes it, so the connection pool outlives any one client. WaitForTask holds one client for the whole wait and, when no pollInterval is supplied, backs off from 1 s toward a 10 s cap, never sleeping past the deadline. Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com> --- .../Client/PveHandlerCache.cs | 70 +++++ src/PSProxmoxVE.Core/Client/PveHttpClient.cs | 55 ++-- .../Services/BackupService.cs | 72 +---- .../Services/CloudInitService.cs | 36 +-- .../Services/ClusterConfigService.cs | 135 ++------- .../Services/ClusterService.cs | 27 +- .../Services/ContainerService.cs | 167 +++-------- .../Services/FirewallService.cs | 234 ++++----------- src/PSProxmoxVE.Core/Services/HaService.cs | 180 +++-------- .../Services/NetworkService.cs | 279 ++++-------------- src/PSProxmoxVE.Core/Services/NodeService.cs | 90 ++---- src/PSProxmoxVE.Core/Services/PoolService.cs | 54 +--- .../Services/PveServiceBase.cs | 86 ++++++ .../Services/SnapshotService.cs | 45 +-- .../Services/StorageService.cs | 108 ++----- src/PSProxmoxVE.Core/Services/TaskService.cs | 148 ++++++---- .../Services/TemplateService.cs | 23 +- src/PSProxmoxVE.Core/Services/UserService.cs | 225 ++++---------- src/PSProxmoxVE.Core/Services/VmService.cs | 265 +++++------------ .../Cmdlets/Tasks/WaitPveTaskCmdlet.cs | 3 +- .../Client/PveHandlerCacheTests.cs | 144 +++++++++ .../Services/ContainerServiceTests.cs | 19 -- .../Services/PveServiceBaseTests.cs | 142 +++++++++ .../Services/TaskServiceTests.cs | 123 ++++++++ .../Services/VmServiceTests.cs | 19 -- 25 files changed, 1144 insertions(+), 1605 deletions(-) create mode 100644 src/PSProxmoxVE.Core/Client/PveHandlerCache.cs create mode 100644 src/PSProxmoxVE.Core/Services/PveServiceBase.cs create mode 100644 tests/PSProxmoxVE.Core.Tests/Client/PveHandlerCacheTests.cs create mode 100644 tests/PSProxmoxVE.Core.Tests/Services/PveServiceBaseTests.cs 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));