diff --git a/src/PSProxmoxVE.Core/Models/Vms/PveVm.cs b/src/PSProxmoxVE.Core/Models/Vms/PveVm.cs index 88d1f32..dd8df0e 100644 --- a/src/PSProxmoxVE.Core/Models/Vms/PveVm.cs +++ b/src/PSProxmoxVE.Core/Models/Vms/PveVm.cs @@ -24,7 +24,10 @@ public class PveVm public string? Name { get; set; } /// - /// 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 or , + /// which are populated when using Get-PveVm -Detailed. /// [JsonPropertyName("status")] [JsonProperty("status")] @@ -94,12 +97,21 @@ public class PveVm public int? Pid { get; set; } /// - /// 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. /// [JsonPropertyName("qmpstatus")] [JsonProperty("qmpstatus")] public string? QmpStatus { get; set; } + /// + /// The effective runtime status, preferring QmpStatus over Status for accurate + /// pause detection. Returns QmpStatus when available (from -Detailed), otherwise Status. + /// + [System.Text.Json.Serialization.JsonIgnore] + [Newtonsoft.Json.JsonIgnore] + public string? EffectiveStatus => QmpStatus ?? Status; + /// /// Indicates whether the QEMU guest agent is running (non-zero) inside the VM. /// @@ -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}"; } } diff --git a/src/PSProxmoxVE.Core/Services/VmService.cs b/src/PSProxmoxVE.Core/Services/VmService.cs index 66734e3..39c8e0c 100644 --- a/src/PSProxmoxVE.Core/Services/VmService.cs +++ b/src/PSProxmoxVE.Core/Services/VmService.cs @@ -80,6 +80,34 @@ namespace PSProxmoxVE.Core.Services return vm; } + /// + /// Enriches a VM object with detailed status from the status/current endpoint, + /// populating QmpStatus and other fields not available from the list endpoint. + /// + /// The authenticated PVE session. + /// The cluster node name. + /// The VM to enrich. + 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(); + vm.Uptime = data["uptime"]?.ToObject(); + vm.CpuCount = data["cpus"]?.ToObject() ?? vm.CpuCount; + vm.MaxMem = data["maxmem"]?.ToObject() ?? vm.MaxMem; + vm.MaxDisk = data["maxdisk"]?.ToObject() ?? vm.MaxDisk; + } + /// /// Returns the full configuration of a VM. /// diff --git a/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmCmdlet.cs index b6ecf0d..2cefabb 100644 --- a/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Vms/GetPveVmCmdlet.cs @@ -58,6 +58,16 @@ namespace PSProxmoxVE.Cmdlets.Vms [Parameter(Mandatory = false, HelpMessage = "Return only VMs marked as templates.")] public SwitchParameter TemplatesOnly { get; set; } + /// + /// + /// 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. + /// + /// + [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); } } diff --git a/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml b/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml index 6535d44..90e7cf9 100644 --- a/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml +++ b/src/PSProxmoxVE/PSProxmoxVE.format.ps1xml @@ -125,7 +125,7 @@ Name - Status + EffectiveStatus Node diff --git a/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 b/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 index 14abe90..76c9ea2 100644 --- a/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 @@ -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)' {