RapidClone.psm1

$global:vmwaresession = ""
$bootVmScriptBlock = {
    sleep $args[0]
    try{
        $vcenter = Connect-VIServer -Server $args[2] -Session $args[3]
    }catch{
        Write-Error "Failed to connect to vcenter {0}" -f $args[2]
    }
    try{
        Start-VM -RunAsync -VM $args[1]
    }catch{
        Write-Error "Failed to start vm {0}" -f $args[1]
    }
}
function GetTag($name,$category){
# -----------------------------
# Get/Set vm tag
# -----------------------------
    if($name){
        try{
            $cat = Get-TagCategory -Name $category
        }catch{
            $cat = New-TagCategory -Name $category -EntityType VM -Description "Used for rapid cloning"
        }
        try{
            $tag = Get-Tag -Name $name -Category $category
        }catch{
            $tag = New-Tag -Name $name -Category $category
        }
        return $tag
    }else{
        return $null
    }
}
function WriteWithColor($text,$state,$color){
# -----------------------------
# fancy output with color and state
# -----------------------------
    Write-Host "[" -NoNewline
    Write-Host $state -NoNewline -ForegroundColor $color
    Write-Host ("] {0}" -f $text)
}
function ProcessVmTasks($tasks,$description,[switch]$loop,[switch]$startvm,$startjobs,[switch]$ignoreTags,[switch]$nooutput,$outputhash){
# -----------------------------
# a vm task processor
# -----------------------------
    while(($tasks.Count -ne 0) -or ($startvm -and ($startjobs.Count -ne 0))){

        # define a list of tasks to be remove in case of completed
        $todelete = @()

        # loop all vm tasks names
        foreach($t in $tasks.Keys){
            try{

                # get vm task
                $task = get-task -id $tasks[$t].id -ErrorAction stop

                # if success
                if($task.State -eq "Success"){

                    if(-not $nooutput){
                        $outputhash[$t].state = "Success"
                    }

                    # do we need to start the vm ?
                    if(-not $startvm){

                        # if not, we are done, set as success
                        WriteWithColor -text ("{0} {1}" -f $description,$t) -state "Success" -color green
                    }else{

                        # start the vm in the background using a start-job
                        # we add a random delay to not stress the vm environment
                        $delay = random -Minimum 0 -Maximum $RandomBootDelaySeconds
                        $startjobs[$t] = start-job -Name "Starting $t" -ScriptBlock $bootVmScriptBlock -ArgumentList @($delay,$t,$Vcenter,$global:vmwaresession)    
                        
                        # set as cloned
                        WriteWithColor -text ("{0} {1} [starting with delay of $delay]" -f $description,$t) -state "Cloned" -color yellow

                    }

                    # if creating clones, we might register tags
                    if($tagshash[$t] -and -not $ignoreTags){
                        $vm = get-vm $t
                        $out = New-TagAssignment -Tag $tagshash[$t] -Entity $vm
                               
                    }                            

                    # mark to remove from task list, cloning is done
                    $todelete+=$t

                }elseif($task.State -ne "Running" -and $task.State -ne "Queued"){

                    # something went wrong here, it's not success nor running
                    WriteWithColor -text $t -state $task.State -color red
                    if(-not $nooutput){
                        $outputhash[$t].state = $task.State
                    }
                    # mark to remove
                    $todelete+=$t
                }

            } catch{$todelete+=$t}
        }   

        # process remove tasks
        $todelete | %{$tasks.Remove($_)} 

        # checking start jobs status
        $todelete = @()

        # we loop all started jobs
        foreach($j in $startjobs.Keys){
            try{

                # get the job status
                $job = get-job -id $startjobs[$j].id -ErrorAction stop

                # if completed
                if($job.State -eq "Completed"){

                        # get the actual result
                        $result = $job | receive-job

                        if($result.State -eq "Running"){
                        WriteWithColor -text $j -state "Started" -color green
                        }else{
                        WriteWithColor -text $j -state $result.State -color red
                        }

                        # mark to delete from jobs
                        $todelete+=$j

                }elseif($job.State -ne "Running" -and $job.State -ne "Queued"){

                    # something went wrong, nor completed, nor running
                    WriteWithColor -text $j -state $job.State -color red
                    $todelete+=$j
                }
            } catch{
                WriteWithColor -text $_.Exception.Message -state "Warning" -color yellow
                $todelete+=$j
            }
        }   

        # remove jobs
        $todelete | %{$startjobs.Remove($_)} 

        # do we go in a loop or not ?
        if($loop){
            sleep 1
        }else{
            break
        }
    }
}
function CheckPlugin($hostname,$hostId){
# -----------------------------
# checks if the nas plugin is installed
# -----------------------------
    $list = @()
    if($hostId){
        $vmHost = Get-VMHost -Id $hostId
    }else{
        $vmHost = Get-VMHost -Name $hostname
    }
    if($vmHost){
        $vmHostName = $vmhost.Name
        $esxcli = $vmHost | Get-EsxCli -V2
        $list = $esxcli.software.vib.list.Invoke()
        $plugin = $list| ?{$_.Name -match "NetappNasPlugin"}
        if($plugin){
            WriteWithColor -text ("[{0}] {1} version {2}" -f $vmHostName,$plugin.Name,$plugin.Version) -state "Installed" -color green
        }else{
            Throw "[$vmHostName] The NetAppNasPlugin is not installed"
        }

    }
}
function GetResourcePoolHosts($resourcepool){
    try{
        $rp = Get-ResourcePool -Name $resourcepool
        $cluster = Get-Cluster -Id $rp.ExtensionData.Owner
        return $cluster.ExtensionData.Host
    }catch{
        Throw "ResourcePool [$resourcepool] is not in a vmware cluster"
    }
}
function getNewCloneNames($nametemplate,$count,$fillGaps){

    $nametemplateNrPlaceholder = ""
    if($nametemplate -match "(#+)"){
        $nametemplateNrPlaceholder = $Matches[1]
        WriteWithColor -text "Name template [$NameTemplate]" -state "Validated" -color green
    }else{
        throw "No proper name template [$NameTemplate], it must contain 1 or more hashtags (#) as number placeholders to make the clones unique."
    }

    $vmSearchPattern = $nametemplate -replace $nametemplateNrPlaceholder,"*"
    $vmNumberRegex =  $nametemplate -replace $nametemplateNrPlaceholder,"([0-9]+)"

    $currentVms = @(get-vm $vmSearchPattern) | %{$_.Name}
    $freeNumbers = @()
    $takenNumbers = @()

    foreach($vm in $currentVms){
        if($vm -match $vmNumberRegex){
            $takenNumbers+=[int]($Matches[1])
        }
    }
    $freeNumbers = @(1..10000) | ?{$_ -notin $takenNumbers}
    $lastNumber = 0
    if($takenNumbers.Count -gt 0){
        $lastNumber = ($takenNumbers | Sort-Object)[-1]
    }

    if($fillGaps){
        $newNumbers = $freeNumbers | Sort-Object | select -first $count
    }else{
        $newNumbers = ($lastNumber+1)..($lastNumber+$count)
    }

    return $newNumbers | %{ $nametemplate -replace $nametemplateNrPlaceholder,([string]($_)).PadLeft($nametemplateNrPlaceholder.Length,"0") }
}
function vmExists($name,[switch]$passthru){
    $check = $null
    try{
        $check = get-vm -Name $name
    }catch{
    }
    if(-not $passthru){
        return [bool]$check
    }else{
        return $check
    }
}
function vmHostExists($name,[switch]$passthru){
    $check = $null
    try{
        $check = Get-VMHost -Name $name
    }catch{
    }
    if(-not $passthru){
        return [bool]$check
    }else{
        return $check
    }
}
function resourcePoolExists($name,[switch]$passthru){
    $check = $null
    try{
        $check = Get-ResourcePool -Name $name
    }catch{
    }
    if(-not $passthru){
        return [bool]$check
    }else{
        return $check
    }
}
function datastoreExists($name,[switch]$passthru){
    $check = $null
    try{
        $check = Get-Datastore -Name $name
    }catch{
    }
    if(-not $passthru){
        return [bool]$check
    }else{
        return $check
    }
}
function osCustomizationSpecExists($name,[switch]$passthru){
    $check = $null
    try{
        $check = Get-OSCustomizationSpec -Name $name
    }catch{
    }
    if(-not $passthru){
        return [bool]$check
    }else{
        return $check
    }
}
function parseVm($vm,[switch]$ignoreErrors,[switch]$silent,[switch]$noTemplateCheck){
# -----------------------------
# checks a vm and returns a hash with common info
# -----------------------------
    $return = @{}
    $templatevm = $null
    if(-not $noTemplateCheck){
        try{
            # is template ?
            $templatevm = get-template -Name $vm
            $return["vmhost"] = (Get-VMHost -id ($templatevm.HostId)).Name
            $return["istemplate"]=$true
            if(-not $silent){
                WriteWithColor -text "template [$vm]" -state "Exists" -color green
            }
        }catch{}
    }
    if(-not $templatevm){
        try{
            # is normal vm
            $templatevm = get-vm -name $vm
            if(-not $silent){
                WriteWithColor -text "Source vm [$vm]" -state "Exists" -color green
            }
            $return["vmhost"] = $templatevm.VMHost.Name
            $return["istemplate"] = $false
        }catch{
            if(-not $ignoreErrors){
                Throw "VM or Template [$vm] not found"
            }
        }
    }
 
    if($templatevm){
        # if found
        $return["name"] = $vm
        $return["tags"] = @{}
        $return["datastore"] = ""
        $return["resourcepool"] = ""
        try{
            $currentDatastores = @($templatevm.ExtensionData.Storage.PerDatastoreUsage)
            if(($currentDatastores.Count -gt 1) -and (-not $ignoreErrors)){
                Throw "The source vm has more than 1 datastore, rapid cloning not possible"
            }
            $return["datastore"] = (Get-Datastore -id ($currentDatastores[0].Datastore)).Name
        }catch{
            Throw "No source datastore present"
        }
        if($templatevm.ResourcePool.Name -ne "Resources"){
            $return["resourcepool"] = $templatevm.ResourcePool.Name
        }
        $tags = (Get-TagAssignment -Entity $templatevm).Tag
        $tagshash[$vm] = $tags

        foreach($tag in $tags){
            $return["tags"][$tag.Category.Name] = $tag.Name
        }
    }

    return $return
}
function stopVms($vms,[switch]$silent){
    $stoptasks = @{}
    foreach($v in $vms){
        try{
            $vm = get-vm $v

            if($vm.PowerState -eq "PoweredOn"){
                $stoptasks["$vm"] = Stop-VM -VM $vm -RunAsync -Confirm:$false
            }else{
                WriteWithColor -text "Stopping clone $vm" -state $vm.PowerState -color green
            }
            ProcessVmTasks -tasks $stoptasks -description "Stopping clone" -ignoreTags -nooutput
        }catch{
            if(-not $silent){
                WriteWithColor -text "Stopping clone $v" -state "Unknown vm" -color red
            }
        }
    }
    ProcessVmTasks -tasks $stoptasks  -description "Stopping clone" -loop -ignoreTags -nooutput
}
function removeVms($vms,[switch]$silent){
    $removetasks = @{}
    foreach($v in $vms){

        try{
            $vm = get-vm $v
            $removetasks["$vm"] = Remove-VM -VM $vm -DeletePermanently -RunAsync -Confirm:$false
            ProcessVmTasks -tasks $removetasks -description "Removing clone" -ignoreTags -nooutput
        }catch{
            if(-not $silent){
                WriteWithColor -text "Removing clone $v" -state "Unknown vm" -color red
            }
        }
    }
    ProcessVmTasks -tasks $removetasks  -description "Removing clone" -loop -ignoreTags -nooutput
}
function initTempTemplates($datastores,$sourcevmhash){
# -----------------------------
# creates datastore-local templates for rapid cloning
# -----------------------------
    $ttemplates = @()
    $datastores = @($datastores)
    $cDatastore = $sourcevmhash.datastore
    $cHost = $sourcevmhash.vmhost
    $cIstemplate = $sourcevmhash.istemplate
    try{
        foreach($datastore in $datastores){

            if($datastore -eq $cDatastore){
                WriteWithColor -text "[$datastore] is the source datastore, rapid cloning" -state "Info" -color yellow
            }else{
                WriteWithColor -text "[$datastore] is not the source datastore, a slow copy is needed first" -state "Info" -color yellow
            }
            $copyName = "{0}__{1}__{2}" -f $datastore,$sourcevmhash.name,(get-date -Format "yyyyMMdd")
            $ttemplates+=$copyName

            if((vmExists -name $copyName) -and $ReuseExistingTemporaryTemplates){
                WriteWithColor -text "Re-using existing temporary template [$copyName]" -state "Success" -color green
            }else{
                removeVms -vms @($copyName) -silent
                if($cIstemplate){
                    $task = New-Vm -VMHost $cHost -Datastore $datastore -Template $sourcevmhash.name -Name $copyName -RunAsync
                }else{
                    $task = New-Vm -VMHost $cHost -Datastore $datastore -VM $sourcevmhash.name -Name $copyName -RunAsync
                }
                $out = $task | Wait-Task
                WriteWithColor -text "Temporary template [$copyName]" -state "Success" -color green
            }
        }
    }catch{
        WriteWithColor -text $_.Exception.Message -state "Error" -color red
        WriteWithColor -text $_.Exception.InnerException.Message -state "Error" -color red
        if($RemoveTemporaryTemplates){
            removeVms -vms @($ttemplates) -silent
        }
        Throw "Something went wrong in preparing templates"
    }
    return $ttemplates
}
function cloneVm($name,$vmhost,$resourcepool,$oscustomizationspec,$sourcevm,$datastore,$istemplate){

    $expression = "New-VM -Name $name"

    if($istemplate){
        $expression += " -Template"
    }else{
        $expression += " -VM"
    }
    $expression += " $sourcevm -Datastore $datastore -RunAsync"

    if($resourcepool){
        $expression += " -ResourcePool $resourcepool"
    }else{
        $expression += " -VMHost $vmhost"
    }

    if($OSCustomizationSpec){
        $expression += " -OsCustomizationSpec $oscustomizationspec"
    }
    Write-Verbose "Invoking : $expression"
    return Invoke-Expression -Command $expression
}
function connectVcenter($Vcenter,$Credentials){
    if($Credentials){
        $VcenterServer = Connect-VIServer $Vcenter -Credential $Credentials
    }else{
        $VcenterServer = Connect-VIServer $Vcenter
    }
    $global:vmwaresession = $VcenterServer.SessionId
}

