PowerCLI: Linked VMs

March 31, 2015 – 9:58 pm

My current employer has a pre-production environment that is being refreshed nightly from production using a PowerCLI script. I started looking into it, to better understand the inner workings and to see if I could improve things. The objective is to have a pre-production environment that is an identical copy of the production, that can be deployed and refreshed quickly. A full clone could be an option but doesn’t meet some of the requirements. I call these Linked VMs, as they are not Linked Clones.

Here’s how it works.

A snapshot is taken of the production servers, thereby “freezing” the base VMDK disk. A new VM is created for the pre-production and it uses the “frozen” base disk of the production server as it’s own disk, in non-persistent mode. The VM is of course connected to an isolated port group. This basically allows the dev/infra team to have an identical copy of the production servers at any time (just run the refresh script).

While this works very well, I have concerns about using production disks for this purpose. I usually prefer to leave production alone and have either full clones or fully provisioned pre-prod environment. But it’s been running safely for a few years this way so far…

The initial pre-production linked VMs were created manually and I wanted to automate the creation of these pre-prod shells. Here’s a summary of what this script does

  • Removes all snapshots on the source VM
  • Clones the source VM without the virtual hard drives
  • Link the source VM’s hard drives to the new clone in non-persistent mode
  • Change the network interface port group to an isolated one
  • Create a snapshot on the source VM (this shields the production VM from the “sharing” of it’s disks)

And here’s the source:

#=============================================================================
# Script       : Linked VM Creation
# Author       : Marc Bouchard
# Credits      : Luc Dekens, Rob Girard
# Version      : 1.0
# Revision     : 03/31/2015
# Purpose      : Creates a non-persistent copy of a production VM, for testing
#                and pre-production purposes.
#
# Parameters   : Source VM
#
# Command Line: Create-Preprod.ps1 -SourceVM "SERVERA"
#=============================================================================

#=============================================================================
# Command line parameters
#=============================================================================

param (
	[Parameter(Mandatory=$true)]
	[String]$SourceVM = $(throw "Name of source VM required")
)

#=============================================================================
# Screen setup
#=============================================================================

# set regular console colours
[console]::backgroundcolor = "black"
[console]::foregroundcolor = "white"

# clear screen
clear-host

#=============================================================================
# --- Load Powershell Snap-ins
#=============================================================================

if (!(get-pssnapin -name VMware.VimAutomation.Core -erroraction 'SilentlyContinue')) {
	Write-Host "[INFO] Adding PowerCLI Snapin"
	add-pssnapin VMware.VimAutomation.Core -ErrorAction 'SilentlyContinue'
	if (!(get-pssnapin -name VMware.VimAutomation.Core -erroraction 'SilentlyContinue')) {
		Write-Host "[ERROR] PowerCLI snapin NOT added" -ForegroundColor Red
	} Else {
		Write-Host "[INFO] PowerCLI snapin added"
	}
}

#=============================================================================
# CUSTOMIZE THIS SECTION ONLY FOR YOUR ENVIRONMENT
#=============================================================================

$ScriptLocation="C:\Scripts\PreProd\"

$DateOnly = Get-Date -f yyyy-MM-dd

$CredUser = "ServiceAccountUserID"
$CredFile = $ScriptLocation + "Create-PreProd.crd"
$vCenter = "vCenterServerName"
$Prefix = "PRE_"
$IsolatedPortGroup = "IsolatedPortGroupName"

#=============================================================================
# --- Function: New-LinkedVM
#=============================================================================

