fix: retry re-auth during cluster join and always reconnect to node A

- Extract re-auth retry into ReauthenticateWithRetry helper (fixes
  cognitive complexity warning)
- Retry auth up to 10x with 3s delay — node B's auth services need
  time to restart after joining the cluster
- Wrap join in try/finally so the test always reconnects to node A,
  even if the join fails

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-25 08:47:17 -05:00
parent 8c131a0609
commit 4277ad73b9
2 changed files with 78 additions and 35 deletions
@@ -3,6 +3,7 @@ using System.Management.Automation;
using System.Net;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Exceptions;
using PSProxmoxVE.Core.Models.Vms;
@@ -57,6 +58,9 @@ namespace PSProxmoxVE.Cmdlets.Cluster
[Parameter(Mandatory = false, HelpMessage = "Wait for the join task to complete before returning.")]
public SwitchParameter Wait { get; set; }
private const int ReauthMaxAttempts = 10;
private const int ReauthDelayMs = 3000;
protected override void ProcessRecord()
{
if (!ShouldProcess($"this node to cluster via '{Hostname}'", "Join cluster"))
@@ -81,22 +85,7 @@ namespace PSProxmoxVE.Cmdlets.Cluster
if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
{
var nodeName = upid.Split(':').Length > 1 ? upid.Split(':')[1] : session.Hostname;
var taskService = new TaskService();
try
{
task = taskService.WaitForTask(session, nodeName, upid);
}
catch (PveApiException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
// Cluster join restarts auth services on this node, invalidating
// the ticket mid-poll. Re-authenticate and retry.
WriteVerbose("Session expired during join — re-authenticating to poll task status...");
var newSession = PveAuthenticator.AuthenticateWithCredentials(
session.Hostname, session.Port, session.SkipCertificateCheck,
"root@pam", plainPassword);
task = taskService.WaitForTask(newSession, nodeName, upid);
}
task = WaitForJoinTask(session, upid, plainPassword);
}
WriteObject(task);
@@ -107,5 +96,55 @@ namespace PSProxmoxVE.Cmdlets.Cluster
Marshal.ZeroFreeGlobalAllocUnicode(ptr);
}
}
/// <summary>
/// Waits for the join task to complete. If the ticket is invalidated mid-poll
/// (cluster join restarts auth services), re-authenticates with retry and
/// continues waiting.
/// </summary>
private PveTask WaitForJoinTask(PveSession session, string upid, string plainPassword)
{
var nodeName = upid.Split(':').Length > 1 ? upid.Split(':')[1] : session.Hostname;
var taskService = new TaskService();
try
{
return taskService.WaitForTask(session, nodeName, upid);
}
catch (PveApiException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
WriteVerbose("Session expired during join — waiting for auth services to restart...");
var newSession = ReauthenticateWithRetry(session, plainPassword);
return taskService.WaitForTask(newSession, nodeName, upid);
}
}
/// <summary>
/// Retries authentication against this node until auth services come back online
/// after the cluster join restarts them.
/// </summary>
private PveSession ReauthenticateWithRetry(PveSession session, string plainPassword)
{
for (int attempt = 0; attempt < ReauthMaxAttempts; attempt++)
{
Thread.Sleep(ReauthDelayMs);
try
{
return PveAuthenticator.AuthenticateWithCredentials(
session.Hostname, session.Port, session.SkipCertificateCheck,
"root@pam", plainPassword);
}
catch (PveApiException retryEx) when (
retryEx.StatusCode == HttpStatusCode.Unauthorized ||
retryEx.StatusCode == HttpStatusCode.ServiceUnavailable)
{
WriteVerbose($"Re-auth attempt {attempt + 1}/{ReauthMaxAttempts} failed, retrying...");
}
}
throw new InvalidOperationException(
"Unable to re-authenticate after cluster join. " +
"Auth services on this node may still be restarting.");
}
}
}