NLVMUG 2018
This week, I had the opportunity to speak at the Dutch VMUG (NLVMUG) about a project I did last year.
Title of my session was: Migrate your datacenter without downtime
Session abstract
Moving a complete datacenter 50 KM, without downtime?
A few years ago this was unthinkable, but with the help of VMware techniques this is possible nowadays. In this customer case, Michael Wilmsen shows you how he did this for a university, using VMware techniques and PowerCLI. Not only the process of script development, but you will also get a better understanding of how the VMware techniques work and how they are applicable.
Many thanks for those who attend my session!
My room was packed and I got some good feedback from the crowed.
As promised, below the script I’ve developed and here presentation in pdf.
Feel free to make changes or improvements to the script.
#Filename: MoveVM.ps1 #Author: M. Wilmsen #Version: 0.9 #Date: 11-9-2017 <# .SYNOPSIS Script to migrate a virtual machine .DESCRIPTION Script to migrate compute and storage from cluster to cluster. Log will be in c:\logs\MigrateVM[-timestamp].log .PARAMETER VMList (mandatory) Path to file where vm are listed to be migrated .PARAMETER SourceCluster Source VMware vSphere cluster .PARAMETER DestinationCluster (mandatory) Destination VMware vSphere cluster .PARAMETER SourceVC (mandatory) Source VMware vCenter server .PARAMETER DestionationVC (mandatory) Destination VMware vCenter server .PARAMETER Username (mandatory) Username with right to migrate VM .PARAMETER Password (mandatory) vCenter password .PARAMETER DryRun (optional) Script will run, but no actions will be performed .PARAMETER SendWhatsApp (optional) Send a email with status updates. A text file with email adress must be in de current working directory .\email.txt .EXAMPLE MigrateVM.ps1 -VMList c:\vmlist.txt -SourceCluster OTA01 -DestionationCluster OTA02 -SourceVC dc2vcs02.campus.eur.nl -Username sys12345@eur.nl -DestionationDatastore DatastoreName -DryRun $True -Email $True #> Param ( [Parameter(Mandatory=$true)] [string]$VMList = $null, [Parameter(Mandatory=$true)] [string]$SourceCluster = $null, [Parameter(Mandatory=$true)] [string]$DestinationCluster = $null, [Parameter(Mandatory=$true)] [string]$SourceVC = $null, [Parameter(Mandatory=$true)] [string]$DestinationVC = $null, [Parameter(Mandatory=$true)] [string]$Username = $null, [string]$Password, [bool]$Dryrun = $false, [bool]$SendWhatsApp = $false ) ################################## INIT ################################################# #Set WebOperation timeout #set-PowerCLIConfiguration -WebOperationTimeoutSeconds 3600 #Define Global variables $PingVM = $false $WhatsAppNumbers = "[insert tel nr]" $WhatsAppGroup = "[insert groupname]" #Define LogFile with time stamp $LogTime = Get-Date -Format "MM-dd-yyyy_hh-mm-ss" $LogPath = ".\" if([IO.Directory]::Exists($LogPath)) { #Do Nothing!! } else { New-Item -ItemType directory -Path $LogPath } $LogFile = $LogPath+"MigrateVM-"+$LogTime+".log" ################################## END INIT ################################################# ################################## FUNCTIONS ################################################# #Define log function Function LogWrite { Param ([string]$logstring) #Add logtime to entry $LogTime = Get-Date -Format "MM-dd-yyyy_hh-mm-ss" $logstring = $LogTime + " : " + $logstring #Write logstring Add-content $LogFile -value $logstring Write-Host $logstring } #Define SendWhatsApp function Function SendWhatsApp { Param ([string] $message) if ( $SendWhatsApp ) { $LogTime = Get-Date -Format "MM-dd-yyyy_hh-mm-ss" $message = $logtime + " : " + $message $instanceId = "2" $clientId = "[email]" #change this line $clientSecret = "[secret]" #change this line foreach ( $number in $WhatsAppNumbers ) { $jsonObj = @{'group_admin'=$number; 'group_name'=$WhatsAppGroup; 'message'=$message;} Try { $res = Invoke-WebRequest -Uri "http://api.whatsmate.net/v2/whatsapp/group/message/$instanceId" ` -Method Post ` -Headers @{"X-WM-CLIENT-ID"=$clientId; "X-WM-CLIENT-SECRET"=$clientSecret;} ` -Body (ConvertTo-Json $jsonObj) LogWrite "WhatsMate Status Code: " $res.StatusCode LogWrite $res.Content } } } } function Get-VmSize($vm) { #Initialize variables $VmDirs =@() $VmSize = 0 $searchSpec = New-Object VMware.Vim.HostDatastoreBrowserSearchSpec $searchSpec.details = New-Object VMware.Vim.FileQueryFlags $searchSpec.details.fileSize = $TRUE Get-View -VIObject $vm | % { #Create an array with the vm's directories $VmDirs += $_.Config.Files.VmPathName.split("/")[0] $VmDirs += $_.Config.Files.SnapshotDirectory.split("/")[0] $VmDirs += $_.Config.Files.SuspendDirectory.split("/")[0] $VmDirs += $_.Config.Files.LogDirectory.split("/")[0] #Add directories of the vm's virtual disk files foreach ($disk in $_.Layout.Disk) { foreach ($diskfile in $disk.diskfile){ $VmDirs += $diskfile.split("/")[0] } } #Only take unique array items $VmDirs = $VmDirs | Sort | Get-Unique foreach ($dir in $VmDirs){ $ds = Get-Datastore ($dir.split("[")[1]).split("]")[0] $dsb = Get-View (($ds | get-view).Browser) $taskMoRef = $dsb.SearchDatastoreSubFolders_Task($dir,$searchSpec) $task = Get-View $taskMoRef while($task.Info.State -eq "running" -or $task.Info.State -eq "queued"){$task = Get-View $taskMoRef } foreach ($result in $task.Info.Result){ foreach ($file in $result.File){ $VmSize += $file.FileSize } } } } return $VmSize } ################################## END FUNCTIONS ################################################# Logwrite "Start Virtual Machine Move" #If WhatsApp make notice if ( $SendWhatsApp ) { LogWrite "Notifications will be send using WhatsApp to WhatsApp Group: $WhatsAppGroup" } #If DryRun make Notice if ( $Dryrun ) { Logwrite "Start move virtual machines (DryRun)" SendWhatsApp "Start move virtual machines (DryRun)" } else { Logwrite "Start move virtual machines" SendWhatsApp "Start move virtual machines" } #Login to vCenter servers if ( -NOT $Password ) { $PasswordSec = Read-Host "Give vCenter password" -AsSecureString $Password = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($PasswordSec)) } #SourceVC $ConnectVC = Connect-VIServer $SourceVC -Username $Username -Password $Password $Message = "Connecting to " + $ConnectVC + " as " + $Username Logwrite $Message #DestionationVC $ConnectVC = Connect-VIServer $DestinationVC -Username $Username -Password $Password $Message = "Connecting to " + $ConnectVC + " as " + $Username Logwrite $Message #Main Script Get-Content $VMList | Foreach-Object { #Set $MigError to false befor migration $MigError = $false #Get VM variables $vm = get-vm $_ $vmip = $vm | Select @{N="IP Address";E={@($_.guest.IPAddress[0])}} $vmip = $vmip."ip address" $VMHDDSize = Get-VmSize($vm) $VMHDDSize = [Math]::Round(($VMHDDSize / 1GB),2) $vmfolder = $vm.folder $vmfolder = $vmfolder.name $NetworkAdapter = Get-NetworkAdapter -VM $vm -Server $SourceVC $SourceVMPortGroup = Get-NetworkAdapter -vm $vm | Get-VDPortgroup $DestinationVMPortgroup = $SourceVMPortGroup.name -replace $SourceCluster,$DestinationCluster $switchname = $DestinationCluster $destinationDatastore = Get-DatastoreCluster –Name $DestinationCluster –Server $DestinationVC | Get-Datastore | Sort-Object -Property FreeSpaceGB -Descending | Select-Object -First 1 $destinationDatastoreFreeSpace = $destinationDatastore | Select Name,@{N=”FreeSpace”;E={$_.ExtensionData.Summary.FreeSpace}} $destinationDatastoreFreeSpace = [Math]::Round(($destinationDatastoreFreeSpace."FreeSpace" / 1GB),2) $DestinationPortgroup = Get-VDPortgroup -VDSwitch $switchname -Name $DestinationVMPortgroup -Server $destVCConn $DestinationHost = Get-Cluster –Name $DestinationCluster –Server $DestinationVC | Get-VMHost -State Connected | Select-Object –First 1 LogWrite "Start move: $vm" Logwrite "VM IP: $vmip" Logwrite "VM Disk Used (GB): $VMHDDSize" Logwrite "VM Folder: $vmfolder" Logwrite "Source vCenter: $SourceVC" Logwrite "VM Source Cluster: $SourceCluster" Logwrite "Destination vCenter: $DestinationVC" Logwrite "VM Destination Cluster: $DestinationCluster" Logwrite "Destination host: $DestinationHost" LogWrite "VM Source PortGroup: $SourceVMPortGroup" LogWrite "VM Destination Portgroup: $DestinationPortgroup" Logwrite "VM Destination Datastore: $destinationDatastore" LogWrite "Destination Datastore FreeSpace GB: $destinationDatastoreFreeSpace " if ( $Dryrun ) { $FreespaceAfterMigration = $destinationDatastoreFreeSpace - $VMHDDSize if ( $FreespaceAfterMigration -lt 0 ) { Logwrite "ERROR: Datastore $destinationDatastore does not have sufficient freespace! Virtual Machine needs $VMHDDSize. Only $destinationDatastoreFreeSpace available." } else { Logwrite "Virtual Machine will fit on datastore $destinationDatastore. Freespace after migration is: $FreespaceAfterMigration GB" } } #Test if VM responsed to ping #if (Test-Connection -comp $vmip -quiet) { # LogWrite "Virtual Machine $vm response to ping before move, virtual machine will be check after move" # $PingVM = $true #} #else { # Logwrite "Virtual Machine does not response to ping, no ping check after move" #} if ($vmip -eq $null) { LogWrite "Virtual Machine ip address not known" } elseif (Test-Connection -comp $vmip -quiet) { LogWrite "Virtual Machine $vm response to ping before being moved. Virtual machine will be checked after being moved" $PingVM = $true } else { Logwrite "Virtual Machine does not respond to ping. No ping check will be performed after moving the Virtual Machine" } #if ( $VMHDDSize -eq if ( -NOT $Dryrun) { #Migrate VM to cluster LogWrite "Move $vm to vCenter $DestinationVC and datastore $DestinationDatastore" Try { $Result = Move-VM -VM $vm ` -Destination $DestinationHost ` -Datastore $DestinationDatastore ` -NetworkAdapter $NetworkAdapter ` -PortGroup $DestinationPortgroup ` -ErrorAction Stop } Catch { $ErrorMessage = $_.Exception.Message LogWrite "ERROR: Move of $vm to cluster $DestinationHost failed!!!" Logwrite "ERROR: Move Status Code: $ErrorMessage" SendWhatsApp "ERROR: Move of $vm failed!!! $ErrorMessage" $MigError = $true } #Migrate VM to folder LogWrite "Move $vm to vCenter $vmfolder" Try { $VMtemp = get-vm $vm $Result = Move-VM -VM $vmtemp ` -Destination $vmfolder ` -ErrorAction Stop } Catch { $ErrorMessage = $_.Exception.Message LogWrite "ERROR: Move of $vm to folder $vmfolder failed!!!" Logwrite "ERROR: Move Status Code: $ErrorMessage" SendWhatsApp "ERROR: Move of $vm failed!!! $ErrorMessage" $MigError = $true } } $MigError = $false #Test if VM is running on destination cluster if ( -NOT $MigError -AND -NOT $Dryrun ) { LogWrite "Check $vm is registered in $DestinationVC" try { $CheckVM = get-vm -name $vm -server $DestinationVC -ErrorAction Stop if ( $CheckVM ) { Logwrite "$vm registered in $DestinationVC" } else { Logwrite "ERROR: $vm not found in $DestinationVC" } } catch { $ErrorMessage = $_.Exception.Message Logwrite "ERROR: $vm not found in $DestinationVC" Logwrite "ERROR: $ErrorMessage" SendWhatsApp "ERROR move: $vm not found in $DestinationVC" } } #Test is VM response to ping, if $PingVM = $True if ($PingVM) { if (Test-Connection -comp $vmip -quiet) { LogWrite "Virtual Machine $vm response to ping after move" } } sleep 1 SendWhatsApp "Finished move action: $vm from $SourceVC to $DestinationVC" Logwrite "Finished move action: $vm from $SourceVC to $DestinationVC" } #Disconnect from vCenter servers Logwrite "Disconnect from vCenter servers $SourceVC $DestinationVC" Disconnect-viserver $SourceVC -Confirm:$false Disconnect-viserver $DestinationVC -Confirm:$false Logwrite "Finished moving virtual machines, exiting....." SendWhatsApp "Move finished, exit. Have a nice day!" Remove-Variable PasswordSec Remove-Variable Password
About Michael
Michael Wilmsen is a experienced VMware Architect with more than 20 years in the IT industry. Main focus is VMware vSphere, Horizon View and Hyper Converged with a deep interest into performance and architecture.
Michael is VCDX 210 certified, has been rewarded with the vExpert title from 2011, Nutanix Tech Champion and a Nutanix Platform Professional.