<#
.Synopsis
   Rapidly clones multiple vm's by leveraging VAAI and the NetApp Nas Plugin.
.DESCRIPTION
   Using NetApps Nas Plugin and the vmware VAAI technology,
   vmware can leverage NetApp file cloning technology
   and thus rapidly (in seconds) clone vm's.
   This cmdlet allows you to easily clone a source vm or a source template.
   When rapid cloning is not possible (for example template not on
   then same datastore), this cmdlet will optimize the cloning process
   by distributing the source template to each datastore and then
   use rapid cloning withing the same datastore.
.EXAMPLE
    New-RapidClone -Vcenter jabba.slash.local -Credentials $credentials -SourceVm centostemplate -NameTemplate "test_##" -Count 3 -StartVM -ResourcePool RP1 -NoNetappNasPluginCheck
 
    [Connected] Vcenter Connection [jabba.slash.local]
    [Exists] Source vm [centostemplate]
    [Exists] ResourcePool is provided [RP1]
    [Validated] Name template [test_##]
    [Info] Inheriting datastore from source [r2d2-ds01]
    [Info] [r2d2-ds01] is the source datastore, rapid cloning
    [Success] Temporary template [r2d2-ds01__centostemplate__20200518]
    [Cloned] Adding clone test_01 [starting with delay of 11]
    [Cloned] Adding clone test_02 [starting with delay of 3]
    [Cloned] Adding clone test_03 [starting with delay of 50]
    [Started] test_02
    [Started] test_01
    [Started] test_03
    [Success] Removing clone r2d2-ds01__centostemplate__20200518
    [Disconnected] Vcenter Connection [jabba.slash.local]
 
.OUTPUTS
   The output is a list objects with the clone information. Pipe it for example to convertto-csv
   and out-file to save the results in a CSV-file.
#>

function New-RapidClone{
    [cmdletbinding()]
    param(

        # Vcenter server
        [Parameter(Mandatory=$true)]
        [string]$Vcenter,

        # PSCredentials, if non given, we assume the current user has direct access.
        [PSCredential]$Credentials,

        # The source vm name or template name
        [Parameter(Mandatory=$true)]
        [string]$SourceVm,

        # The naming convention template. Add hashtags (#) where you want auto numbering.
        # The script is smart enough to check whether
        [Parameter(Mandatory=$true)]
        [string]$NameTemplate,

        # Number of clones to create
        [Parameter(Mandatory=$true)]
        [int]$Count,

        # Indicates if the clones need to be booted.
        # Booting happens in the background and a random delay is added to not boot all clones at once
        [switch]$StartVM=$false,

        # This will not re-create temorary templates, but re-use existing ones if they exist
        # When running tests, they might gain time, as you slow copied templates can be re-used.
        # Use in combination with switch RemoveTemporaryTemplates, by setting it to $false.
        [switch]$ReuseExistingTemporaryTemplates,

        # Remove temporary template after execution
        # When running tests, you may turn these off to avoid re-copying templates
        # Use the ReuseExistingTemporaryTemplates flag to re-use them
        [switch]$RemoveTemporaryTemplates=$true,

        # The maximum random boot delay in seconds. If you create more clones, you could make this higher to spread the boot processes.
        [int]$RandomBootDelaySeconds=60,

        # The script will automatically search for the last number and continue from there.
        # This switch will start from 1 instead and start filling gaps.
        [switch]$FillGaps,

        # The script does a Netapp Nas Plugin check. With this flag you can omit this check
        # If you pass a resourcepool, we assume the resourcepool is on a vmware cluster and we will check every host in the cluster
        [switch]$NoNetappNasPluginCheck,
    
        # A list of datastore over which the script should spread the clones.
        # If non are given. The datastore where the source vm lives is used.
        [string[]]$Datastores,

        # The destination VMHost
        # You can also choose for a resource pool
        # If both are missing, the VMHost of the source VM is used.
        [string]$VMHost,

        # The destination ResourcePool
        # You can also choose for a single VMHost
        # If both are missing, the VMHost of the source VM is used.
        [string]$ResourcePool,

        # OSCustomizationSpec
        [string]$OSCustomizationSpec
    )
    
    Begin{

        $tagshash = @{} 
        $ErrorActionPreference = "stop"

        # ----------------------------------
        # Connecting to vcenter
        # ----------------------------------
        try{
            ConnectVcenter -Vcenter $Vcenter -Credentials $Credentials
            WriteWithColor -text "Vcenter Connection [$Vcenter]" -state "Connected" -color green
        }catch{
            Throw "Failed to connect to vcenter $Vcenter"
        }

    }
    Process{
        try{

            $currentHost = ""
            $currentDatastore = ""
            $sourceVmIsTemplate=$false

            # ----------------------------------
            # Checking source vm
            # ----------------------------------
            $vmhash = parseVm -vm $SourceVm
            $currentHost = $vmhash.vmhost
            $currentDatastore = $vmhash.datastore
            $sourceVmIsTemplate = $vmhash.istemplate

            # ----------------------------------
            # check vm host
            # ----------------------------------
            if($VMHost){
                if(vmHostExists -name $VMHost){
                    WriteWithColor -text "VMHost is provided [$VMHost]" -state "Exists" -color green
                }else{
                    Throw "The provided VMHost [$VMHost] is not found."
                }
            }
                
            # ----------------------------------
            # check resource pool
            # ----------------------------------
            if($ResourcePool){
                if(resourcePoolExists -name $ResourcePool){
                    WriteWithColor -text "ResourcePool is provided [$ResourcePool]" -state "Exists" -color green
                }else{
                    Throw "The provided ResourcePool [$ResourcePool] is not found."
                }
            }
                
            # ----------------------------------
            # fall back to template resources
            # ----------------------------------
            if(-not $VMHost -and -not $ResourcePool){
                WriteWithColor -text "Inheriting destination from source [$currentHost]" -state "Info" -color cyan
                $VMHost = $currentHost
            }

            # ----------------------------------
            # check if Netapp Nas Plugin installed
            # ----------------------------------
            if(-not $NoNetappNasPluginCheck -and $VMHost){
                CheckPlugin -hostname $VMHost
            }
            if(-not $NoNetappNasPluginCheck -and $ResourcePool){
                $rpHosts = GetResourcePoolHosts -resourcepool $ResourcePool
                $rpHosts | %{CheckPlugin -hostid $_}
            }
            # ----------------------------------
            # check OSCustomizationSpec
            # ----------------------------------
            if($OSCustomizationSpec){
                if(osCustomizationSpecExists -name $OSCustomizationSpec){
                    WriteWithColor -text "OSCustomizationSpec is provided [$OSCustomizationSpec]" -state "Exists" -color green
                }else{
                    Throw "The provided OSCustomizationSpec [$OSCustomizationSpec] is not found."
                }
            }

            # ----------------------------------
            # Getting new free auto increment names
            # ----------------------------------
            $cloneNames = getNewCloneNames -nametemplate $NameTemplate -count $Count -fillGaps $FillGaps

            # ----------------------------------
            # searching and checking datastores
            # ----------------------------------
            if($Datastores){
                foreach($datastore in $Datastores){
                    if(datastoreExists -name $datastore){
                        WriteWithColor -text "Datastore [$datastore]" -state "Exists" -color green
                    }else{
                        Throw "Datastore [$datastore] not found"
                    }
                }

            }else{
                WriteWithColor -text "Inheriting datastore from source [$currentDatastore]" -state "Info" -color cyan
                $Datastores = @($currentDatastore)
            }

            #####################################
            # AT THIS POINT EVERYTHING IS CHECKED
            # NOW THE REAL COPYING STARTS
            # FIRST WE WILL MAKE SURE THAT EVERY
            # DATASTORE HAS A COPY
            #####################################
            $tempTemplates = @()
            $tempTemplates+=initTempTemplates -datastores $Datastores -sourcevmhash $vmhash

            # ----------------------------------
            # Start cloning
            # ----------------------------------
            $clonetasks = @{}
            $startjobs = @{}
            $outputhash = @{}

            # Getting tags
            $tags = @()
            $tags += GetTag -name $SourceVm -category ParentTemplate
            $tags += GetTag -name $NameTemplate -category NameTemplate
            $tags += GetTag -name $OSCustomizationSpec -category OsCustomizationSpec
            $tags = $tags | ?{$_}  # remove empty tags
          
            $i=0
            $roundrobin=0

            # create all clones
            foreach($clonename in $cloneNames){
                $i++
                $pct = [int]($i/$count*100)
                Write-Progress -Activity "Cloning" -Status "Adding $clonename" -PercentComplete $pct

                # round robin get template & datastore
                $thisdatastore = $Datastores[$roundrobin]
                $thistemplate = $tempTemplates |?{([string]$_).StartsWith("{0}__" -f $thisdatastore)}
                $roundrobin++
                if($roundrobin -ge $Datastores.Count){
                    $roundrobin=0
                }
                # register the tags
                $tagshash[$clonename] = $tags

                # set outputhash
                $outputhash[$clonename]=@{}
                $outputhash[$clonename]["vm"] = $clonename
                $outputhash[$clonename]["vmhost"] = $VMHost
                $outputhash[$clonename]["datastore"] = $thisdatastore
                $outputhash[$clonename]["parenttemplate"] = $SourceVm
                $outputhash[$clonename]["nametemplate"] = $NameTemplate
                $outputhash[$clonename]["oscustomizationspec"] = $OSCustomizationSpec
                $outputhash[$clonename]["resourcepool"] = $ResourcePool

                # invoke the clone
                $clonetasks["$clonename"] = cloneVm -name $clonename -vmhost $VMHost -resourcepool $ResourcePool -oscustomizationspec $OSCustomizationSpec -sourcevm $thistemplate -datastore $thisdatastore

                # process completed tasks
                ProcessVmTasks -tasks $clonetasks -description "Adding clone" -startjobs $startjobs -startvm:$StartVM  -outputhash $outputhash
            }
            # process final tasks
            Write-Progress -Activity "Adding clone task" -Status "Done" -Completed
            ProcessVmTasks -tasks $clonetasks -description "Adding clone" -startjobs $startjobs -startvm:$StartVM -loop  -outputhash $outputhash

        }catch{
            WriteWithColor -text $_.Exception.Message -state "Error" -color red
            if($_.Exception.InnerException.Message){
                WriteWithColor -text $_.Exception.InnerException.Message -state "Error" -color red
            }
        }
    }
    End{
        try{

            # remove templates if required
            if($RemoveTemporaryTemplates){
                removeVms -vms @($tempTemplates) -silent
            }

            # disconnect
            $out = Disconnect-VIServer -Server $vcenter -Confirm:$false
            WriteWithColor -text "Vcenter Connection [$Vcenter]" -state "Disconnected" -color green
        }catch{}

        # send to output stream for further piping
        foreach($h in $outputhash.Keys){
            New-Object -TypeName PSObject -Property $outputhash[$h]
        }    
    }
}

<#
.Synopsis
   Rapidly re-clones multiple vm-clones by leveraging VAAI and the NetApp Nas Plugin.
.DESCRIPTION
   Using NetApps Nas Plugin and the vmware VAAI technology,
   vmware can leverage NetApp file cloning technology
   and thus rapidly (in seconds) clone vm's.
   This cmdlet allows you to easily re-clone previously clones vm's.
   Using tags on the vm's, we remember the original template
   and re-deploy it with the same name.
   When rapid cloning is not possible (for example template not on
   then same datastore), this cmdlet will optimize the cloning process
   by distributing the source template to each datastore and then
   use rapid cloning withing the same datastore.
.EXAMPLE
    Update-RapidClone -Vcenter jabba.slash.local -Credentials $credentials -StartVM -Names (get-vm test*).Name
 
    [Connected] Vcenter Connection [jabba.slash.local]
    [Success] Stopping clone test_01
    [Success] Stopping clone test_02
    [Success] Stopping clone test_03
    [Success] Removing clone test_01
    [Success] Removing clone test_02
    [Success] Removing clone test_03
    [Exists] Source vm [centostemplate]
    [Info] [r2d2-ds01] is the source datastore, rapid cloning
    [Success] Temporary template [r2d2-ds01__centostemplate__20200518]
    [Cloned] Adding clone test_01 [starting with delay of 12]
    [Cloned] Adding clone test_02 [starting with delay of 34]
    [Cloned] Adding clone test_03 [starting with delay of 15]
    [Started] test_01
    [Started] test_03
    [Started] test_02
    [Success] Removing clone r2d2-ds01__centostemplate__20200518
    [Disconnected] Vcenter Connection [jabba.slash.local]
.OUTPUTS
   The output is a list objects with the clone information. Pipe it for example to convertto-csv
   and out-file to save the results in a CSV-file.
#>

function Update-RapidClone{

    [cmdletbinding()]
    param(

        # Vcenter server
        [Parameter(Mandatory=$true)]
        $Vcenter,

        # PSCredentials, if non given, we assume the current user has direct access.
        [PSCredential]$Credentials,

        # Indicates if the clones need to be booted.
        # Booting happens in the background and a random delay is added to not boot all clones at once
        [switch]$StartVM=$false,

        # This will not re-create temorary templates, but re-use existing ones if they exist
        # When running tests, they might gain time, as you slow copied templates can be re-used.
        # Use in combination with switch RemoveTemporaryTemplates, by setting it to $false.
        [switch]$ReuseExistingTemporaryTemplates,

        # Remove temporary template after execution
        # When running tests, you may turn these off to avoid re-copying templates
        # Use the ReuseExistingTemporaryTemplates flag to re-use them
        [switch]$RemoveTemporaryTemplates=$true,

        # The maximum random boot delay in seconds. If you create more clones, you could make this higher to spread the boot processes.
        [int]$RandomBootDelaySeconds=60,

        # An array of vm names to be re-deployed
        [Parameter(Mandatory=$true)]
        [string[]]$Names
    )

    Begin{

        $tagshash = @{} 
        $ErrorActionPreference = "stop"

        # ----------------------------------
        # Connecting to vcenter
        # ----------------------------------
        try{
            ConnectVcenter -Vcenter $Vcenter -Credentials $Credentials
            WriteWithColor -text "Vcenter Connection [$Vcenter]" -state "Connected" -color green
        }catch{
            Throw "Failed to connect to vcenter $Vcenter"
        }

    }
    Process{
        try{

            $startjobs = @{}
            $clonetasks = @{}
            $templateshash = @{}

            # we analyze all passed vm's
            $vmhashlist = $Names | %{parseVm -vm $_ -ignoreErrors -silent -noTemplateCheck}
            $tempTemplates = @()
            $sourcevmhashes = @{}
            $outputhash = @{}

            # remove the old clones
            stopVms -vms $Names
            removeVms -vms $Names

            # find all template-datastore combinations
            foreach($vmhash in $vmhashlist){
                $parenttemplate = $vmhash.tags.ParentTemplate
                $datastore = $vmhash.datastore
                if(-not $templateshash.ContainsKey($parenttemplate)){
                    $templateshash[$parenttemplate] = @{}
                    $templateshash[$parenttemplate]["data"] = parseVm -vm $parenttemplate
                    $templateshash[$parenttemplate]["datastores"] = @()
                }

                if($datastore -notin $templateshash[$parenttemplate]["datastores"]){
                    $templateshash[$parenttemplate]["datastores"] += $datastore
                }
            }   

            # prep all templates for rapid cloning
            foreach($k in $templateshash.Keys){
                $tempTemplates+=initTempTemplates -datastores $templateshash[$k].datastores -sourcevmhash $templateshash[$k].data
            }

            # we loop the list
            $i = 0
            foreach($vmhash in $vmhashlist){

                $vmname = $vmhash.name
                $vmhost = $vmhash.vmhost
                $datastore = $vmhash.datastore
                $parenttemplate = $vmhash.tags.ParentTemplate
                $nametemplate = $vmhash.tags.NameTemplate
                $oscustomizationspec = $vmhash.tags.OsCustomizationSpec
                $resourcepool = $vmhash.resourcepool

                # set output data
                $outputhash[$vmname] = @{}
                $outputhash[$vmname]["vm"] = $vmname
                $outputhash[$vmname]["vmhost"] = $vmhost
                $outputhash[$vmname]["datastore"] = $datastore
                $outputhash[$vmname]["parenttemplate"] = $parenttemplate
                $outputhash[$vmname]["nametemplate"] = $nametemplate
                $outputhash[$vmname]["oscustomizationspec"] = $oscustomizationspec
                $outputhash[$vmname]["resourcepool"] = $resourcepool

                # progress bar
                $i++
                $pct = [int]($i/$vmhashlist.Keys.Count*100)
                Write-Progress -Activity "Cloning" -Status "Adding $clonename" -PercentComplete $pct

                # quick check if the vm is compliant to this script, we expect certain tags
                if(-not $parenttemplate){
                    WriteWithColor -text "$vmname is not tagged with ParentTemplate" -state "Warning" -color yellow
                    continue
                }

                # redeploy the clones
                $clonetasks[$vmname] = clonevm -name $vmname -vmhost $vmhost -resourcepool $resourcepool -oscustomizationspec $oscustomizationspec -sourcevm $parenttemplate -datastore $datastore

                # check if tasks have completed
                ProcessVmTasks -tasks $clonetasks -description "Adding clone" -startjobs $startjobs -startvm:$StartVM -outputhash $outputhash

            }    

            # finalize tasks
            Write-Progress -Activity "Adding clone task" -Status "Done" -Completed
            ProcessVmTasks -tasks $clonetasks -description "Adding clone" -startjobs $startjobs -startvm:$StartVM -loop -outputhash $outputhash
                    
 
        }catch{
            WriteWithColor -text $_.Exception.Message -state "Error" -color red
            if($_.Exception.InnerException.Message){
                WriteWithColor -text $_.Exception.InnerException.Message -state "Error" -color red
            }
        }
    }
    End{
        try{

            # remove templates if required
            if($RemoveTemporaryTemplates){
                removeVms -vms @($tempTemplates) -silent
            }

            # disconnect
            $out = Disconnect-VIServer -Server $vcenter -Confirm:$false
            WriteWithColor -text "Vcenter Connection [$Vcenter]" -state "Disconnected" -color green
        }catch{}

        # send to output stream for further piping
        foreach($h in $outputhash.Keys){
            New-Object -TypeName PSObject -Property $outputhash[$h]
        }   
    }
}

<#
.Synopsis
   Rapidly removes multiple vm's.
.DESCRIPTION
   This cmdlet is dangerous. It rapidly stops and remove vm's in bulk.
.EXAMPLE
    Remove-RapidClone -Vcenter jabba.slash.local -Credentials $credentials -Names (get-vm test*).name -Force
    [Connected] Vcenter Connection [jabba.slash.local]
    [Success] Stopping clone test_01
    [Success] Stopping clone test_02
    [Success] Stopping clone test_03
    [Success] Removing clone test_01
    [Success] Removing clone test_02
    [Success] Removing clone test_03
    [Disconnected] Vcenter Connection [jabba.slash.local]
 
#>

function Remove-RapidClone{

    [cmdletbinding(SupportsShouldProcess = $true,ConfirmImpact='High')]
    param(

        # Vcenter server
        [Parameter(Mandatory=$true)]
        $Vcenter,

        # PSCredentials, if non given, we assume the current user has direct access.
        [PSCredential]$Credentials,

        # An array of vm names to be removed
        [Parameter(Mandatory=$true)]
        [string[]]$Names,

        # Same as -confirm:$false
        [switch]$Force


    )
    Begin{

        ###################################
        # START CODE
        ###################################
        $ErrorActionPreference = "stop"

        # ----------------------------------
        # Connecting to vcenter
        # ----------------------------------
        try{
            ConnectVcenter -Vcenter $Vcenter -Credentials $Credentials
            WriteWithColor -text "Vcenter Connection [$Vcenter]" -state "Connected" -color green
        }catch{
            Throw "Failed to connect to vcenter $Vcenter"
        }

    }
    Process{
        try{

            #####################################
            # REMOVE ACTION
            #####################################

            if ($Force -or $PSCmdlet.ShouldProcess($Names,"Remove vm's")) {

                stopVms -vms $Names
                removeVms -vms $Names
            }
  
        }catch{
            WriteWithColor -text $_.Exception.Message -state "Error" -color red
            if($_.Exception.InnerException.Message){
                WriteWithColor -text $_.Exception.InnerException.Message -state "Error" -color red
            }
        }
    }
    End{
        try{

            # disconnect
            $out = Disconnect-VIServer -Server $vcenter -Confirm:$false
            WriteWithColor -text "Vcenter Connection [$Vcenter]" -state "Disconnected" -color green
        }catch{}
    }
}

Export-ModuleMember -Function New-RapidClone
Export-ModuleMember -Function Update-RapidClone
Export-ModuleMember -Function Remove-RapidClone