feat: add -Detailed switch to Get-PveVm for accurate pause detection

PVE list endpoint reports Status=running for paused VMs. The new
-Detailed switch queries status/current per VM to populate QmpStatus.

- PveVm.EffectiveStatus computed property: returns QmpStatus ?? Status
- PveVm.Status doc comment explains the limitation
- VmService.EnrichVmStatus() queries status/current endpoint
- Get-PveVm -Detailed enriches each VM with qmpstatus, pid, uptime
- format.ps1xml table view uses EffectiveStatus for the Status column
- Integration test asserts EffectiveStatus after suspend/resume

Verified locally: Get-PveVm -Detailed correctly shows 'paused' for
suspended VMs where Get-PveVm (without -Detailed) shows 'running'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-20 16:18:38 -05:00
parent 5e37737bc4
commit 8d5f33f592
5 changed files with 81 additions and 7 deletions
+15 -3
View File
@@ -24,7 +24,10 @@ public class PveVm
public string? Name { get; set; }
/// <summary>
/// The current runtime status of the VM (e.g., "running", "stopped").
/// The runtime status from the VM list endpoint (e.g., "running", "stopped").
/// Note: reports "running" for both running AND paused VMs. For accurate pause
/// detection, use <see cref="EffectiveStatus"/> or <see cref="QmpStatus"/>,
/// which are populated when using Get-PveVm -Detailed.
/// </summary>
[JsonPropertyName("status")]
[JsonProperty("status")]
@@ -94,12 +97,21 @@ public class PveVm
public int? Pid { get; set; }
/// <summary>
/// The QMP (QEMU Machine Protocol) status string.
/// The QMP (QEMU Machine Protocol) status string (e.g., "running", "paused", "stopped").
/// Only populated from the status/current endpoint (Get-PveVm -Detailed), not the list endpoint.
/// </summary>
[JsonPropertyName("qmpstatus")]
[JsonProperty("qmpstatus")]
public string? QmpStatus { get; set; }
/// <summary>
/// The effective runtime status, preferring QmpStatus over Status for accurate
/// pause detection. Returns QmpStatus when available (from -Detailed), otherwise Status.
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
public string? EffectiveStatus => QmpStatus ?? Status;
/// <summary>
/// Indicates whether the QEMU guest agent is running (non-zero) inside the VM.
/// </summary>
@@ -115,7 +127,7 @@ public class PveVm
var uptimeStr = Uptime.HasValue ? TimeSpan.FromSeconds(Uptime.Value).ToString(@"d\.hh\:mm\:ss") : "N/A";
var templateStr = Template is 1 ? " [template]" : string.Empty;
return $"VM {VmId}{templateStr}: {Name ?? "(unnamed)"} | Node: {Node ?? "N/A"} | "
+ $"Status: {Status ?? "N/A"} | CPUs: {CpuCount?.ToString() ?? "N/A"} | "
+ $"Status: {EffectiveStatus ?? "N/A"} | CPUs: {CpuCount?.ToString() ?? "N/A"} | "
+ $"Mem: {maxMemMb} | Disk: {maxDiskGb} | Uptime: {uptimeStr}";
}
}
@@ -80,6 +80,34 @@ namespace PSProxmoxVE.Core.Services
return vm;
}
/// <summary>
/// Enriches a VM object with detailed status from the status/current endpoint,
/// populating QmpStatus and other fields not available from the list endpoint.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="vm">The VM to enrich.</param>
public void EnrichVmStatus(PveSession session, string node, PveVm vm)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (vm == null) throw new ArgumentNullException(nameof(vm));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"nodes/{node}/qemu/{vm.VmId}/status/current")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
if (data == null) return;
vm.QmpStatus = data["qmpstatus"]?.ToString();
vm.Status = data["status"]?.ToString() ?? vm.Status;
vm.Pid = data["pid"]?.ToObject<int?>();
vm.Uptime = data["uptime"]?.ToObject<long?>();
vm.CpuCount = data["cpus"]?.ToObject<int?>() ?? vm.CpuCount;
vm.MaxMem = data["maxmem"]?.ToObject<long?>() ?? vm.MaxMem;
vm.MaxDisk = data["maxdisk"]?.ToObject<long?>() ?? vm.MaxDisk;
}
/// <summary>
/// Returns the full configuration of a VM.
/// </summary>
+30 -1
View File
@@ -58,6 +58,16 @@ namespace PSProxmoxVE.Cmdlets.Vms
[Parameter(Mandatory = false, HelpMessage = "Return only VMs marked as templates.")]
public SwitchParameter TemplatesOnly { get; set; }
/// <summary>
/// <para type="description">
/// When specified, queries the status/current endpoint for each VM to populate
/// QmpStatus and EffectiveStatus. This provides accurate pause detection but
/// requires one additional API call per VM.
/// </para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Include detailed status (QmpStatus) via per-VM API call.")]
public SwitchParameter Detailed { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
@@ -84,7 +94,26 @@ namespace PSProxmoxVE.Cmdlets.Vms
if (TemplatesOnly.IsPresent)
vms = vms.Where(v => v.Template == 1);
foreach (var vm in vms)
// Materialize before enrichment to avoid multiple enumeration
var vmList = vms.ToList();
if (Detailed.IsPresent)
{
WriteVerbose($"Enriching {vmList.Count} VM(s) with detailed status...");
foreach (var vm in vmList)
{
try
{
service.EnrichVmStatus(session, vm.Node ?? Node ?? string.Empty, vm);
}
catch
{
// Skip enrichment for inaccessible VMs (e.g. locked, migrating)
}
}
}
foreach (var vm in vmList)
WriteObject(vm);
}
}
+1 -1
View File
@@ -125,7 +125,7 @@
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Status</PropertyName>
<PropertyName>EffectiveStatus</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Node</PropertyName>
@@ -1153,14 +1153,19 @@ Describe 'Integration Tests' -Tag 'Integration' {
if (Skip-IfNoLinuxVm) { return }
# Suspend — -Wait -Timeout polls qmpstatus until 'paused'
# Note: PVE list endpoint reports Status=running for paused VMs;
# the -Wait polling uses the status/current endpoint which has qmpstatus.
$suspendTask = Suspend-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30
$suspendTask | Should -Not -BeNullOrEmpty
# Verify with -Detailed (fetches qmpstatus from status/current endpoint)
$vm = Get-PveVm -Node $script:Node -VmId $script:LinuxVmId -Detailed
$vm.EffectiveStatus | Should -Be 'paused'
# Resume — -Wait -Timeout polls qmpstatus until 'running'
$resumeTask = Resume-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30
$resumeTask | Should -Not -BeNullOrEmpty
$vm = Get-PveVm -Node $script:Node -VmId $script:LinuxVmId -Detailed
$vm.EffectiveStatus | Should -Be 'running'
}
It 'Should gracefully restart a VM via ACPI (Restart-PveVm)' {