feat: add Move-PveContainer cmdlet for LXC migration

- New Move-PveContainer cmdlet with -Online and -Wait support
- Uses existing ContainerService.MigrateContainer
- Pester unit tests added to ContainerLifecycle.Tests.ps1
- README cmdlet reference updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-20 12:31:46 -05:00
parent fb4a21a4f9
commit de400e159c
3 changed files with 126 additions and 1 deletions
+1
View File
@@ -238,6 +238,7 @@ SDN management requires Proxmox VE 8.0 or later. Connected server is version 7.4
| `Stop-PveContainer` | Stop a container |
| `Restart-PveContainer` | Restart a container |
| `Copy-PveContainer` | Clone a container |
| `Move-PveContainer` | Migrate a container to another node |
| `Get-PveContainerConfig` | Get container configuration |
| `Set-PveContainerConfig` | Modify container configuration |
| `Get-PveContainerSnapshot` | List container snapshots |
@@ -0,0 +1,63 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Containers
{
/// <summary>
/// <para type="synopsis">Migrates an LXC container to a different Proxmox VE node.</para>
/// <para type="description">
/// Performs a live or offline migration of the specified container to the target node.
/// Use -Online for live migration (container remains running). Use -Wait to block until the
/// migration task completes.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Move, "PveContainer", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public sealed class MovePveContainerCmdlet : PveCmdletBase
{
/// <summary>The node on which the container currently resides.</summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>The ID of the container to migrate. Accepts pipeline input.</summary>
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>The destination node to migrate the container to.</summary>
[Parameter(Mandatory = true, HelpMessage = "The destination node for migration.")]
public string TargetNode { get; set; } = string.Empty;
/// <summary>
/// When specified, performs a live migration so the container remains running during migration.
/// Requires shared storage between the source and target nodes.
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Perform live migration (container stays running).")]
public SwitchParameter Online { get; set; }
/// <summary>When specified, waits for the migration task to complete before returning.</summary>
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"Container {VmId} from node '{Node}' to node '{TargetNode}'", "Move-PveContainer"))
return;
var session = GetSession();
var service = new ContainerService();
WriteVerbose($"Migrating container {VmId} from '{Node}' to '{TargetNode}'...");
var task = service.MigrateContainer(session, Node, VmId, TargetNode, Online.IsPresent);
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
}
WriteObject(task);
}
}
}
@@ -18,7 +18,7 @@ BeforeAll {
'Start-PveContainer', 'Stop-PveContainer',
'Restart-PveContainer',
'Get-PveContainerConfig', 'Set-PveContainerConfig',
'Copy-PveContainer')) {
'Copy-PveContainer', 'Move-PveContainer')) {
$script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
}
@@ -293,3 +293,64 @@ Describe 'Restart-PveContainer' {
}
}
}
# ---------------------------------------------------------------------------
# Move-PveContainer
# ---------------------------------------------------------------------------
Describe 'Move-PveContainer' {
BeforeAll { $script:Cmd = Get-Command 'Move-PveContainer' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Move-PveContainer'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'Parameter metadata' {
It 'Node should be Mandatory' {
Skip-IfMissing 'Move-PveContainer'
$isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'VmId should be Mandatory' {
Skip-IfMissing 'Move-PveContainer'
$isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'TargetNode should be Mandatory' {
Skip-IfMissing 'Move-PveContainer'
$isMandatory = $script:Cmd.Parameters['TargetNode'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'Should have Online switch parameter' {
Skip-IfMissing 'Move-PveContainer'
$script:Cmd.Parameters.ContainsKey('Online') | Should -BeTrue
}
It 'Should have Wait switch parameter' {
Skip-IfMissing 'Move-PveContainer'
$script:Cmd.Parameters.ContainsKey('Wait') | Should -BeTrue
}
It 'Should support ShouldProcess' {
Skip-IfMissing 'Move-PveContainer'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Move-PveContainer'
{ Move-PveContainer -Node 'pve-node1' -VmId 200 -TargetNode 'pve-node2' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}