mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-07 20:45:40 +00:00
3ea0e11988
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>
159 lines
6.2 KiB
C#
159 lines
6.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net;
|
|
using System.Threading.Tasks;
|
|
using Moq;
|
|
using Xunit;
|
|
using PSProxmoxVE.Core.Authentication;
|
|
using PSProxmoxVE.Core.Client;
|
|
using PSProxmoxVE.Core.Exceptions;
|
|
using PSProxmoxVE.Core.Services;
|
|
|
|
namespace PSProxmoxVE.Core.Tests.Services
|
|
{
|
|
public class TemplateServiceTests
|
|
{
|
|
private const string Node = "pve1";
|
|
private const int VmId = 9000;
|
|
|
|
private static PveSession CreateSession()
|
|
{
|
|
return new PveSession("pve.example.com", 8006, false,
|
|
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateTemplate_CallsPostAsync_ReturnsUpid()
|
|
{
|
|
// Arrange
|
|
const string upid = "UPID:pve1:000ABC:00000001:5F1234AB:qmtemplate:9000:root@pam:";
|
|
var json = $@"{{""data"": ""{upid}""}}";
|
|
var mockClient = new Mock<IPveHttpClient>();
|
|
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
|
.ReturnsAsync(json);
|
|
|
|
var service = new TemplateService(mockClient.Object);
|
|
|
|
// Act
|
|
var task = service.CreateTemplate(CreateSession(), Node, VmId);
|
|
|
|
// Assert
|
|
Assert.Equal(upid, task.Upid);
|
|
Assert.Equal(Node, task.Node);
|
|
Assert.Equal("running", task.Status);
|
|
mockClient.Verify(c => c.PostAsync(
|
|
$"nodes/{Node}/qemu/{VmId}/template",
|
|
It.IsAny<Dictionary<string, string>>()),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateTemplate_WithTaskObject_ParsesCorrectly()
|
|
{
|
|
// Arrange — some PVE versions return a full task object instead of a bare UPID string
|
|
var json = @"{
|
|
""data"": {
|
|
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmtemplate:9000:root@pam:"",
|
|
""type"": ""qmtemplate"",
|
|
""status"": ""running"",
|
|
""node"": ""pve1"",
|
|
""user"": ""root@pam""
|
|
}
|
|
}";
|
|
var mockClient = new Mock<IPveHttpClient>();
|
|
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
|
.ReturnsAsync(json);
|
|
|
|
var service = new TemplateService(mockClient.Object);
|
|
|
|
// Act
|
|
var task = service.CreateTemplate(CreateSession(), Node, VmId);
|
|
|
|
// Assert
|
|
Assert.Equal("UPID:pve1:000ABC:00000001:5F1234AB:qmtemplate:9000:root@pam:", task.Upid);
|
|
Assert.Equal("qmtemplate", task.Type);
|
|
Assert.Equal(Node, task.Node);
|
|
}
|
|
|
|
[Fact]
|
|
public void CreateTemplate_EmptyNode_ThrowsArgumentNullException()
|
|
{
|
|
var service = new TemplateService(new Mock<IPveHttpClient>().Object);
|
|
|
|
Assert.Throws<ArgumentNullException>("node", () => service.CreateTemplate(CreateSession(), " ", VmId));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_NullClient_ThrowsArgumentNullException()
|
|
{
|
|
Assert.Throws<ArgumentNullException>("client", () => new TemplateService(null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetTemplates_SingleNode_ReturnsOnlyTemplateFlaggedVms()
|
|
{
|
|
// Arrange
|
|
var json = @"{""data"": [
|
|
{""vmid"": 100, ""name"": ""web-template"", ""template"": 1},
|
|
{""vmid"": 101, ""name"": ""running-vm"", ""template"": 0},
|
|
{""vmid"": 102, ""name"": ""db-template"", ""template"": 1}
|
|
]}";
|
|
var mockClient = new Mock<IPveHttpClient>();
|
|
mockClient.Setup(c => c.GetAsync($"nodes/{Node}/qemu")).ReturnsAsync(json);
|
|
var service = new TemplateService(mockClient.Object);
|
|
|
|
// Act
|
|
var templates = service.GetTemplates(CreateSession(), Node);
|
|
|
|
// Assert
|
|
Assert.Equal(2, templates.Length);
|
|
Assert.All(templates, t => Assert.Equal(1, t.Template));
|
|
Assert.Contains(templates, t => t.VmId == 100);
|
|
Assert.Contains(templates, t => t.VmId == 102);
|
|
mockClient.Verify(c => c.GetAsync($"nodes/{Node}/qemu"), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetTemplates_EscapesNodeInPath()
|
|
{
|
|
// Arrange
|
|
var json = @"{""data"": []}";
|
|
var mockClient = new Mock<IPveHttpClient>();
|
|
mockClient.Setup(c => c.GetAsync("nodes/pve%20node/qemu")).ReturnsAsync(json);
|
|
var service = new TemplateService(mockClient.Object);
|
|
|
|
// Act
|
|
service.GetTemplates(CreateSession(), "pve node");
|
|
|
|
// Assert
|
|
mockClient.Verify(c => c.GetAsync("nodes/pve%20node/qemu"), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetTemplates_AllNodes_SourcesFromClusterResourcesInOneCall()
|
|
{
|
|
// Arrange: issue #152 — VmService.GetVms(node: null) now sources the all-nodes
|
|
// listing from cluster/resources instead of a call per node.
|
|
var mockClient = new Mock<IPveHttpClient>();
|
|
mockClient.Setup(c => c.GetAsync("cluster/resources?type=vm"))
|
|
.ReturnsAsync(@"{""data"": [
|
|
{""type"": ""qemu"", ""vmid"": 100, ""node"": ""pve1"", ""template"": 1},
|
|
{""type"": ""qemu"", ""vmid"": 101, ""node"": ""pve1"", ""template"": 0},
|
|
{""type"": ""qemu"", ""vmid"": 200, ""node"": ""pve2"", ""template"": 1},
|
|
{""type"": ""lxc"", ""vmid"": 300, ""node"": ""pve2"", ""template"": 1}
|
|
]}");
|
|
var service = new TemplateService(mockClient.Object);
|
|
|
|
// Act
|
|
var templates = service.GetTemplates(CreateSession());
|
|
|
|
// Assert
|
|
Assert.Equal(2, templates.Length);
|
|
Assert.Contains(templates, t => t.VmId == 100 && t.Node == "pve1");
|
|
Assert.Contains(templates, t => t.VmId == 200 && t.Node == "pve2");
|
|
Assert.DoesNotContain(templates, t => t.VmId == 300);
|
|
mockClient.Verify(c => c.GetAsync("cluster/resources?type=vm"), Times.Once);
|
|
}
|
|
}
|
|
}
|