mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-08-28 16:06:59 +00:00
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:
@@ -24,7 +24,10 @@ public class PveVm
|
|||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[JsonPropertyName("status")]
|
[JsonPropertyName("status")]
|
||||||
[JsonProperty("status")]
|
[JsonProperty("status")]
|
||||||
@@ -94,12 +97,21 @@ public class PveVm
|
|||||||
public int? Pid { get; set; }
|
public int? Pid { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
[JsonPropertyName("qmpstatus")]
|
[JsonPropertyName("qmpstatus")]
|
||||||
[JsonProperty("qmpstatus")]
|
[JsonProperty("qmpstatus")]
|
||||||
public string? QmpStatus { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Indicates whether the QEMU guest agent is running (non-zero) inside the VM.
|
/// Indicates whether the QEMU guest agent is running (non-zero) inside the VM.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -115,7 +127,7 @@ public class PveVm
|
|||||||
var uptimeStr = Uptime.HasValue ? TimeSpan.FromSeconds(Uptime.Value).ToString(@"d\.hh\:mm\:ss") : "N/A";
|
var uptimeStr = Uptime.HasValue ? TimeSpan.FromSeconds(Uptime.Value).ToString(@"d\.hh\:mm\:ss") : "N/A";
|
||||||
var templateStr = Template is 1 ? " [template]" : string.Empty;
|
var templateStr = Template is 1 ? " [template]" : string.Empty;
|
||||||
return $"VM {VmId}{templateStr}: {Name ?? "(unnamed)"} | Node: {Node ?? "N/A"} | "
|
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}";
|
+ $"Mem: {maxMemMb} | Disk: {maxDiskGb} | Uptime: {uptimeStr}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,34 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
return vm;
|
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>
|
/// <summary>
|
||||||
/// Returns the full configuration of a VM.
|
/// Returns the full configuration of a VM.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ namespace PSProxmoxVE.Cmdlets.Vms
|
|||||||
[Parameter(Mandatory = false, HelpMessage = "Return only VMs marked as templates.")]
|
[Parameter(Mandatory = false, HelpMessage = "Return only VMs marked as templates.")]
|
||||||
public SwitchParameter TemplatesOnly { get; set; }
|
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()
|
protected override void ProcessRecord()
|
||||||
{
|
{
|
||||||
var session = GetSession();
|
var session = GetSession();
|
||||||
@@ -84,7 +94,26 @@ namespace PSProxmoxVE.Cmdlets.Vms
|
|||||||
if (TemplatesOnly.IsPresent)
|
if (TemplatesOnly.IsPresent)
|
||||||
vms = vms.Where(v => v.Template == 1);
|
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);
|
WriteObject(vm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
<PropertyName>Name</PropertyName>
|
<PropertyName>Name</PropertyName>
|
||||||
</TableColumnItem>
|
</TableColumnItem>
|
||||||
<TableColumnItem>
|
<TableColumnItem>
|
||||||
<PropertyName>Status</PropertyName>
|
<PropertyName>EffectiveStatus</PropertyName>
|
||||||
</TableColumnItem>
|
</TableColumnItem>
|
||||||
<TableColumnItem>
|
<TableColumnItem>
|
||||||
<PropertyName>Node</PropertyName>
|
<PropertyName>Node</PropertyName>
|
||||||
|
|||||||
@@ -1153,14 +1153,19 @@ Describe 'Integration Tests' -Tag 'Integration' {
|
|||||||
if (Skip-IfNoLinuxVm) { return }
|
if (Skip-IfNoLinuxVm) { return }
|
||||||
|
|
||||||
# Suspend — -Wait -Timeout polls qmpstatus until 'paused'
|
# 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 = Suspend-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30
|
||||||
$suspendTask | Should -Not -BeNullOrEmpty
|
$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'
|
# Resume — -Wait -Timeout polls qmpstatus until 'running'
|
||||||
$resumeTask = Resume-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30
|
$resumeTask = Resume-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30
|
||||||
$resumeTask | Should -Not -BeNullOrEmpty
|
$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)' {
|
It 'Should gracefully restart a VM via ACPI (Restart-PveVm)' {
|
||||||
|
|||||||
Reference in New Issue
Block a user