Files
PSProxmoxVE/src/PSProxmoxVE.Core/Services/TemplateService.cs
T
goodolclint-claude[bot] 3ea0e11988 fix: Get-PveVm sources the all-nodes listing from cluster/resources, not per-node fan-out (#223)
VmService.GetVms(node: null) now issues a single GET cluster/resources?type=vm
instead of GET /nodes followed by one GET /nodes/{n}/qemu per node -- 17 round
trips down to 1 on a 16-node cluster. VmService.GetVm(session, node, vmid) now
fetches nodes/{node}/qemu/{vmid}/status/current directly instead of listing
the whole node and filtering client-side. GetPveVmCmdlet's -Detailed path
shares one IPveHttpClient across the enrichment loop and calls WriteObject
per VM as it is enriched, instead of materializing the whole list first and
opening a fresh client per VM.

cluster/resources's "type=vm" filter returns both qemu and lxc rows (per the
PVE OpenAPI spec), so the mapping filters to type=="qemu" explicitly. The
resources endpoint does not carry QmpStatus/Pid/AgentStatus, so those stay
null until -Detailed or GetVm enrich from status/current; the default table
view (VmId/Name/EffectiveStatus/Node/CpuCount/MaxMem/Uptime) is unaffected.

GetVm's status/current call converts a 404 or 500 into the pre-existing
InvalidOperationException("not found") contract that ImportPveOvaCmdlet
depends on, but 502/503/504 propagate unchanged since PveHttpClient wraps a
connectivity failure as 503 -- a down node must not read as a missing VM.

Removing the old per-node fan-out (issue #142's node-skip behavior) means
onNodeSkipped is no longer invoked for the all-nodes path: a single
cluster/resources call has no per-node failure to report, so a
node-unreachable condition now surfaces via that node's rows rather than a
WriteWarning. The parameter stays on VmService.GetVms/TemplateService.GetTemplates
for source compatibility with the cmdlets that still wire it.

TemplateService.GetTemplates and its tests are also updated since it
delegates to VmService.GetVms and inherited the same per-node fan-out.

Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 17:33:02 +00:00

99 lines
4.0 KiB
C#

using System;
using System.Linq;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Utilities;
namespace PSProxmoxVE.Core.Services
{
/// <summary>
/// Service for Proxmox VE VM template operations.
/// Templates are VMs with the "template" flag set to 1.
/// </summary>
public class TemplateService : PveServiceBase
{
private readonly VmService _vmService;
/// <summary>
/// Initializes a new instance of the <see cref="TemplateService"/> class.
/// </summary>
public TemplateService()
{
_vmService = new VmService();
}
/// <summary>
/// Initializes a new instance of the <see cref="TemplateService"/> class with an injected HTTP client.
/// </summary>
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
public TemplateService(IPveHttpClient client) : base(client)
{
_vmService = new VmService(client);
}
/// <summary>
/// Returns all VM templates. If <paramref name="node"/> is null, searches all cluster nodes.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">Optional cluster node name to filter templates by node.</param>
/// <param name="onNodeSkipped">
/// Forwarded to <see cref="VmService.GetVms"/>, which does not invoke it for the
/// all-nodes listing (a single <c>cluster/resources</c> call has no per-node
/// failure to report); kept for source compatibility with existing callers.
/// </param>
public PveVm[] GetTemplates(PveSession session, string? node = null, Action<string, Exception>? onNodeSkipped = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
var vms = _vmService.GetVms(session, node, onNodeSkipped);
return vms.Where(v => v.Template == 1).ToArray();
}
/// <summary>
/// Converts an existing VM into a template. Returns the task UPID.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="vmid">The VM ID.</param>
/// <remarks>
/// The VM must be stopped and must not already be a template.
/// Once converted, this operation cannot be reversed via the API.
/// </remarks>
public PveTask CreateTemplate(PveSession session, string node, int vmid)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
return Invoke(session, client =>
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/template")
.GetAwaiter().GetResult();
return PveTaskResponse.Parse(response, node);
});
}
/// <summary>
/// Removes a VM template (delegates to <see cref="VmService.RemoveVm"/>).
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The cluster node name.</param>
/// <param name="vmid">The VM ID.</param>
/// <param name="purge">
/// If true, also removes all associated backup files and jobs.
/// </param>
public PveTask RemoveTemplate(
PveSession session,
string node,
int vmid,
bool purge = false)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
return _vmService.RemoveVm(session, node, vmid, purge);
}
}
}