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.
This commit is contained in:
goodolclint-claude[bot]
2026-09-02 16:34:52 +00:00
committed by GitHub
parent 68f953075d
commit c12bfe9183
5 changed files with 76 additions and 12 deletions
@@ -329,16 +329,22 @@ namespace PSProxmoxVE.Core.Services
PveSession session,
string node,
int vmid,
bool purge = false)
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)}/lxc/{vmid}{purgeParam}")
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}{queryString}")
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
+9 -3
View File
@@ -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);
}
@@ -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 skiplock=1 to PVE, which bypasses locks. PVE honours this for root@pam only.
/// </para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Force the operation without additional checks.")]
[Parameter(Mandatory = false, HelpMessage = "Bypass locks (root@pam only); sends skiplock=1 to PVE.")]
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)
{
@@ -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, which bypasses locks. PVE honours this for root@pam only.
/// </para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Force the operation without additional checks.")]
[Parameter(Mandatory = false, HelpMessage = "Bypass locks (root@pam only); sends skiplock=1 to PVE.")]
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)
{
@@ -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!);
}
}
}