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.

RSS feed for comments on this post.

Leave a Reply

You must be logged in to post a comment.