feat: add -Timeout parameter with state polling to lifecycle cmdlets

All VM and container lifecycle cmdlets (Start, Stop, Restart, Suspend,
Resume, Reset) now poll VM/container status when -Wait is specified,
blocking until the expected state is reached or -Timeout (default 60s)
is exceeded.

Implementation:
- PveCmdletBase.WaitForStatusTransition() — shared helper that waits
  for PVE task completion then polls status via API
- 9 cmdlets updated: 6 VM (Start, Stop, Restart, Suspend, Resume,
  Reset) + 3 container (Start, Stop, Restart)
- -Timeout parameter with [ValidateRange(1, 3600)] on each

Integration tests:
- Replace all manual Start-Sleep + polling loops with -Wait -Timeout
- Switch to real Ubuntu cloud OVA (571 MB) for Import-PveOva testing
- OVA test verifies full import + VM start

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-20 15:15:30 -05:00
parent d1eb674c92
commit 2b772ebc65
12 changed files with 185 additions and 171 deletions
@@ -33,10 +33,11 @@ namespace PSProxmoxVE.Cmdlets.Containers
/// <summary>
/// <para type="description">
/// Timeout in seconds for the graceful shutdown phase. Defaults to 60 seconds.
/// Timeout in seconds for the graceful shutdown phase and -Wait status polling. Defaults to 60 seconds.
/// </para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Maximum time to wait for the task.")]
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
/// <summary>
@@ -52,7 +53,6 @@ namespace PSProxmoxVE.Cmdlets.Containers
var session = GetSession();
var containerService = new ContainerService();
var taskService = new TaskService();
WriteVerbose($"Restarting container {VmId} on node '{Node}'...");
@@ -60,13 +60,13 @@ namespace PSProxmoxVE.Cmdlets.Containers
var shutdownTask = containerService.ShutdownContainer(session, Node, VmId, Timeout);
if (Wait.IsPresent)
taskService.WaitForTask(session, shutdownTask.Node ?? Node, shutdownTask.Upid!, null, null, null);
WaitForStatusTransition(session, Node, shutdownTask, VmId, "stopped", Timeout, isContainer: true);
// Start
var startTask = containerService.StartContainer(session, Node, VmId);
if (Wait.IsPresent)
startTask = taskService.WaitForTask(session, startTask.Node ?? Node, startTask.Upid!, null, null, null);
startTask = WaitForStatusTransition(session, Node, startTask, VmId, "running", Timeout, isContainer: true);
WriteObject(startTask);
}
@@ -36,6 +36,11 @@ namespace PSProxmoxVE.Cmdlets.Containers
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
/// <summary>Maximum seconds to wait for the status transition when -Wait is specified. Default 60.</summary>
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
protected override void ProcessRecord()
{
if (!ShouldProcess($"Container {VmId} on node '{Node}'", "Start-PveContainer"))
@@ -49,8 +54,7 @@ namespace PSProxmoxVE.Cmdlets.Containers
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, task.Node ?? Node, task.Upid!, null, null, null);
task = WaitForStatusTransition(session, Node, task, VmId, "running", Timeout, isContainer: true);
}
WriteObject(task);
@@ -38,6 +38,11 @@ namespace PSProxmoxVE.Cmdlets.Containers
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
/// <summary>Maximum seconds to wait for the status transition when -Wait is specified. Default 60.</summary>
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
protected override void ProcessRecord()
{
if (!ShouldProcess($"Container {VmId} on node '{Node}'", "Stop-PveContainer"))
@@ -51,8 +56,7 @@ namespace PSProxmoxVE.Cmdlets.Containers
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, task.Node ?? Node, task.Upid!, null, null, null);
task = WaitForStatusTransition(session, Node, task, VmId, "stopped", Timeout, isContainer: true);
}
WriteObject(task);
+67
View File
@@ -1,6 +1,9 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Exceptions;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets
{
@@ -35,5 +38,69 @@ namespace PSProxmoxVE.Cmdlets
return session;
}
/// <summary>
/// Waits for a PVE task to complete, then optionally polls VM status until
/// it matches <paramref name="expectedStatus"/>. Used by lifecycle cmdlets
/// (Start, Stop, Suspend, Resume, etc.) when -Wait is specified.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="task">The task returned by the lifecycle API call.</param>
/// <param name="vmid">The VM or container ID to poll.</param>
/// <param name="expectedStatus">The expected status string (e.g. "running", "stopped", "paused").</param>
/// <param name="timeoutSeconds">Maximum seconds to wait for the status transition. Default 60.</param>
/// <param name="isContainer">True to poll container status instead of VM status.</param>
/// <returns>The completed task.</returns>
protected PveTask WaitForStatusTransition(
PveSession session,
string node,
PveTask task,
int vmid,
string expectedStatus,
int timeoutSeconds = 60,
bool isContainer = false)
{
var taskService = new TaskService();
// First wait for the PVE task to complete
if (!string.IsNullOrEmpty(task.Upid))
{
task = taskService.WaitForTask(session, node, task.Upid, null, null, null);
}
// Then poll until VM/container reaches the expected status
var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
while (DateTime.UtcNow < deadline)
{
try
{
string? currentStatus;
if (isContainer)
{
var ct = new ContainerService().GetContainer(session, node, vmid);
currentStatus = ct.Status;
}
else
{
var vm = new VmService().GetVm(session, node, vmid);
currentStatus = vm.Status;
}
if (string.Equals(currentStatus, expectedStatus, StringComparison.OrdinalIgnoreCase))
return task;
}
catch
{
// Ignore transient errors during polling
}
System.Threading.Thread.Sleep(2000);
}
throw new PveTaskTimeoutException(
task.Upid ?? "unknown",
TimeSpan.FromSeconds(timeoutSeconds));
}
}
}
@@ -38,6 +38,11 @@ namespace PSProxmoxVE.Cmdlets.Vms
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
/// <summary>Maximum seconds to wait for the status transition when -Wait is specified. Default 60.</summary>
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
protected override void ProcessRecord()
{
if (!ShouldProcess($"VM {VmId} on node '{Node}'", "Reset-PveVm"))
@@ -51,8 +56,7 @@ namespace PSProxmoxVE.Cmdlets.Vms
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
task = WaitForStatusTransition(session, Node, task, VmId, "running", Timeout);
}
WriteObject(task);
@@ -33,10 +33,11 @@ namespace PSProxmoxVE.Cmdlets.Vms
/// <summary>
/// <para type="description">
/// Timeout in seconds for the graceful shutdown phase. Defaults to 60 seconds.
/// Timeout in seconds for the graceful shutdown phase and -Wait status polling. Defaults to 60 seconds.
/// </para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Maximum time to wait for the task.")]
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
/// <summary>
@@ -52,7 +53,6 @@ namespace PSProxmoxVE.Cmdlets.Vms
var session = GetSession();
var vmService = new VmService();
var taskService = new TaskService();
WriteVerbose($"Restarting VM {VmId} on node '{Node}'...");
@@ -60,13 +60,13 @@ namespace PSProxmoxVE.Cmdlets.Vms
var shutdownTask = vmService.ShutdownVm(session, Node, VmId, Timeout);
if (Wait.IsPresent)
taskService.WaitForTask(session, Node, shutdownTask.Upid, null, null, null);
WaitForStatusTransition(session, Node, shutdownTask, VmId, "stopped", Timeout);
// Start
var startTask = vmService.StartVm(session, Node, VmId);
if (Wait.IsPresent)
startTask = taskService.WaitForTask(session, Node, startTask.Upid, null, null, null);
startTask = WaitForStatusTransition(session, Node, startTask, VmId, "running", Timeout);
WriteObject(startTask);
}
@@ -36,6 +36,11 @@ namespace PSProxmoxVE.Cmdlets.Vms
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
/// <summary>Maximum seconds to wait for the status transition when -Wait is specified. Default 60.</summary>
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
protected override void ProcessRecord()
{
if (!ShouldProcess($"VM {VmId} on node '{Node}'", "Resume-PveVm"))
@@ -49,8 +54,7 @@ namespace PSProxmoxVE.Cmdlets.Vms
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
task = WaitForStatusTransition(session, Node, task, VmId, "running", Timeout);
}
WriteObject(task);
@@ -36,6 +36,11 @@ namespace PSProxmoxVE.Cmdlets.Vms
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
/// <summary>Maximum seconds to wait for the status transition when -Wait is specified. Default 60.</summary>
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
protected override void ProcessRecord()
{
if (!ShouldProcess($"VM {VmId} on node '{Node}'", "Start-PveVm"))
@@ -49,8 +54,7 @@ namespace PSProxmoxVE.Cmdlets.Vms
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
task = WaitForStatusTransition(session, Node, task, VmId, "running", Timeout);
}
WriteObject(task);
@@ -38,6 +38,11 @@ namespace PSProxmoxVE.Cmdlets.Vms
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
/// <summary>Maximum seconds to wait for the status transition when -Wait is specified. Default 60.</summary>
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
protected override void ProcessRecord()
{
if (!ShouldProcess($"VM {VmId} on node '{Node}'", "Stop-PveVm"))
@@ -51,8 +56,7 @@ namespace PSProxmoxVE.Cmdlets.Vms
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
task = WaitForStatusTransition(session, Node, task, VmId, "stopped", Timeout);
}
WriteObject(task);
@@ -37,6 +37,11 @@ namespace PSProxmoxVE.Cmdlets.Vms
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
/// <summary>Maximum seconds to wait for the status transition when -Wait is specified. Default 60.</summary>
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
[ValidateRange(1, 3600)]
public int Timeout { get; set; } = 60;
protected override void ProcessRecord()
{
if (!ShouldProcess($"VM {VmId} on node '{Node}'", "Suspend-PveVm"))
@@ -50,8 +55,7 @@ namespace PSProxmoxVE.Cmdlets.Vms
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
task = WaitForStatusTransition(session, Node, task, VmId, "paused", Timeout);
}
WriteObject(task);