Files
PSProxmoxVE/src/PSProxmoxVE/Cmdlets/Containers/CopyPveContainerCmdlet.cs
T
goodolclint-claude[bot] 21209fc7ad fix: allocate a real VMID for Copy-PveVm/Copy-PveContainer and honor -Storage
Copy-PveVm and Copy-PveContainer defaulted newid to 0 when -NewVmId
was omitted, and never sent -Storage on the clone request even
though both cmdlets declare it. Both cmdlets now allocate via
ClusterConfigService.GetNextId when -NewVmId is null, and forward
-Storage into the clone form body. Since PVE rejects storage on a
linked clone, -Storage without -Full now fails fast client-side
instead of failing later against the API.

NewPveVmCmdlet and ImportPveOvaCmdlet hand-rolled the same
GET cluster/nextid call with a manual JObject parse; both now go
through ClusterConfigService.GetNextId so a response without a
data field raises the service's diagnosable InvalidOperationException
rather than a NullReferenceException.

Closes #135
2026-09-02 19:20:23 +00:00

113 lines
4.8 KiB
C#

using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Containers
{
/// <summary>
/// <para type="synopsis">Clones (copies) an LXC container on a Proxmox VE node.</para>
/// <para type="description">
/// Creates a clone of an existing LXC container. By default, a linked clone is created
/// when the source is a template. Use -Full to create a full independent copy. Optionally
/// specify a target node to create the clone on a different cluster node.
/// Use -Wait to block until the clone task completes.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Copy, "PveContainer", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public sealed class CopyPveContainerCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node on which the source container resides.</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The node where the source container resides.")]
public string SourceNode { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The ID of the source container to clone. Accepts pipeline input.</para>
/// </summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">The container ID to assign to the new clone. When omitted, the next available ID is used.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Container ID for the new clone.")]
[ValidateRange(100, 999999999)]
public int? NewVmId { get; set; }
/// <summary>
/// <para type="description">The hostname for the new clone.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Hostname for the new clone.")]
public string? NewName { get; set; }
/// <summary>
/// <para type="description">The target node for the clone. Defaults to the source node.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Target node for the clone.")]
public string? TargetNode { get; set; }
/// <summary>
/// <para type="description">
/// When specified, creates a full independent clone instead of a linked clone.
/// A full clone is required when the source container is not a template.
/// </para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Perform a full (non-linked) clone.")]
public SwitchParameter Full { get; set; }
/// <summary>
/// <para type="description">Target storage pool for the full clone root filesystem.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The storage pool name.")]
public string? Storage { get; set; }
/// <summary>
/// <para type="description">When specified, waits for the clone task to complete before returning.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
protected override void ProcessRecord()
{
if (!string.IsNullOrEmpty(Storage) && !Full.IsPresent)
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("-Storage is only valid together with -Full; PVE rejects a target storage on a linked clone.", nameof(Storage)),
"StorageRequiresFullClone",
ErrorCategory.InvalidArgument,
Storage));
var target = TargetNode ?? SourceNode;
if (!ShouldProcess($"Container {VmId} on node '{SourceNode}' to new container on node '{target}'", "Copy-PveContainer"))
return;
var session = GetSession();
var containerService = new ContainerService();
WriteVerbose($"Cloning container {VmId}...");
var newid = NewVmId ?? new ClusterConfigService().GetNextId(session);
if (!NewVmId.HasValue)
WriteVerbose($"Auto-assigned container ID: {newid}");
var task = containerService.CloneContainer(
session,
SourceNode,
VmId,
newid,
NewName,
TargetNode,
Full.IsPresent,
Storage);
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, task.Node ?? SourceNode, task.Upid!, null, null, null);
}
WriteObject(task);
}
}
}