Public/VM/New-VergeVMClone.ps1

function New-VergeVMClone {
    <#
    .SYNOPSIS
        Creates a clone of a VergeOS virtual machine.

    .DESCRIPTION
        New-VergeVMClone creates an independent copy of an existing VM. The clone
        includes all drives and configuration. By default, new MAC addresses are
        assigned to network interfaces.

    .PARAMETER VM
        A VM object from Get-VergeVM. Accepts pipeline input.

    .PARAMETER VMName
        The name of the VM to clone.

    .PARAMETER VMKey
        The key (ID) of the VM to clone.

    .PARAMETER Name
        The name for the new cloned VM. If not specified, uses the format
        "{OriginalName}_{Timestamp}".

    .PARAMETER PreserveMACAddresses
        Keep the same MAC addresses as the source VM. Use with caution as
        this can cause network conflicts if both VMs are on the same network.

    .PARAMETER PowerOn
        Start the cloned VM immediately after creation.

    .PARAMETER PassThru
        Return the cloned VM object.

    .PARAMETER Server
        The VergeOS connection to use. Defaults to the current default connection.

    .EXAMPLE
        New-VergeVMClone -VMName "Template-Ubuntu22" -Name "NewWebServer"

        Clones the template VM to create a new server.

    .EXAMPLE
        Get-VergeVM -Name "Template-*" | New-VergeVMClone

        Clones all template VMs using pipeline input.

    .EXAMPLE
        New-VergeVMClone -VMName "WebServer01" -Name "WebServer01-Test" -PowerOn -PassThru

        Clones a VM, starts it, and returns the new VM object.

    .EXAMPLE
        New-VergeVMClone -VMName "AppServer" -PreserveMACAddresses

        Clones with MAC addresses preserved (for offline testing).

    .OUTPUTS
        None by default. Verge.VM when -PassThru is specified.

    .NOTES
        Cloning creates a complete copy of all VM drives, which may take time
        for large VMs. The source VM can remain running during cloning.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'ByVMName')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByVM')]
        [Alias('SourceVM')]
        [PSTypeName('Verge.VM')]
        [PSCustomObject]$VM,

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByVMName')]
        [Alias('SourceVMName')]
        [string]$VMName,

        [Parameter(Mandatory, ParameterSetName = 'ByVMKey')]
        [Alias('SourceVMKey')]
        [int]$VMKey,

        [Parameter(Position = 1)]
        [ValidateLength(1, 128)]
        [string]$Name,

        [Parameter()]
        [switch]$PreserveMACAddresses,

        [Parameter()]
        [switch]$PowerOn,

        [Parameter()]
        [switch]$PassThru,

        [Parameter()]
        [object]$Server
    )

    begin {
        # Resolve connection
        if (-not $Server) {
            $Server = $script:DefaultConnection
        }
        if (-not $Server) {
            throw [System.InvalidOperationException]::new(
                'Not connected to VergeOS. Use Connect-VergeOS to establish a connection.'
            )
        }
    }

    process {
        # Resolve source VM based on parameter set
        $targetVM = switch ($PSCmdlet.ParameterSetName) {
            'ByVMName' {
                Get-VergeVM -Name $VMName -Server $Server | Select-Object -First 1
            }
            'ByVMKey' {
                Get-VergeVM -Key $VMKey -Server $Server
            }
            'ByVM' {
                $VM
            }
        }

        if (-not $targetVM) {
            Write-Error -Message "Source VM not found" -ErrorId 'VMNotFound'
            return
        }

        # Check if VM is a snapshot
        if ($targetVM.IsSnapshot) {
            Write-Error -Message "Cannot clone '$($targetVM.Name)': VM is a snapshot. Clone the parent VM instead." -ErrorId 'CannotCloneSnapshot'
            return
        }

        # Generate clone name if not provided
        $cloneName = if ($Name) {
            $Name
        } else {
            "$($targetVM.Name)_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
        }

        # Build action body
        $body = @{
            vm     = $targetVM.Key
            action = 'clone'
            params = @{
                name          = $cloneName
                preserve_macs = $PreserveMACAddresses.IsPresent
            }
        }

        $macText = if ($PreserveMACAddresses) { ' (preserving MAC addresses)' } else { '' }

        if ($PSCmdlet.ShouldProcess($targetVM.Name, "Clone VM to '$cloneName'$macText")) {
            try {
                Write-Verbose "Cloning VM '$($targetVM.Name)' to '$cloneName'"
                $response = Invoke-VergeAPI -Method POST -Endpoint 'vm_actions' -Body $body -Connection $Server

                Write-Verbose "Clone command sent for VM '$($targetVM.Name)'"

                # Wait for clone to be created
                Start-Sleep -Seconds 3

                # Try to get the cloned VM
                $clonedVM = Get-VergeVM -Name $cloneName -Server $Server | Select-Object -First 1

                if ($clonedVM) {
                    Write-Verbose "Clone '$cloneName' created with Key: $($clonedVM.Key)"

                    # Power on if requested
                    if ($PowerOn) {
                        Write-Verbose "Powering on cloned VM '$cloneName'"
                        Start-VergeVM -Key $clonedVM.Key -Server $Server
                        Start-Sleep -Milliseconds 500
                        $clonedVM = Get-VergeVM -Key $clonedVM.Key -Server $Server
                    }

                    if ($PassThru) {
                        Write-Output $clonedVM
                    }
                }
                else {
                    Write-Warning "Clone operation initiated but cloned VM '$cloneName' not immediately available. It may still be creating."
                }
            }
            catch {
                Write-Error -Message "Failed to clone VM '$($targetVM.Name)': $($_.Exception.Message)" -ErrorId 'CloneFailed'
            }
        }
    }
}