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}"