function New-LinkedVM {

Param
	(
		[parameter(Mandatory=$true)]
 		[ValidateNotNullOrEmpty()]
 		[PSObject]$LinkSource,

 		[parameter(Mandatory=$true)]
 		[ValidateNotNullOrEmpty()]
 		[String]$LinkTarget,

		[parameter(Mandatory=$false)]
 		[ValidateNotNullOrEmpty()]
 		[String]$LinkPortGroup,

		[parameter(Mandatory=$false)]
 		[ValidateNotNullOrEmpty()]
 		[String]$LinkFolder
	)

#=============================================================================

try {
	if ($LinkSource.GetType().Name -eq "string"){
		try {
			$LinkSource = Get-VM $LinkSource -ErrorAction Stop
		}
		catch [Exception]{
			Write-Host "[WARNING] Source VM does not exist ($LinkSource)" -ForegroundColor Yellow
			Return
		}
	}
	elseif ($LinkSource -isnot [VMware.VimAutomation.ViCore.Impl.V1.Inventory.VirtualMachineImpl]){
	 	Write-Host "[WARNING] Invalid object" -ForegroundColor Yellow
 		Return
 	}

	$Exists = Get-VM $LinkTarget -ErrorAction SilentlyContinue
	if ($Exists){
		Write-Host "[WARNING] Target VM already exists ($LinkTarget)" -ForegroundColor Yellow
		Return
	}

	#=============================================================================
	# --- Remove old snapshots
	#=============================================================================
	
	Write-Host "[INFO]    Removing snapshots from source VM: " $SourceVM
	$Snapshot = Get-Snapshot -VM $LinkSource
	
	if ($Snapshot) {
		$Task = Get-VM $LinkSource | Get-Snapshot | Remove-Snapshot -confirm:$false -runasync
		wait-task -Task $Task
	}

	#=============================================================================
	# --- Get source VM information
	#=============================================================================

	if ($PSBoundParameters.ContainsKey('VMFolder')){
		try {
			$Folder = Get-Folder $LinkFolder -Type VM -ErrorAction Stop
 			$CloneFolder = $Folder.ExtensionData.MoRef
		}
		catch [Exception] {
			Write-Host "[WARNING] VM Folder $VMFolder does not exist, using existing folder instead" -ForegroundColor Yellow
 			$CloneFolder = $LinkSource.ExtensionData.Parent
 		}
	}
	else {
		$CloneFolder = $LinkSource.ExtensionData.Parent
	}
		
	#=============================================================================
	# --- Create clone specifications and exclude source hard disks
	#=============================================================================
	
	$CloneSpec = New-Object -TypeName VMware.Vim.VirtualMachineCloneSpec
	$CloneSpec.Location = New-Object -TypeName VMware.Vim.VirtualMachineRelocateSpec
	$CloneSpec.Location.Datastore = $LinkSource.ExtensionData.Datastore[0]
	$CloneSpec.Location.Host = $LinkSource.ExtensionData.Runtime.Host
	$CloneSpec.Config = New-Object -TypeName VMware.Vim.VirtualMachineConfigSpec
 
	Get-HardDisk -VM $LinkSource | %{
  		$DeviceChange = New-Object -TypeName VMware.Vim.VirtualDeviceConfigSpec
  		$DeviceChange.Device = $_.ExtensionData
  		$DeviceChange.Operation = [VMware.Vim.VirtualDeviceConfigSpecOperation]::remove
  		$DeviceChange.FileOperation = [VMware.Vim.VirtualDeviceConfigSpecFileOperation]::destroy
  		$CloneSpec.Config.DeviceChange += $DeviceChange
	}
 
	#=============================================================================
	# --- Create clone
	#=============================================================================

	$CloneTaskMoRef=$LinkSource.ExtensionData.CloneVM_Task($CloneFolder,$LinkTarget,$CloneSpec)
	$CloneTask=Get-View $CloneTaskMoRef
	while("running","queued" -contains $CloneTask.Info.State){
  		sleep 1
  		$CloneTask.UpdateViewData("Info.State")
	}

	#=============================================================================
	# --- Add SourceVM hard disks in non-persistent mode
	#
	# NOTE: The source VM cannot have any snapshots, otherwise the New-HardDisk
	#       addition will fail.
	#=============================================================================

	$AllDisks = Get-HardDisk -VM $LinkSource 
	ForEach ($HD in $AllDisks) {
		Write-Host "[INFO]    Reconfiguring HD: " $HD
		New-HardDisk -VM $LinkTarget -Persistence IndependentNonPersistent -DiskPath $HD.Filename | Out-Null
		}

	#=============================================================================
	# --- Change network interface port groups to isolated network
	#=============================================================================

	$AllNICs = Get-NetworkAdapter -VM $LinkTarget
	
	ForEach ($NIC in $AllNICs) {
		Write-Host "[INFO]    Reconfiguring NIC: " $NIC
		Set-NetworkAdapter -NetworkAdapter $NIC -Portgroup $LinkPortGroup -Confirm:$false | Out-Null
		}

	#=============================================================================
	# --- Create new snapshot for source VM
	#=============================================================================

	Write-Host "[INFO]    Creating snapshot on source VM: " $SourceVM
	$Task = New-Snapshot -VM $LinkSource -Name ($(Get-Date -format s)) -runasync 
	wait-task -Task $Task  | Out-Null

}
catch [Exception]{
	throw "[ERROR]   New-LinkedVM failed to complete..."
	}
}
#=============================================================================
# --- MAIN PROGRAM
#=============================================================================

Write-Host "[INFO]    ======================================"
Write-Host "[INFO]           Create Linked VM Script"
Write-Host "[INFO]    ======================================"
Write-Host "[INFO]"

#=============================================================================
# --- Credentials management
#=============================================================================

if ((Test-Path $CredFile) -eq $false) {
	$cred = new-object system.management.automation.pscredential $CredUser,
		(read-host -assecurestring -prompt "Enter password for privileged account")
      try
      {
         $cred.Password | ConvertFrom-SecureString | Set-Content $CredFile -ErrorAction Stop
      }
      catch
      {
         Write-Host "[ERROR]   Credentials file creation problem" -ForegroundColor Red
         Exit
      }
}
else {
   $password = get-content $CredFile | convertto-securestring
	$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $CredUser,$password
}

#=============================================================================
# --- Connect to vCenter server
#=============================================================================

try
{
	Write-Host "[INFO]    Connecting to vCenter: " $vCenter
	Connect-VIServer -server $vCenter -credential $cred -ErrorAction Stop -WarningAction SilentlyContinue | Out-Null
}
Catch
{
	Exit
}

#=============================================================================
# --- Create Linked VM
#=============================================================================

$LinkedVM = $Prefix + $SourceVM

New-LinkedVM -LinkSource $SourceVM -LinkTarget $LinkedVM -LinkPortGroup $IsolatedPortGroup

#=============================================================================
# --- Close session
#=============================================================================

Disconnect-VIServer -server $vCenter -confirm:$false -force
Write-Host "[INFO]    Disconnecting from vCenter"
Write-Host

Post a Comment