diff --git a/README.md b/README.md index 0cbba48..a94a761 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/src/PSProxmoxVE/Cmdlets/Containers/MovePveContainerCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Containers/MovePveContainerCmdlet.cs new file mode 100644 index 0000000..3505d98 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Containers/MovePveContainerCmdlet.cs @@ -0,0 +1,63 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Vms; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Containers +{ + /// + /// Migrates an LXC container to a different Proxmox VE node. + /// + /// 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. + /// + /// + [Cmdlet(VerbsCommon.Move, "PveContainer", SupportsShouldProcess = true)] + [OutputType(typeof(PveTask))] + public sealed class MovePveContainerCmdlet : PveCmdletBase + { + /// The node on which the container currently resides. + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")] + public string Node { get; set; } = string.Empty; + + /// The ID of the container to migrate. Accepts pipeline input. + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The container identifier.")] + [ValidateRange(100, 999999999)] + public int VmId { get; set; } + + /// The destination node to migrate the container to. + [Parameter(Mandatory = true, HelpMessage = "The destination node for migration.")] + public string TargetNode { get; set; } = string.Empty; + + /// + /// When specified, performs a live migration so the container remains running during migration. + /// Requires shared storage between the source and target nodes. + /// + [Parameter(Mandatory = false, HelpMessage = "Perform live migration (container stays running).")] + public SwitchParameter Online { get; set; } + + /// When specified, waits for the migration task to complete before returning. + [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); + } + } +} diff --git a/tests/PSProxmoxVE.Tests/Containers/ContainerLifecycle.Tests.ps1 b/tests/PSProxmoxVE.Tests/Containers/ContainerLifecycle.Tests.ps1 index 8b3939b..8bb325c 100644 --- a/tests/PSProxmoxVE.Tests/Containers/ContainerLifecycle.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Containers/ContainerLifecycle.Tests.ps1 @@ -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*' + } + } +}