mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-07 20:45:40 +00:00
fix: surface swallowed errors in status polling and per-node listing (#181)
* fix: surface swallowed errors in status polling and per-node listing WaitForStatusTransition's poll loop caught every exception except OOM/ StackOverflow and discarded it silently. An expired ticket, a deleted VM, or a wrong node name under -Wait -Timeout spun for the full timeout and then raised PveTaskTimeoutException instead of the real 401/403/404. The loop now catches only PveApiException (excluding 401/403/404, which propagate) and HttpRequestException, and WriteVerbose's what it swallows. VmService.GetVms and ContainerService.GetContainers caught PveApiException of any status per node and continued, so a permission problem or an unreachable node looked identical to "no VMs". The per-node catch now narrows to a 5xx/408/connectivity failure (IsNodeUnreachable) and takes an optional onNodeSkipped callback; Get-PveVm and Get-PveContainer wire it to WriteWarning. Any other status (401/403/404 included) propagates. Adds xUnit coverage for the per-node aggregation path (403 propagates, 500/ 408/connectivity failures are skipped and reported, the other nodes' results still come back), reaching NodeService's internal client via reflection since it is not otherwise constructor-injectable from VmService/ ContainerService. Closes #142 * test: add per-node aggregation coverage and wire onNodeSkipped Adds the remaining changes: VmService/ContainerService per-node catch narrowing plus onNodeSkipped callback, the Get-PveVm/Get-PveContainer WriteWarning wiring, and the xUnit coverage for the aggregation loop. --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
8a82146acc
commit
0ec75a02e7
@@ -34,7 +34,13 @@ namespace PSProxmoxVE.Core.Services
|
||||
/// <summary>
|
||||
/// Returns containers. If <paramref name="node"/> is null, queries every cluster node.
|
||||
/// </summary>
|
||||
public PveContainer[] GetContainers(PveSession session, string? node = null)
|
||||
/// <param name="onNodeSkipped">
|
||||
/// Optional callback invoked with the node name and the exception when a node is
|
||||
/// skipped because it is unreachable (connectivity failure or a 5xx from that node).
|
||||
/// A 401/403/404 or any other non-5xx <see cref="PSProxmoxVE.Core.Exceptions.PveApiException"/>
|
||||
/// propagates instead of being swallowed.
|
||||
/// </param>
|
||||
public PveContainer[] GetContainers(PveSession session, string? node = null, Action<string, Exception>? onNodeSkipped = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
@@ -52,14 +58,31 @@ namespace PSProxmoxVE.Core.Services
|
||||
ct.Node ??= n.Name;
|
||||
all.AddRange(containers);
|
||||
}
|
||||
catch (Exception ex) when (ex is PSProxmoxVE.Core.Exceptions.PveApiException or System.Net.Http.HttpRequestException)
|
||||
catch (Exception ex) when (IsNodeUnreachable(ex))
|
||||
{
|
||||
// Skip offline/inaccessible nodes
|
||||
onNodeSkipped?.Invoke(n.Name, ex);
|
||||
}
|
||||
}
|
||||
return all.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True for a connectivity failure or a 5xx PVE API response — the cases where the
|
||||
/// node itself is unreachable rather than the request being rejected. A 401/403/404
|
||||
/// (or any other non-5xx status) means the request was understood and refused, which
|
||||
/// is not something a per-node listing loop should hide.
|
||||
/// </summary>
|
||||
private static bool IsNodeUnreachable(Exception ex) => ex switch
|
||||
{
|
||||
System.Net.Http.HttpRequestException => true,
|
||||
// PveHttpClient wraps a connection failure as 503 and a client-side timeout as
|
||||
// 408 (Client/PveHttpClient.cs SendOnceAsync) — both mean the node did not answer,
|
||||
// not that it rejected the request.
|
||||
PSProxmoxVE.Core.Exceptions.PveApiException apiEx =>
|
||||
apiEx.StatusCode == System.Net.HttpStatusCode.RequestTimeout || (int)apiEx.StatusCode >= 500,
|
||||
_ => false
|
||||
};
|
||||
|
||||
private PveContainer[] GetContainersOnNode(PveSession session, string node)
|
||||
{
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
|
||||
@@ -37,7 +37,13 @@ namespace PSProxmoxVE.Core.Services
|
||||
/// </summary>
|
||||
/// <param name="session">The authenticated PVE session.</param>
|
||||
/// <param name="node">Optional cluster node name to filter VMs by node.</param>
|
||||
public PveVm[] GetVms(PveSession session, string? node = null)
|
||||
/// <param name="onNodeSkipped">
|
||||
/// Optional callback invoked with the node name and the exception when a node is
|
||||
/// skipped because it is unreachable (connectivity failure or a 5xx from that node).
|
||||
/// A 401/403/404 or any other non-5xx <see cref="PSProxmoxVE.Core.Exceptions.PveApiException"/>
|
||||
/// propagates instead of being swallowed.
|
||||
/// </param>
|
||||
public PveVm[] GetVms(PveSession session, string? node = null, Action<string, Exception>? onNodeSkipped = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
@@ -57,14 +63,31 @@ namespace PSProxmoxVE.Core.Services
|
||||
vm.Node ??= n.Name;
|
||||
all.AddRange(vms);
|
||||
}
|
||||
catch (Exception ex) when (ex is PSProxmoxVE.Core.Exceptions.PveApiException or System.Net.Http.HttpRequestException)
|
||||
catch (Exception ex) when (IsNodeUnreachable(ex))
|
||||
{
|
||||
// Skip nodes that are offline or inaccessible
|
||||
onNodeSkipped?.Invoke(n.Name, ex);
|
||||
}
|
||||
}
|
||||
return all.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True for a connectivity failure or a 5xx PVE API response — the cases where the
|
||||
/// node itself is unreachable rather than the request being rejected. A 401/403/404
|
||||
/// (or any other non-5xx status) means the request was understood and refused, which
|
||||
/// is not something a per-node listing loop should hide.
|
||||
/// </summary>
|
||||
private static bool IsNodeUnreachable(Exception ex) => ex switch
|
||||
{
|
||||
System.Net.Http.HttpRequestException => true,
|
||||
// PveHttpClient wraps a connection failure as 503 and a client-side timeout as
|
||||
// 408 (Client/PveHttpClient.cs SendOnceAsync) — both mean the node did not answer,
|
||||
// not that it rejected the request.
|
||||
PSProxmoxVE.Core.Exceptions.PveApiException apiEx =>
|
||||
apiEx.StatusCode == System.Net.HttpStatusCode.RequestTimeout || (int)apiEx.StatusCode >= 500,
|
||||
_ => false
|
||||
};
|
||||
|
||||
private PveVm[] GetVmsOnNode(PveSession session, string node)
|
||||
{
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
|
||||
@@ -59,7 +59,8 @@ namespace PSProxmoxVE.Cmdlets.Containers
|
||||
WriteVerbose("Getting containers...");
|
||||
var service = new ContainerService();
|
||||
|
||||
IEnumerable<PveContainer> containers = service.GetContainers(session, Node);
|
||||
IEnumerable<PveContainer> containers = service.GetContainers(session, Node,
|
||||
onNodeSkipped: (nodeName, ex) => WriteWarning($"Skipping node '{nodeName}': {ex.Message}"));
|
||||
|
||||
if (VmId.HasValue)
|
||||
containers = containers.Where(c => c.VmId == VmId.Value);
|
||||
|
||||
@@ -144,9 +144,16 @@ namespace PSProxmoxVE.Cmdlets
|
||||
if (snapshot.StatusMatched && !snapshot.Locked)
|
||||
return task;
|
||||
}
|
||||
catch (Exception ex) when (ex is not OutOfMemoryException and not StackOverflowException)
|
||||
catch (PSProxmoxVE.Core.Exceptions.PveApiException ex) when (
|
||||
ex.StatusCode != System.Net.HttpStatusCode.Unauthorized
|
||||
&& ex.StatusCode != System.Net.HttpStatusCode.Forbidden
|
||||
&& ex.StatusCode != System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
// Ignore transient errors during polling
|
||||
WriteVerbose($"Status poll failed, retrying: {ex.Message}");
|
||||
}
|
||||
catch (System.Net.Http.HttpRequestException ex)
|
||||
{
|
||||
WriteVerbose($"Status poll failed, retrying: {ex.Message}");
|
||||
}
|
||||
|
||||
System.Threading.Thread.Sleep(2000);
|
||||
|
||||
@@ -75,7 +75,8 @@ namespace PSProxmoxVE.Cmdlets.Vms
|
||||
WriteVerbose("Getting VMs...");
|
||||
var service = new VmService();
|
||||
|
||||
IEnumerable<PveVm> vms = service.GetVms(session, Node);
|
||||
IEnumerable<PveVm> vms = service.GetVms(session, Node,
|
||||
onNodeSkipped: (nodeName, ex) => WriteWarning($"Skipping node '{nodeName}': {ex.Message}"));
|
||||
|
||||
if (VmId.HasValue)
|
||||
vms = vms.Where(v => v.VmId == VmId.Value);
|
||||
|
||||
Reference in New Issue
Block a user