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>
This commit is contained in:
goodolclint-claude[bot]
2026-09-02 22:55:38 +00:00
committed by GitHub
parent 40f4eae9e2
commit 907b2aa1f2
25 changed files with 1144 additions and 1605 deletions
@@ -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
{
/// <summary>
/// Process-wide cache of <see cref="HttpClientHandler"/> instances, one per
/// (host, port, skipCertificateCheck). The handler owns the connection pool, so sharing
/// it lets every <see cref="PveHttpClient"/> 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.
/// </summary>
internal sealed class PveHandlerCache
{
/// <summary>The cache production clients share.</summary>
internal static readonly PveHandlerCache Shared = new PveHandlerCache(CreateHandler);
private readonly object _gate = new object();
private readonly Dictionary<string, HttpClientHandler> _handlers =
new Dictionary<string, HttpClientHandler>(StringComparer.OrdinalIgnoreCase);
private readonly Func<bool, HttpClientHandler> _factory;
/// <summary>
/// Test seam: a cache whose handlers come from <paramref name="factory"/>, which
/// receives the skipCertificateCheck flag for the key being populated.
/// </summary>
internal PveHandlerCache(Func<bool, HttpClientHandler> factory)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
/// <summary>Number of handlers built so far.</summary>
internal int Count
{
get { lock (_gate) return _handlers.Count; }
}
/// <summary>Returns the handler for the endpoint, building it on first use.</summary>
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;
}
}
}
+35 -20
View File
@@ -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
{
/// <summary>
@@ -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).
/// </param>
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)
{
}
/// <summary>
/// Test seam: same as the public constructor but drawing the transport handler from
/// <paramref name="handlerCache"/> instead of <see cref="PveHandlerCache.Shared"/>.
/// </summary>
/// <param name="session">The authenticated PVE session providing credentials and base URL.</param>
/// <param name="handlerCache">The cache to take the handler from.</param>
internal PveHttpClient(PveSession session, PveHandlerCache handlerCache)
: this(session, timeoutOverride: null, guestLockRetryWindow: null, handler: null, guestLockRetryDelay: null,
handlerCache: handlerCache ?? throw new ArgumentNullException(nameof(handlerCache)))
{
}
/// <summary>
/// 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.
/// </summary>
/// <param name="session">The authenticated PVE session providing credentials and base URL.</param>
/// <param name="timeoutOverride">Optional per-instance timeout override.</param>
@@ -62,42 +73,42 @@ namespace PSProxmoxVE.Core.Client
/// Null uses <see cref="GuestLockRetry.DefaultWindow"/>, the same as the public constructor.
/// </param>
/// <param name="handler">
/// Message handler to send requests through. Null builds the production
/// certificate-validation handler from <see cref="PveSession.SkipCertificateCheck"/>.
/// 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.
/// </param>
/// <param name="guestLockRetryDelay">
/// Invoked before each guest-lock reissue instead of sleeping. Null uses
/// <see cref="Task.Delay(TimeSpan)"/>, the same as the public constructor.
/// </param>
/// <param name="handlerCache">
/// Cache to take the handler from when <paramref name="handler"/> is null. Null uses
/// <see cref="PveHandlerCache.Shared"/>, the same as the public constructor.
/// </param>
internal PveHttpClient(
PveSession session,
TimeSpan? timeoutOverride,
TimeSpan? guestLockRetryWindow,
HttpMessageHandler? handler,
Func<TimeSpan, Task>? guestLockRetryDelay = null)
Func<TimeSpan, Task>? 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;
}
/// <summary>The transport handler requests go through.</summary>
internal HttpMessageHandler Handler => _handler;
/// <summary>
/// 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();
}
/// <inheritdoc />
/// <summary>
/// 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.
/// </summary>
public void Dispose()
{
if (!_disposed)
+16 -56
View File
@@ -12,10 +12,8 @@ namespace PSProxmoxVE.Core.Services
/// <summary>
/// Service for Proxmox VE Backup (vzdump) and backup job API operations.
/// </summary>
public class BackupService
public class BackupService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>
/// Initializes a new instance of <see cref="BackupService"/> with no injected client.
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
@@ -27,10 +25,7 @@ namespace PSProxmoxVE.Core.Services
/// The caller owns the client's lifetime; this service will not dispose it.
/// </summary>
/// <param name="client">The HTTP client to use for all requests.</param>
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<PveBackupJob[]>() ?? Array.Empty<PveBackupJob>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveBackupJob>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
// -------------------------------------------------------------------------
@@ -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.
/// </summary>
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 <see cref="CloudInitService"/> class with an injected HTTP client.
/// </summary>
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
public CloudInitService(IPveHttpClient client)
{
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
}
public CloudInitService(IPveHttpClient client) : base(client) { }
/// <summary>
/// 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<PveCloudInitConfig>() ?? new PveCloudInitConfig();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
}
}
@@ -14,10 +14,8 @@ namespace PSProxmoxVE.Core.Services
/// Service for Proxmox VE cluster configuration API operations
/// (/cluster/config, /cluster/options, /cluster/nextid).
/// </summary>
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 <see cref="ClusterConfigService"/> class with an injected HTTP client.
/// </summary>
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
public ClusterConfigService(IPveHttpClient client)
{
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
}
public ClusterConfigService(IPveHttpClient client) : base(client) { }
/// <summary>
/// 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<string, object?>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveClusterConfigNode[]>() ?? Array.Empty<PveClusterConfigNode>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveClusterJoinInfo>() ?? new PveClusterJoinInfo();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<int>() ?? 0;
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveClusterOptions>() ?? new PveClusterOptions();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveClusterStatus[]>() ?? Array.Empty<PveClusterStatus>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
}
}
@@ -9,10 +9,8 @@ namespace PSProxmoxVE.Core.Services
/// <summary>
/// Service for Proxmox VE cluster-level API operations.
/// </summary>
public class ClusterService
public class ClusterService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>
/// Initializes a new instance of the <see cref="ClusterService"/> class.
/// </summary>
@@ -22,10 +20,7 @@ namespace PSProxmoxVE.Core.Services
/// Initializes a new instance of the <see cref="ClusterService"/> class with an injected HTTP client.
/// </summary>
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
public ClusterService(IPveHttpClient client)
{
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
}
public ClusterService(IPveHttpClient client) : base(client) { }
/// <summary>
/// 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<PveClusterStatus[]>() ?? Array.Empty<PveClusterStatus>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveClusterResource[]>() ?? Array.Empty<PveClusterResource>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
}
}
+42 -125
View File
@@ -12,19 +12,21 @@ namespace PSProxmoxVE.Core.Services
/// <summary>
/// Service for Proxmox VE Linux Container (LXC) API operations.
/// </summary>
public class ContainerService
public class ContainerService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
private readonly NodeService _nodeService = new NodeService();
private readonly NodeService _nodeService;
/// <summary>Initializes a new instance that creates its own HTTP clients.</summary>
public ContainerService() { }
public ContainerService()
{
_nodeService = new NodeService();
}
/// <summary>Initializes a new instance that uses the supplied HTTP client for all requests.</summary>
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
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<PveContainer[]>() ?? Array.Empty<PveContainer>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveContainerConfig>() ?? 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<PveSnapshot[]>() ?? Array.Empty<PveSnapshot>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>Starts a container. Returns the task UPID.</summary>
@@ -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();
}
});
}
/// <summary>Removes a container. Returns the task UPID.</summary>
@@ -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();
}
});
}
/// <summary>Clones a container. Returns the task UPID.</summary>
@@ -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();
}
});
}
/// <summary>Migrates a container to another node. Returns the task UPID.</summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveContainerInterface[]>() ?? Array.Empty<PveContainerInterface>();
}
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)
+52 -182
View File
@@ -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.
/// </summary>
public class FirewallService
public class FirewallService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>Initializes a new instance that creates its own HTTP clients.</summary>
public FirewallService() { }
/// <summary>Initializes a new instance that uses the supplied HTTP client for all requests.</summary>
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
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<PveFirewallRule[]>() ?? Array.Empty<PveFirewallRule>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveFirewallGroup[]>() ?? Array.Empty<PveFirewallGroup>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveFirewallRule[]>() ?? Array.Empty<PveFirewallRule>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveFirewallAlias[]>() ?? Array.Empty<PveFirewallAlias>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveFirewallIpSet[]>() ?? Array.Empty<PveFirewallIpSet>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveFirewallIpSetEntry[]>() ?? Array.Empty<PveFirewallIpSetEntry>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveFirewallOptions>() ?? new PveFirewallOptions();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveFirewallRef[]>() ?? Array.Empty<PveFirewallRef>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
// -------------------------------------------------------------------------
+40 -140
View File
@@ -11,10 +11,8 @@ namespace PSProxmoxVE.Core.Services
/// <summary>
/// Service for Proxmox VE High Availability (HA) API operations.
/// </summary>
public class HaService
public class HaService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>
/// Initializes a new instance of the <see cref="HaService"/> class.
/// </summary>
@@ -24,10 +22,7 @@ namespace PSProxmoxVE.Core.Services
/// Initializes a new instance of the <see cref="HaService"/> class with an injected HTTP client.
/// </summary>
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
public HaService(IPveHttpClient client)
{
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
}
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<PveHaResource[]>() ?? Array.Empty<PveHaResource>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveHaResource>() ?? new PveHaResource();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -91,15 +76,10 @@ namespace PSProxmoxVE.Core.Services
var formData = new Dictionary<string, string>(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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -162,19 +132,14 @@ namespace PSProxmoxVE.Core.Services
var formData = new Dictionary<string, string> { ["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();
}
});
}
/// <summary>
@@ -191,19 +156,14 @@ namespace PSProxmoxVE.Core.Services
var formData = new Dictionary<string, string> { ["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<PveHaGroup[]>() ?? Array.Empty<PveHaGroup>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveHaGroup>() ?? new PveHaGroup();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveHaStatus[]>() ?? Array.Empty<PveHaStatus>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveHaRule[]>() ?? Array.Empty<PveHaRule>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveHaRule>() ?? new PveHaRule();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
}
}
+62 -217
View File
@@ -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.
/// </summary>
public class NetworkService
public class NetworkService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>Initializes a new instance that creates its own HTTP clients.</summary>
public NetworkService() { }
/// <summary>Initializes a new instance that uses the supplied HTTP client for all requests.</summary>
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
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<PveNetwork[]>() ?? Array.Empty<PveNetwork>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveNetwork>() ?? new PveNetwork();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveSdnZone[]>() ?? Array.Empty<PveSdnZone>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveSdnVnet[]>() ?? Array.Empty<PveSdnVnet>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveSdnZone>() ?? new PveSdnZone();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveSdnVnet>() ?? new PveSdnVnet();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveSdnSubnet[]>() ?? Array.Empty<PveSdnSubnet>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveSdnIpam[]>() ?? Array.Empty<PveSdnIpam>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveSdnDns[]>() ?? Array.Empty<PveSdnDns>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveSdnController[]>() ?? Array.Empty<PveSdnController>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<string, string>())
.GetAwaiter().GetResult();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
// -------------------------------------------------------------------------
+20 -70
View File
@@ -13,10 +13,8 @@ namespace PSProxmoxVE.Core.Services
/// <summary>
/// Service for node-level and cluster-version Proxmox VE API operations.
/// </summary>
public class NodeService
public class NodeService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>
/// Initializes a new instance of <see cref="NodeService"/> with no injected client.
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
@@ -28,10 +26,7 @@ namespace PSProxmoxVE.Core.Services
/// The caller owns the client's lifetime; this service will not dispose it.
/// </summary>
/// <param name="client">The HTTP client to use for all requests.</param>
public NodeService(IPveHttpClient client)
{
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
}
public NodeService(IPveHttpClient client) : base(client) { }
/// <summary>
/// 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<PveNode[]>() ?? Array.Empty<PveNode>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveNodeStatus>() ?? new PveNodeStatus();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -178,16 +143,11 @@ namespace PSProxmoxVE.Core.Services
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
var formData = config ?? new Dictionary<string, string>();
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();
}
});
}
/// <summary>
@@ -202,16 +162,11 @@ namespace PSProxmoxVE.Core.Services
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
var formData = config ?? new Dictionary<string, string>();
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();
}
});
}
/// <summary>
@@ -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();
}
});
}
// -------------------------------------------------------------------------
+12 -42
View File
@@ -10,10 +10,8 @@ namespace PSProxmoxVE.Core.Services
/// <summary>
/// Service for Proxmox VE resource pool API operations.
/// </summary>
public class PoolService
public class PoolService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>
/// Initializes a new instance of <see cref="PoolService"/> with no injected client.
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
@@ -25,10 +23,7 @@ namespace PSProxmoxVE.Core.Services
/// The caller owns the client's lifetime; this service will not dispose it.
/// </summary>
/// <param name="client">The HTTP client to use for all requests.</param>
public PoolService(IPveHttpClient client)
{
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
}
public PoolService(IPveHttpClient client) : base(client) { }
/// <summary>
/// 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<PvePool[]>() ?? Array.Empty<PvePool>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PvePool>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<string, string> { { "poolid", poolId } };
if (!string.IsNullOrEmpty(comment))
config["comment"] = comment!;
client.PostAsync("pools", config).GetAwaiter().GetResult();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
}
}
@@ -0,0 +1,86 @@
using System;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
namespace PSProxmoxVE.Core.Services
{
/// <summary>
/// Common base for the API services: owns the optional injected <see cref="IPveHttpClient"/>
/// 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.
/// </summary>
public abstract class PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>Initializes a service that opens its own HTTP client per call.</summary>
private protected PveServiceBase() { }
/// <summary>Initializes a service that uses the supplied HTTP client for every call.</summary>
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
private protected PveServiceBase(IPveHttpClient client)
{
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
}
/// <summary>
/// Runs <paramref name="action"/> against the injected client, or against a client
/// opened for this call and disposed when it returns.
/// </summary>
/// <typeparam name="T">The result type.</typeparam>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="action">The work to run against the client.</param>
private protected T Invoke<T>(PveSession session, Func<IPveHttpClient, T> action) =>
Invoke(session, timeoutOverride: null, action);
/// <summary>
/// Runs <paramref name="action"/> against the injected client, or against a client
/// opened for this call and disposed when it returns.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="action">The work to run against the client.</param>
private protected void Invoke(PveSession session, Action<IPveHttpClient> action)
{
if (action == null) throw new ArgumentNullException(nameof(action));
Invoke<object?>(session, timeoutOverride: null, client => { action(client); return null; });
}
/// <summary>
/// Runs <paramref name="action"/> against the injected client, or against a client
/// opened for this call with <paramref name="timeoutOverride"/> applied and disposed
/// when it returns. The override is ignored for an injected client.
/// </summary>
/// <typeparam name="T">The result type.</typeparam>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="timeoutOverride">Per-call request timeout for a client opened here.</param>
/// <param name="action">The work to run against the client.</param>
private protected T Invoke<T>(PveSession session, TimeSpan? timeoutOverride, Func<IPveHttpClient, T> 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();
}
}
/// <summary>
/// Test seam: builds the per-call client. Production services always get a
/// <see cref="PveHttpClient"/> for <paramref name="session"/>.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="timeoutOverride">Per-call request timeout, or null for the session's.</param>
internal virtual IPveHttpClient CreateClient(PveSession session, TimeSpan? timeoutOverride) =>
new PveHttpClient(session, timeoutOverride);
}
}
@@ -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.
/// </summary>
public class SnapshotService
public class SnapshotService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>
/// Initializes a new instance of the <see cref="SnapshotService"/> class.
/// </summary>
@@ -24,10 +22,7 @@ namespace PSProxmoxVE.Core.Services
/// Initializes a new instance of the <see cref="SnapshotService"/> class with an injected HTTP client.
/// </summary>
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
public SnapshotService(IPveHttpClient client)
{
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
}
public SnapshotService(IPveHttpClient client) : base(client) { }
/// <summary>
/// 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<PveSnapshot[]>() ?? Array.Empty<PveSnapshot>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
// -------------------------------------------------------------------------
+24 -84
View File
@@ -12,10 +12,8 @@ namespace PSProxmoxVE.Core.Services
/// <summary>
/// Service for Proxmox VE storage API operations.
/// </summary>
public class StorageService
public class StorageService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>
/// Initializes a new instance of <see cref="StorageService"/> with no injected client.
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
@@ -27,10 +25,7 @@ namespace PSProxmoxVE.Core.Services
/// The caller owns the client's lifetime; this service will not dispose it.
/// </summary>
/// <param name="client">The HTTP client to use for all requests.</param>
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<PveStorage[]>() ?? Array.Empty<PveStorage>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveStorageContent[]>() ?? Array.Empty<PveStorageContent>();
}
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();
}
});
}
/// <summary>
@@ -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<PveStorage>() ?? new PveStorage();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveStorageStatus>() ?? new PveStorageStatus();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
// -------------------------------------------------------------------------
+90 -58
View File
@@ -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
/// <summary>
/// Service for querying and waiting on Proxmox VE asynchronous tasks (UPIDs).
/// </summary>
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<TimeSpan, Task> _pollDelay;
/// <summary>Initializes a new instance that creates its own HTTP clients.</summary>
public TaskService() { }
public TaskService() : this(Sleep) { }
/// <summary>Initializes a new instance that uses the supplied HTTP client for all requests.</summary>
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
public TaskService(IPveHttpClient client)
public TaskService(IPveHttpClient client) : this(client, Sleep) { }
/// <summary>
/// Test seam: same as <see cref="TaskService()"/> but with the wait between polls
/// replaceable, so a test can assert the poll schedule without sleeping for it.
/// </summary>
/// <param name="pollDelay">Invoked with each computed poll interval instead of sleeping.</param>
internal TaskService(Func<TimeSpan, Task> pollDelay)
{
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
_pollDelay = pollDelay ?? throw new ArgumentNullException(nameof(pollDelay));
}
/// <summary>
/// Test seam: same as <see cref="TaskService(IPveHttpClient)"/> but with the wait between
/// polls replaceable, so a test can assert the poll schedule without sleeping for it.
/// </summary>
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
/// <param name="pollDelay">Invoked with each computed poll interval instead of sleeping.</param>
internal TaskService(IPveHttpClient client, Func<TimeSpan, Task> pollDelay) : base(client)
{
_pollDelay = pollDelay ?? throw new ArgumentNullException(nameof(pollDelay));
}
private static Task Sleep(TimeSpan duration)
{
Thread.Sleep(duration);
return Task.CompletedTask;
}
/// <summary>
/// Returns the current status of a task identified by its UPID.
/// </summary>
@@ -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<PveTask>() ?? 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<PveTask>() ?? new PveTask { Upid = upid };
task.Node = node;
return task;
}
/// <summary>
@@ -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<PveTaskLog[]>() ?? Array.Empty<PveTaskLog>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
/// 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.
/// </summary>
/// <param name="session">Active PVE session.</param>
/// <param name="node">Node name where the task is running.</param>
/// <param name="upid">Task UPID.</param>
/// <param name="timeout">Maximum time to wait. Defaults to 10 minutes.</param>
/// <param name="pollInterval">Interval between status polls. Defaults to 2 seconds. Minimum 1 second.</param>
/// <param name="pollInterval">
/// 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 <paramref name="timeout"/>.
/// </param>
/// <param name="progressCallback">Optional callback invoked on each poll with the current task.</param>
/// <returns>The completed <see cref="PveTask"/>.</returns>
/// <exception cref="PveTaskTimeoutException">Thrown when the task does not complete within <paramref name="timeout"/>.</exception>
@@ -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);
}
});
}
/// <summary>
@@ -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<string> { $"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();
}
});
}
/// <summary>
@@ -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();
}
});
}
}
}
@@ -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.
/// </summary>
public class TemplateService
public class TemplateService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
private readonly VmService _vmService = new VmService();
private readonly VmService _vmService;
/// <summary>
/// Initializes a new instance of the <see cref="TemplateService"/> class.
/// </summary>
public TemplateService() { }
public TemplateService()
{
_vmService = new VmService();
}
/// <summary>
/// Initializes a new instance of the <see cref="TemplateService"/> class with an injected HTTP client.
/// </summary>
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
public TemplateService(IPveHttpClient client)
public TemplateService(IPveHttpClient client) : base(client)
{
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
_vmService = new VmService(client);
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
+50 -175
View File
@@ -11,10 +11,8 @@ namespace PSProxmoxVE.Core.Services
/// <summary>
/// Service for Proxmox VE access control — users, roles, and permissions (ACLs).
/// </summary>
public class UserService
public class UserService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
/// <summary>
/// Initializes a new instance of <see cref="UserService"/> with no injected client.
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
@@ -26,10 +24,7 @@ namespace PSProxmoxVE.Core.Services
/// The caller owns the client's lifetime; this service will not dispose it.
/// </summary>
/// <param name="client">The HTTP client to use for all requests.</param>
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<PveUser[]>() ?? Array.Empty<PveUser>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>Returns a single user by their user ID (e.g. "admin@pam").</summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>Removes a user account.</summary>
@@ -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();
}
});
}
/// <summary>Updates one or more properties of an existing user.</summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>Removes an API token.</summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveRole[]>() ?? Array.Empty<PveRole>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>Creates a new role.</summary>
@@ -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();
}
});
}
/// <summary>Removes a role.</summary>
@@ -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();
}
});
}
/// <summary>
@@ -367,16 +302,11 @@ namespace PSProxmoxVE.Core.Services
if (string.IsNullOrWhiteSpace(privileges)) throw new ArgumentNullException(nameof(privileges));
var formData = new Dictionary<string, string> { ["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<PveGroup[]>() ?? Array.Empty<PveGroup>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>Creates a new group.</summary>
@@ -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();
}
});
}
/// <summary>Updates a group's properties.</summary>
@@ -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();
}
});
}
/// <summary>Removes a group.</summary>
@@ -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<PveDomain[]>() ?? Array.Empty<PveDomain>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>Creates a new authentication domain/realm.</summary>
@@ -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();
}
});
}
/// <summary>Updates an authentication domain/realm.</summary>
@@ -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();
}
});
}
/// <summary>Removes an authentication domain/realm.</summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
}
}
+70 -195
View File
@@ -13,19 +13,21 @@ namespace PSProxmoxVE.Core.Services
/// <summary>
/// Service for Proxmox VE QEMU/KVM virtual machine API operations.
/// </summary>
public class VmService
public class VmService : PveServiceBase
{
private readonly IPveHttpClient? _injectedClient;
private readonly NodeService _nodeService = new NodeService();
private readonly NodeService _nodeService;
/// <summary>Initializes a new instance that creates its own HTTP clients.</summary>
public VmService() { }
public VmService()
{
_nodeService = new NodeService();
}
/// <summary>Initializes a new instance that uses the supplied HTTP client for all requests.</summary>
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
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<PveVm[]>() ?? Array.Empty<PveVm>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<int?>() ?? vm.CpuCount;
vm.MaxMem = data["maxmem"]?.ToObject<long?>() ?? vm.MaxMem;
vm.MaxDisk = data["maxdisk"]?.ToObject<long?>() ?? vm.MaxDisk;
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveVmConfig>() ?? 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();
}
});
}
/// <summary>Starts a VM. Returns the task UPID.</summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>Resets a VM (hard reset). Returns the task UPID.</summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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;
}
});
}
/// <summary>
@@ -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<PveGuestNetworkInterface[]>() ?? Array.Empty<PveGuestNetworkInterface>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<int>() ?? 0;
return pid;
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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<PveGuestOsInfo>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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<PveGuestFsInfo[]>() ?? Array.Empty<PveGuestFsInfo>();
}
finally
{
if (_injectedClient == null) client.Dispose();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
/// <summary>
@@ -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();
}
});
}
}
}
@@ -45,7 +45,8 @@ namespace PSProxmoxVE.Cmdlets.Tasks
public TimeSpan? Timeout { get; set; }
/// <summary>
/// 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)
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "How often to poll task status.")]
@@ -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<HttpResponseMessage> 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<CannedHandler> built) NewCache()
{
var built = new List<CannedHandler>();
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));
}
}
}
@@ -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
// ---------------------------------------------------------------------
/// <summary>
/// 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.
/// </summary>
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<IPveHttpClient> SetupTwoNodeCluster()
{
var mockClient = new Mock<IPveHttpClient>();
@@ -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<string>();
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<PveApiException>(() => 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<string>();
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<string>();
var containers = service.GetContainers(CreateSession(), onNodeSkipped: (node, ex) => skipped.Add(node));
@@ -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<string> GetAsync(string resource) { Gets++; return Task.FromResult("{\"data\":null}"); }
public Task<string> PostAsync(string resource, Dictionary<string, string>? data = null) => throw new NotSupportedException();
public Task<string> PostAsync(string resource, IEnumerable<KeyValuePair<string, string>> data) => throw new NotSupportedException();
public Task<string> PutAsync(string resource, Dictionary<string, string>? data = null) => throw new NotSupportedException();
public Task<string> DeleteAsync(string resource) => throw new NotSupportedException();
public string Get(string resource) => GetAsync(resource).GetAwaiter().GetResult();
public string Post(string resource, Dictionary<string, string>? data = null) => throw new NotSupportedException();
public string Put(string resource, Dictionary<string, string>? data = null) => throw new NotSupportedException();
public string Delete(string resource) => throw new NotSupportedException();
public Task<string> UploadFileAsync(string resource, string filePath, Dictionary<string, string>? formFields = null,
string? checksum = null, string? checksumAlgorithm = null, Action<long, long>? 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<string>(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<InvalidOperationException>(() => 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<ArgumentNullException>(() => 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);
}
}
}
@@ -336,6 +336,129 @@ namespace PSProxmoxVE.Core.Tests.Services
Assert.Equal(new List<string?> { "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<IPveHttpClient> client, List<TimeSpan> delays) ServiceWithRecordedDelays(int runningPolls)
{
var mockClient = new Mock<IPveHttpClient>();
var sequence = mockClient.SetupSequence(c => c.GetAsync(It.IsAny<string>()));
for (var i = 0; i < runningPolls; i++)
sequence = sequence.ReturnsAsync(RunningJson);
sequence.ReturnsAsync(StoppedJson);
var delays = new List<TimeSpan>();
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<string>()), 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<IPveHttpClient>();
mockClient.Setup(c => c.GetAsync(It.IsAny<string>())).ReturnsAsync(RunningJson);
var delays = new List<TimeSpan>();
var service = new TaskService(mockClient.Object, d => { delays.Add(d); return Task.CompletedTask; });
var timeout = TimeSpan.FromMilliseconds(300);
Assert.Throws<PveTaskTimeoutException>(() =>
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<string>()), 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<Mock<IPveHttpClient>> Built = new List<Mock<IPveHttpClient>>();
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<IPveHttpClient>();
var sequence = mock.SetupSequence(c => c.GetAsync(It.IsAny<string>()));
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<string>()), Times.Exactly(5));
client.Verify(c => c.Dispose(), Times.Once);
}
[Fact]
public void StopTask_CallsDeleteAsyncWithCorrectPath()
{
@@ -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
// ---------------------------------------------------------------------
/// <summary>
/// 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.
/// </summary>
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<IPveHttpClient> SetupTwoNodeCluster()
{
var mockClient = new Mock<IPveHttpClient>();
@@ -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<string>();
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<PveApiException>(() => 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<string>();
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<string>();
var vms = service.GetVms(CreateSession(), onNodeSkipped: (node, ex) => skipped.Add(node));