mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-03 18:55:33 +00:00
fix: plumb skiplock/force parameters through Remove-PveVm/Container (#164)
* fix: plumb skiplock parameter through Remove-PveVm and Remove-PveContainer
When -Force is specified, both cmdlets now pass skiplock=1 to PVE, which bypasses locks.
PVE honours the skiplock parameter for root@pam only.
Also updated help text on both cmdlets to clarify the limitation and behaviour.
Fixes #136.
* fix: plumb skiplock parameter for VMs, force for containers
VM removal: add skiplock=1 parameter when -Force is specified. PVE honors it
for root@pam only; non-root callers receive 403 errors. Updated help text.
Container removal: map -Force to force=1 (LXC-specific parameter for forcing
removal of running containers). Containers do not support skiplock.
Also: clarified class and parameter documentation to remove false claims about
-Force suppressing confirmation (it does not).
Fixes #136. Addresses correctness reviewer findings.
* fix: revert unrelated formatting churn, add container force test
The em-dash-to-hyphen sweep in commit 75f4bbf touched VmService.cs and
VmServiceTests.cs outside the skiplock/force change; restore the
original text there, including the <vmid> XML-doc escape that
had been un-escaped and was malforming the generated documentation.
Add ContainerServiceTests.cs, covering the untested force=1
query-string plumbing in ContainerService.RemoveContainer.
---------
Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
a7d2b877f5
commit
e942878aa2
@@ -329,16 +329,22 @@ namespace PSProxmoxVE.Core.Services
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
bool purge = false)
|
||||
bool purge = false,
|
||||
bool force = false)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var purgeParam = purge ? "?purge=1" : "?purge=0";
|
||||
var queryParams = new List<string>();
|
||||
queryParams.Add(purge ? "purge=1" : "purge=0");
|
||||
if (force)
|
||||
queryParams.Add("force=1");
|
||||
var queryString = "?" + string.Join("&", queryParams);
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}{purgeParam}")
|
||||
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}{queryString}")
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
@@ -394,16 +394,22 @@ namespace PSProxmoxVE.Core.Services
|
||||
/// <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 RemoveVm(PveSession session, string node, int vmid, bool purge = false)
|
||||
/// <param name="skipLock">If true, bypasses locks (PVE honours this for root@pam only).</param>
|
||||
public PveTask RemoveVm(PveSession session, string node, int vmid, bool purge = false, bool skipLock = false)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var purgeParam = purge ? "?purge=1" : "?purge=0";
|
||||
var queryParams = new List<string>();
|
||||
queryParams.Add(purge ? "purge=1" : "purge=0");
|
||||
if (skipLock)
|
||||
queryParams.Add("skiplock=1");
|
||||
var queryString = "?" + string.Join("&", queryParams);
|
||||
|
||||
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||
try
|
||||
{
|
||||
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}{purgeParam}")
|
||||
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}{queryString}")
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace PSProxmoxVE.Cmdlets.Containers
|
||||
/// <para type="synopsis">Removes an LXC container from a Proxmox VE node.</para>
|
||||
/// <para type="description">
|
||||
/// Deletes an LXC container and, optionally, all associated storage.
|
||||
/// This operation is destructive and requires confirmation unless -Force is specified.
|
||||
/// This operation is destructive and requires confirmation.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Remove, "PveContainer",
|
||||
@@ -42,10 +42,10 @@ namespace PSProxmoxVE.Cmdlets.Containers
|
||||
|
||||
/// <summary>
|
||||
/// <para type="description">
|
||||
/// When specified, bypasses locks and forces removal even if a lock is set on the container.
|
||||
/// When specified, sends force=1 to PVE, allowing removal of running containers.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = false, HelpMessage = "Force the operation without additional checks.")]
|
||||
[Parameter(Mandatory = false, HelpMessage = "Force destroy, even if running.")]
|
||||
public SwitchParameter Force { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -63,7 +63,7 @@ namespace PSProxmoxVE.Cmdlets.Containers
|
||||
var containerService = new ContainerService();
|
||||
|
||||
WriteVerbose($"Removing container {VmId} from node '{Node}'...");
|
||||
var task = containerService.RemoveContainer(session, Node, VmId, Purge.IsPresent);
|
||||
var task = containerService.RemoveContainer(session, Node, VmId, Purge.IsPresent, Force.IsPresent);
|
||||
|
||||
if (Wait.IsPresent)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace PSProxmoxVE.Cmdlets.Vms
|
||||
/// <para type="synopsis">Removes a QEMU/KVM virtual machine from a Proxmox VE node.</para>
|
||||
/// <para type="description">
|
||||
/// Deletes a virtual machine and, optionally, all associated disk images.
|
||||
/// This operation is destructive and requires confirmation unless -Force is specified.
|
||||
/// This operation is destructive and requires confirmation.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Remove, "PveVm",
|
||||
@@ -42,10 +42,10 @@ namespace PSProxmoxVE.Cmdlets.Vms
|
||||
|
||||
/// <summary>
|
||||
/// <para type="description">
|
||||
/// When specified, bypasses locks and forces removal even if a lock is set on the VM.
|
||||
/// When specified, sends skiplock=1 to PVE to bypass locks. PVE honours this parameter for root@pam only; non-root callers will receive a 403 permission error.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = false, HelpMessage = "Force the operation without additional checks.")]
|
||||
[Parameter(Mandatory = false, HelpMessage = "Bypass locks via skiplock=1 (root@pam only).")]
|
||||
public SwitchParameter Force { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -63,7 +63,7 @@ namespace PSProxmoxVE.Cmdlets.Vms
|
||||
var vmService = new VmService();
|
||||
|
||||
WriteVerbose($"Removing VM {VmId} from node '{Node}'...");
|
||||
var task = vmService.RemoveVm(session, Node, VmId, Purge.IsPresent);
|
||||
var task = vmService.RemoveVm(session, Node, VmId, Purge.IsPresent, Force.IsPresent);
|
||||
|
||||
if (Wait.IsPresent)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using Moq;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace PSProxmoxVE.Core.Tests.Services
|
||||
{
|
||||
public class ContainerServiceTests
|
||||
{
|
||||
private const string TestNode = "pve1";
|
||||
private const int TestVmId = 100;
|
||||
|
||||
private static PveSession CreateSession() =>
|
||||
new PveSession("pve1.example.com", 8006, true, "PVE:root@pam:TEST_TOKEN");
|
||||
|
||||
[Fact]
|
||||
public void RemoveContainer_WithForceTrue_IncludesForceInQueryString()
|
||||
{
|
||||
string? resource = null;
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient
|
||||
.Setup(c => c.DeleteAsync(It.IsAny<string>()))
|
||||
.Callback<string>(r => resource = r)
|
||||
.ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:vzdestroy:100:root@pam:\"}");
|
||||
|
||||
var service = new ContainerService(mockClient.Object);
|
||||
service.RemoveContainer(CreateSession(), TestNode, TestVmId, purge: false, force: true);
|
||||
|
||||
Assert.Equal($"nodes/{TestNode}/lxc/{TestVmId}?purge=0&force=1", resource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveContainer_WithForceFalse_OmitsForceFromQueryString()
|
||||
{
|
||||
string? resource = null;
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient
|
||||
.Setup(c => c.DeleteAsync(It.IsAny<string>()))
|
||||
.Callback<string>(r => resource = r)
|
||||
.ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:vzdestroy:100:root@pam:\"}");
|
||||
|
||||
var service = new ContainerService(mockClient.Object);
|
||||
service.RemoveContainer(CreateSession(), TestNode, TestVmId, purge: false, force: false);
|
||||
|
||||
Assert.Equal($"nodes/{TestNode}/lxc/{TestVmId}?purge=0", resource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveContainer_WithPurgeAndForce_IncludesBothInQueryString()
|
||||
{
|
||||
string? resource = null;
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient
|
||||
.Setup(c => c.DeleteAsync(It.IsAny<string>()))
|
||||
.Callback<string>(r => resource = r)
|
||||
.ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:vzdestroy:100:root@pam:\"}");
|
||||
|
||||
var service = new ContainerService(mockClient.Object);
|
||||
service.RemoveContainer(CreateSession(), TestNode, TestVmId, purge: true, force: true);
|
||||
|
||||
Assert.Equal($"nodes/{TestNode}/lxc/{TestVmId}?purge=1&force=1", resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,5 +165,57 @@ namespace PSProxmoxVE.Core.Tests.Services
|
||||
Assert.Empty(captured!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveVm_WithSkipLockTrue_IncludesSkiplockInQueryString()
|
||||
{
|
||||
string? resource = null;
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient
|
||||
.Setup(c => c.DeleteAsync(It.IsAny<string>()))
|
||||
.Callback<string>(r => resource = r)
|
||||
.ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:qmremove:100:root@pam:\"}");
|
||||
|
||||
var service = new VmService(mockClient.Object);
|
||||
service.RemoveVm(CreateSession(), TestNode, TestVmId, purge: false, skipLock: true);
|
||||
|
||||
Assert.NotNull(resource);
|
||||
Assert.Contains("skiplock=1", resource!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveVm_WithSkipLockFalse_OmitsSkiplockFromQueryString()
|
||||
{
|
||||
string? resource = null;
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient
|
||||
.Setup(c => c.DeleteAsync(It.IsAny<string>()))
|
||||
.Callback<string>(r => resource = r)
|
||||
.ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:qmremove:100:root@pam:\"}");
|
||||
|
||||
var service = new VmService(mockClient.Object);
|
||||
service.RemoveVm(CreateSession(), TestNode, TestVmId, purge: false, skipLock: false);
|
||||
|
||||
Assert.NotNull(resource);
|
||||
Assert.DoesNotContain("skiplock", resource!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveVm_WithPurgeAndSkipLock_IncludesBothInQueryString()
|
||||
{
|
||||
string? resource = null;
|
||||
var mockClient = new Mock<IPveHttpClient>();
|
||||
mockClient
|
||||
.Setup(c => c.DeleteAsync(It.IsAny<string>()))
|
||||
.Callback<string>(r => resource = r)
|
||||
.ReturnsAsync("{\"data\":\"UPID:pve1:00001234:00005678:6A970AAB:qmremove:100:root@pam:\"}");
|
||||
|
||||
var service = new VmService(mockClient.Object);
|
||||
service.RemoveVm(CreateSession(), TestNode, TestVmId, purge: true, skipLock: true);
|
||||
|
||||
Assert.NotNull(resource);
|
||||
Assert.Contains("purge=1", resource!);
|
||||
Assert.Contains("skiplock=1", resource!);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user