From 2b772ebc65b3efd7542ec3f018f3d91676382f2d Mon Sep 17 00:00:00 2001 From: Clint Branham Date: Fri, 20 Mar 2026 15:15:30 -0500 Subject: [PATCH] feat: add -Timeout parameter with state polling to lifecycle cmdlets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Containers/RestartPveContainerCmdlet.cs | 10 +- .../Containers/StartPveContainerCmdlet.cs | 8 +- .../Containers/StopPveContainerCmdlet.cs | 8 +- src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs | 67 ++++++++++ .../Cmdlets/Vms/ResetPveVmCmdlet.cs | 8 +- .../Cmdlets/Vms/RestartPveVmCmdlet.cs | 10 +- .../Cmdlets/Vms/ResumePveVmCmdlet.cs | 8 +- .../Cmdlets/Vms/StartPveVmCmdlet.cs | 8 +- .../Cmdlets/Vms/StopPveVmCmdlet.cs | 8 +- .../Cmdlets/Vms/SuspendPveVmCmdlet.cs | 8 +- .../Integration/Integration.Tests.ps1 | 123 +++++++++--------- .../scripts/prepare-test-environment.sh | 90 ++----------- 12 files changed, 185 insertions(+), 171 deletions(-) diff --git a/src/PSProxmoxVE/Cmdlets/Containers/RestartPveContainerCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/RestartPveContainerCmdlet.cs index c171f9d..c59caec 100644 --- a/src/PSProxmoxVE/Cmdlets/Containers/RestartPveContainerCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Containers/RestartPveContainerCmdlet.cs @@ -33,10 +33,11 @@ namespace PSProxmoxVE.Cmdlets.Containers /// /// - /// 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. /// /// - [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; /// @@ -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); } diff --git a/src/PSProxmoxVE/Cmdlets/Containers/StartPveContainerCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/StartPveContainerCmdlet.cs index 9bd21f7..c9f79a7 100644 --- a/src/PSProxmoxVE/Cmdlets/Containers/StartPveContainerCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Containers/StartPveContainerCmdlet.cs @@ -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; } + /// Maximum seconds to wait for the status transition when -Wait is specified. Default 60. + [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); diff --git a/src/PSProxmoxVE/Cmdlets/Containers/StopPveContainerCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/StopPveContainerCmdlet.cs index b964338..bf67201 100644 --- a/src/PSProxmoxVE/Cmdlets/Containers/StopPveContainerCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Containers/StopPveContainerCmdlet.cs @@ -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; } + /// Maximum seconds to wait for the status transition when -Wait is specified. Default 60. + [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); diff --git a/src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs b/src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs index f0f051d..58ab8cd 100644 --- a/src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs +++ b/src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs @@ -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; } + + /// + /// Waits for a PVE task to complete, then optionally polls VM status until + /// it matches . Used by lifecycle cmdlets + /// (Start, Stop, Suspend, Resume, etc.) when -Wait is specified. + /// + /// The authenticated PVE session. + /// The cluster node name. + /// The task returned by the lifecycle API call. + /// The VM or container ID to poll. + /// The expected status string (e.g. "running", "stopped", "paused"). + /// Maximum seconds to wait for the status transition. Default 60. + /// True to poll container status instead of VM status. + /// The completed task. + 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)); + } } } diff --git a/src/PSProxmoxVE/Cmdlets/Vms/ResetPveVmCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/ResetPveVmCmdlet.cs index d98a4f2..305e96d 100644 --- a/src/PSProxmoxVE/Cmdlets/Vms/ResetPveVmCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Vms/ResetPveVmCmdlet.cs @@ -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; } + /// Maximum seconds to wait for the status transition when -Wait is specified. Default 60. + [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); diff --git a/src/PSProxmoxVE/Cmdlets/Vms/RestartPveVmCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/RestartPveVmCmdlet.cs index 03b6089..316a0e6 100644 --- a/src/PSProxmoxVE/Cmdlets/Vms/RestartPveVmCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Vms/RestartPveVmCmdlet.cs @@ -33,10 +33,11 @@ namespace PSProxmoxVE.Cmdlets.Vms /// /// - /// 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. /// /// - [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; /// @@ -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); } diff --git a/src/PSProxmoxVE/Cmdlets/Vms/ResumePveVmCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/ResumePveVmCmdlet.cs index c17f56c..9f5d4aa 100644 --- a/src/PSProxmoxVE/Cmdlets/Vms/ResumePveVmCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Vms/ResumePveVmCmdlet.cs @@ -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; } + /// Maximum seconds to wait for the status transition when -Wait is specified. Default 60. + [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); diff --git a/src/PSProxmoxVE/Cmdlets/Vms/StartPveVmCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/StartPveVmCmdlet.cs index 42da0ac..cd181a6 100644 --- a/src/PSProxmoxVE/Cmdlets/Vms/StartPveVmCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Vms/StartPveVmCmdlet.cs @@ -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; } + /// Maximum seconds to wait for the status transition when -Wait is specified. Default 60. + [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); diff --git a/src/PSProxmoxVE/Cmdlets/Vms/StopPveVmCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/StopPveVmCmdlet.cs index ba7248c..0b3dcb9 100644 --- a/src/PSProxmoxVE/Cmdlets/Vms/StopPveVmCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Vms/StopPveVmCmdlet.cs @@ -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; } + /// Maximum seconds to wait for the status transition when -Wait is specified. Default 60. + [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); diff --git a/src/PSProxmoxVE/Cmdlets/Vms/SuspendPveVmCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/SuspendPveVmCmdlet.cs index 5c6878a..402d4ac 100644 --- a/src/PSProxmoxVE/Cmdlets/Vms/SuspendPveVmCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Vms/SuspendPveVmCmdlet.cs @@ -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; } + /// Maximum seconds to wait for the status transition when -Wait is specified. Default 60. + [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); diff --git a/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 b/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 index 55a12da..fef673e 100644 --- a/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 @@ -412,10 +412,10 @@ Describe 'Integration Tests' -Tag 'Integration' { It 'Should start and stop a VM' { if (Skip-IfNoTestVm) { return } - $startTask = Start-PveVm -Node $script:Node -VmId $script:TestVmId -Wait + $startTask = Start-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Timeout 30 $startTask | Should -Not -BeNullOrEmpty - $stopTask = Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Confirm:$false + $stopTask = Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Timeout 30 -Confirm:$false $stopTask | Should -Not -BeNullOrEmpty } @@ -423,13 +423,13 @@ Describe 'Integration Tests' -Tag 'Integration' { if (Skip-IfNoTestVm) { return } # Start the VM first - Start-PveVm -Node $script:Node -VmId $script:TestVmId -Wait | Out-Null + Start-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Timeout 30 | Out-Null # Hard reset (no ACPI — works even without guest OS) - $task = Reset-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Confirm:$false + $task = Reset-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Timeout 30 -Confirm:$false $task | Should -Not -BeNullOrEmpty - Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Confirm:$false | Out-Null + Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Timeout 30 -Confirm:$false | Out-Null } It 'Should clone a VM' { @@ -488,7 +488,7 @@ Describe 'Integration Tests' -Tag 'Integration' { # Ensure the VM is stopped for snapshot $vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $script:TestVmId } if ($vm.Status -eq 'running') { - Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Confirm:$false | Out-Null + Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Timeout 30 -Confirm:$false | Out-Null } $snapName = 'pester-snap' @@ -625,7 +625,7 @@ Describe 'Integration Tests' -Tag 'Integration' { $task.IsSuccessful | Should -BeTrue # Clean up - Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Confirm:$false | Out-Null + Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Timeout 30 -Confirm:$false | Out-Null } } @@ -900,7 +900,7 @@ Describe 'Integration Tests' -Tag 'Integration' { It 'Should start a container (Start-PveContainer)' { if (Skip-IfNoTestContainer) { return } - $task = Start-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait + $task = Start-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Timeout 30 $task | Should -Not -BeNullOrEmpty $ct = Get-PveContainer -Node $script:Node -VmId $script:TestContainerId @@ -910,7 +910,7 @@ Describe 'Integration Tests' -Tag 'Integration' { It 'Should stop a container (Stop-PveContainer)' { if (Skip-IfNoTestContainer) { return } - $task = Stop-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Confirm:$false + $task = Stop-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Timeout 30 -Confirm:$false $task | Should -Not -BeNullOrEmpty $ct = Get-PveContainer -Node $script:Node -VmId $script:TestContainerId @@ -921,16 +921,16 @@ Describe 'Integration Tests' -Tag 'Integration' { if (Skip-IfNoTestContainer) { return } # Start first so we can restart - Start-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait | Out-Null + Start-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Timeout 30 | Out-Null - $task = Restart-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait + $task = Restart-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Timeout 30 $task | Should -Not -BeNullOrEmpty $ct = Get-PveContainer -Node $script:Node -VmId $script:TestContainerId $ct.Status | Should -Be 'running' # Stop for subsequent tests - Stop-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Confirm:$false | Out-Null + Stop-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Timeout 30 -Confirm:$false | Out-Null } It 'Should clone a container (Copy-PveContainer)' { @@ -964,7 +964,7 @@ Describe 'Integration Tests' -Tag 'Integration' { # Ensure container is stopped $ct = Get-PveContainer -Node $script:Node -VmId $script:TestContainerId if ($ct.Status -eq 'running') { - Stop-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Confirm:$false | Out-Null + Stop-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Timeout 30 -Confirm:$false | Out-Null } $task = New-PveContainerSnapshot ` @@ -1084,7 +1084,7 @@ Describe 'Integration Tests' -Tag 'Integration' { It 'Should start the Linux VM (Start-PveVm)' { if (Skip-IfNoLinuxVm) { return } - $task = Start-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait + $task = Start-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30 $task | Should -Not -BeNullOrEmpty } @@ -1152,33 +1152,19 @@ Describe 'Integration Tests' -Tag 'Integration' { It 'Should suspend and resume a running VM (Suspend-PveVm / Resume-PveVm)' { if (Skip-IfNoLinuxVm) { return } - # Suspend — sends QMP stop - { Suspend-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -ErrorAction Stop } | - Should -Not -Throw + # Suspend — -Wait -Timeout polls until status is 'paused' + $suspendTask = Suspend-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30 + $suspendTask | Should -Not -BeNullOrEmpty - # Poll for paused status (may take several seconds) - $paused = $false - for ($i = 0; $i -lt 10; $i++) { - Start-Sleep -Seconds 2 - $vm = Get-PveVm -Node $script:Node | - Where-Object { $_.VmId -eq $script:LinuxVmId } - if ($vm.Status -eq 'paused') { $paused = $true; break } - } - $paused | Should -BeTrue -Because 'VM should transition to paused within 20s' + $vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $script:LinuxVmId } + $vm.Status | Should -Be 'paused' - # Resume — sends QMP cont - { Resume-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -ErrorAction Stop } | - Should -Not -Throw + # Resume — -Wait -Timeout polls until status is 'running' + $resumeTask = Resume-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30 + $resumeTask | Should -Not -BeNullOrEmpty - # Poll for running status - $running = $false - for ($i = 0; $i -lt 10; $i++) { - Start-Sleep -Seconds 2 - $vm = Get-PveVm -Node $script:Node | - Where-Object { $_.VmId -eq $script:LinuxVmId } - if ($vm.Status -eq 'running') { $running = $true; break } - } - $running | Should -BeTrue -Because 'VM should return to running within 20s' + $vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $script:LinuxVmId } + $vm.Status | Should -Be 'running' } It 'Should gracefully restart a VM via ACPI (Restart-PveVm)' { @@ -1188,16 +1174,12 @@ Describe 'Integration Tests' -Tag 'Integration' { $vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $script:LinuxVmId } if ($vm.Status -eq 'paused') { - Resume-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -ErrorAction SilentlyContinue | Out-Null - Start-Sleep -Seconds 3 + Resume-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 15 -ErrorAction SilentlyContinue | Out-Null } - if (Skip-IfNoLinuxVm) { return } - $task = Restart-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Confirm:$false + $task = Restart-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 60 -Confirm:$false $task | Should -Not -BeNullOrEmpty - # Wait a moment for the VM to come back up - Start-Sleep -Seconds 10 $vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $script:LinuxVmId } $vm.Status | Should -Be 'running' @@ -1206,7 +1188,7 @@ Describe 'Integration Tests' -Tag 'Integration' { It 'Should gracefully stop a VM via ACPI (Stop-PveVm)' { if (Skip-IfNoLinuxVm) { return } - $task = Stop-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Confirm:$false + $task = Stop-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30 -Confirm:$false $task | Should -Not -BeNullOrEmpty $vm = Get-PveVm -Node $script:Node | @@ -1232,32 +1214,43 @@ Describe 'Integration Tests' -Tag 'Integration' { $metadata.Disks.Count | Should -BeGreaterThan 0 } - It 'Should upload OVA and create VM (Import-PveOva)' { + It 'Should import OVA as a VM (Import-PveOva)' { if (Skip-IfNoTarget) { return } if (-not $script:OvaPath -or -not (Test-Path $script:OvaPath)) { Set-ItResult -Skipped -Because 'PVETEST_OVA_PATH not set or file not found' return } - try { - $vm = Import-PveOva -Node $script:Node -Storage $script:Storage ` - -Path $script:OvaPath -TargetStorage 'local-lvm' ` - -Name 'pester-ova-vm' -Wait + $vm = Import-PveOva -Node $script:Node -Storage $script:Storage ` + -Path $script:OvaPath -TargetStorage 'local-lvm' ` + -Name 'pester-ova-vm' -Wait - $vm | Should -Not -BeNullOrEmpty - $script:CreatedVmIds.Add($vm.VmId) - } - catch { - # Disk import may fail with synthetic test VMDK — verify VM was at least created - $found = Get-PveVm -Node $script:Node -Name 'pester-ova-vm' -ErrorAction SilentlyContinue | - Select-Object -First 1 - if ($found) { - $script:CreatedVmIds.Add($found.VmId) - Set-ItResult -Skipped -Because "OVA upload and VM creation succeeded but disk import failed (synthetic VMDK): $_" - } else { - throw - } + $vm | Should -Not -BeNullOrEmpty + $script:CreatedVmIds.Add($vm.VmId) + + # Verify the VM exists + $found = Get-PveVm -Node $script:Node -Name 'pester-ova-vm' | Select-Object -First 1 + $found | Should -Not -BeNullOrEmpty + } + + It 'Should start the OVA-imported VM' { + if (Skip-IfNoTarget) { return } + + $ovaVm = Get-PveVm -Node $script:Node -Name 'pester-ova-vm' -ErrorAction SilentlyContinue | + Select-Object -First 1 + if (-not $ovaVm) { + Set-ItResult -Skipped -Because 'OVA VM was not imported' + return } + + $task = Start-PveVm -Node $script:Node -VmId $ovaVm.VmId -Wait -Timeout 60 + $task | Should -Not -BeNullOrEmpty + + $vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $ovaVm.VmId } + $vm.Status | Should -Be 'running' + + # Clean up + Stop-PveVm -Node $script:Node -VmId $ovaVm.VmId -Wait -Timeout 30 -Confirm:$false | Out-Null } } @@ -1270,7 +1263,7 @@ Describe 'Integration Tests' -Tag 'Integration' { $vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $script:LinuxVmId } if ($vm.Status -eq 'running') { - Stop-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Confirm:$false | Out-Null + Stop-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30 -Confirm:$false | Out-Null } { New-PveTemplate -Node $script:Node -VmId $script:LinuxVmId -Confirm:$false -ErrorAction Stop } | @@ -1327,7 +1320,7 @@ Describe 'Integration Tests' -Tag 'Integration' { # Ensure stopped $vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $script:TestVmId } if ($vm.Status -eq 'running') { - Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Confirm:$false | Out-Null + Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Timeout 30 -Confirm:$false | Out-Null } { Remove-PveVm ` diff --git a/tests/infrastructure/scripts/prepare-test-environment.sh b/tests/infrastructure/scripts/prepare-test-environment.sh index 7c8427a..a011e0a 100755 --- a/tests/infrastructure/scripts/prepare-test-environment.sh +++ b/tests/infrastructure/scripts/prepare-test-environment.sh @@ -56,90 +56,16 @@ else echo "Cloud image already cached at ${CLOUD_IMAGE_PATH}" fi -# Create a minimal test OVA for Import-PveOva testing -# OVA = TAR containing an OVF descriptor + a small VMDK/raw disk -OVA_PATH="${OUTPUT_DIR}/test-appliance.ova" +# Download Ubuntu cloud OVA for Import-PveOva testing +OVA_URL="https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.ova" +OVA_FILENAME="ubuntu-24.04-server-cloudimg-amd64.ova" +OVA_PATH="${OUTPUT_DIR}/${OVA_FILENAME}" if [ ! -f "${OVA_PATH}" ]; then - echo "Creating minimal test OVA..." - OVA_TMPDIR=$(mktemp -d) - - # Create a minimal valid sparse VMDK using qemu-img - qemu-img create -f vmdk -o subformat=streamOptimized "${OVA_TMPDIR}/test-disk.vmdk" 64M 2>/dev/null - - # Create OVF descriptor - cat > "${OVA_TMPDIR}/test-appliance.ovf" <<'OVF' - - - - - - - Virtual disk information - - - - A minimal test appliance - - Linux 64-bit - Linux - - - Virtual hardware requirements - - Virtual Hardware Family - 0 - test-appliance - vmx-13 - - - Number of Virtual CPUs - 1 virtual CPU(s) - 1 - 3 - 1 - - - byte * 2^20 - Memory Size - 256MB of memory - 2 - 4 - 256 - - - SCSI Controller - 3 - lsilogic - 6 - - - Hard Disk 1 - ovf:/disk/vmdisk1 - 4 - 3 - 17 - - - VM Network - Ethernet adapter 1 - 5 - E1000 - 10 - - - - -OVF - - # Pack as OVA (TAR, OVF first per spec) - (cd "${OVA_TMPDIR}" && tar cf "${OVA_PATH}" test-appliance.ovf test-disk.vmdk) - rm -rf "${OVA_TMPDIR}" - echo "Created test OVA at ${OVA_PATH} ($(du -h "${OVA_PATH}" | cut -f1))" + echo "Downloading Ubuntu cloud OVA (this may take a few minutes)..." + curl -fSL -o "${OVA_PATH}" "${OVA_URL}" + echo "Downloaded OVA ($(du -h "${OVA_PATH}" | cut -f1))" else - echo "Test OVA already cached at ${OVA_PATH}" + echo "OVA already cached at ${OVA_PATH}" fi echo "CLOUD_IMAGE_PATH=${CLOUD_IMAGE_PATH}"