Powershell – Split PFX certificates

Standard

Hi!!
It’s been awhile since I’ve posted something but last week I wrote a small script which I think will be useful to you!

Part of my many tasks where I work is to manage our PKI and in general work with and issue certificates. When issuing certificates (which include the private key) using a Windows PKI you normally export the file in PFX format. This is useful when working with Windows servers or applications. However in Linux servers or applications it’s more common that you need the certificate split into two files e.g. cert.crt/cert.key which separate the public/private keys.
In order to convert/split the PFX file into two files and separate the public/private keys you need to use openssl and manually run specific commands. This can be a bit tricky if you aren’t familiar with openssl and certificates in general.

The script I wrote does this for you automatically and can also optionally convert the private key to PEM format or decrypt the private key so it isn’t password protected (some applications need this). This can save you a lot of time and hopefully make your lives easier 🙂

Prerequisites

You need to install OpenSSL as the script uses this to perform the different actions.

I have built the script in three different sections – Parameters, Initialize and Script Main. To begin with I will quickly explain the Parameters section.

PARAMETERS

#PARAMETERS
$openssl = 'C:\OpenSSL-Win64\bin\openssl.exe'
$sourcePFX = 'c:\mycert.pfx'
$sourcePFX_Passphrase = 'somepassword'
$convertToPEM = $true
$decryptPrivateKey = $true

$openssl – Path to your openssl.exe here.
$sourcePFX – Path to the PFX you want to connvert/split.
$sourcePFX_Passphrase – password of the $sourcePFX file.
$convertToPEM – Set to $true to convert the private key to PEM format. $false to ignore/not do it.
$decryptPrivateKey – Set to $true to decrypt the private key and remove the password protection (don’t give it out to anyone who’s not supposed to have it after this…). $false to ignore/not do it.

INITIALIZE

$sourcePFX_Dir = (Get-ChildItem $sourcePFX).DirectoryName
$PrivateKeyFile = $sourcePFX.Replace('.pfx', '-encrypted.key')
$PublicKeyFile = $sourcePFX.Replace('.pfx', '.crt')
$ErrorActionPreference = 'SilentlyContinue'

In this section use the parameters given to create some variables I will using in the script main.
$sourcePFX_Dir = (Get-ChildItem $sourcePFX).DirectoryName
Here I get the path of the directory of the source PFX so we can create the new certificates in the same place.

$PrivateKeyFile = $sourcePFX.Replace(‘.pfx’, ‘-encrypted.key’)
$PublicKeyFile = $sourcePFX.Replace(‘.pfx’, ‘.crt’)
Here I create file names of the new certs based on source PFX name.

$ErrorActionPreference = ‘SilentlyContinue’
Here I tell powershell to not display errors when running the script. This is because openssl returns messages that cause powershell to error out (even when it succeeds). This is a bit confusing so I chose to remove the errors displayed. You can remove this if you want as it’s not crucial.

SCRIPT MAIN

The embedded comments in this section should be enough to tell you what I’m doing here. Basically I start with running commands that extract the public and private keys into two new files. Then I add a few if statements that convert the converts to PEM and/or decrypts the private key if you set those params to true earlier.

#SCRIPT MAIN
#Extract the private key
& $openssl 'pkcs12' -in $sourcePFX -nocerts -out $PrivateKeyFile -password pass:$sourcePFX_Passphrase -passout pass:$sourcePFX_Passphrase 
#Extract the public key
& $openssl pkcs12 -in $sourcePFX -clcerts -nokeys -out $PublicKeyFile -password pass:$sourcePFX_Passphrase
#if specified convert private key to PEM
if ($convertToPEM)
{
	$PrivateKeyPEMFile = $PrivateKeyFile.Replace('.key', '-pem.key')
	& $openssl rsa -in $PrivateKeyFile -outform PEM -out $PrivateKeyPEMFile -passin pass:$sourcePFX_Passphrase -passout pass:$sourcePFX_Passphrase
}
#If specified decrypt the private key
if ($decryptPrivateKey)
{
	if ($convertToPEM)
	{
		$PrivateKeyFile = $PrivateKeyPEMFile
	}
	$decryptedKeyFile = $PrivateKeyFile.Replace('.key', '-decrypted.key').Replace('-encrypted','')
	& $openssl rsa -in $PrivateKeyFile -out $decryptedKeyFile -passin pass:$sourcePFX_Passphrase -passout pass:$sourcePFX_Passphrase
}

That’s it!!
I hope you find the script useful!
I have copied in the full script below for you to copy if needed.

<#	
	.NOTES
	===========================================================================	 
	 Created on:   	29-Mar-2017 12:16 PM
	 Created by:   	Noam Wajnman	  
	 Filename:		Split-PFXCertificate.ps1     	
	===========================================================================
	.DESCRIPTION
		Splits a PFX certificate into a public/private key pair. See the Parameters 
		section	to optionally convert to PEM and/or decrypt the private key.
#>
#PARAMETERS
$openssl = 'C:\OpenSSL-Win64\bin\openssl.exe'
$sourcePFX = 'c:\mycert.pfx'
$sourcePFX_Passphrase = 'somepassword'
$convertToPEM = $true
$decryptPrivateKey = $true
#INITIALIZE
$sourcePFX_Dir = (Get-ChildItem $sourcePFX).DirectoryName
$PrivateKeyFile = $sourcePFX.Replace('.pfx', '-encrypted.key')
$PublicKeyFile = $sourcePFX.Replace('.pfx', '.crt')
$ErrorActionPreference = 'SilentlyContinue'
#SCRIPT MAIN
#Extract the private key
& $openssl 'pkcs12' -in $sourcePFX -nocerts -out $PrivateKeyFile -password pass:$sourcePFX_Passphrase -passout pass:$sourcePFX_Passphrase 
#Extract the public key
& $openssl pkcs12 -in $sourcePFX -clcerts -nokeys -out $PublicKeyFile -password pass:$sourcePFX_Passphrase
#if specified convert private key to PEM
if ($convertToPEM)
{
	$PrivateKeyPEMFile = $PrivateKeyFile.Replace('.key', '-pem.key')
	& $openssl rsa -in $PrivateKeyFile -outform PEM -out $PrivateKeyPEMFile -passin pass:$sourcePFX_Passphrase -passout pass:$sourcePFX_Passphrase
}
#If specified decrypt the private key
if ($decryptPrivateKey)
{
	if ($convertToPEM)
	{
		$PrivateKeyFile = $PrivateKeyPEMFile
	}
	$decryptedKeyFile = $PrivateKeyFile.Replace('.key', '-decrypted.key').Replace('-encrypted','')
	& $openssl rsa -in $PrivateKeyFile -out $decryptedKeyFile -passin pass:$sourcePFX_Passphrase -passout pass:$sourcePFX_Passphrase
}

Remove a VMFS datastore using powershell

Standard

Recently I had to remove a lot of VMFS datastores from our VMWare infrastructure as we had migrated to new storage and no longer needed them.
In order to remove a datastore from your vmware infrastructure correctly you must follow the instructions given in this article. http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2004605
Basically it says you have to make sure nothing is using the datastore and then unmount and detach the datastore from each connected host. Since we work with large clusters with many ESXi hosts I didn’t enjoy the prospect of manually unmounting and detaching each datastore from each host. To avoid this I found this article http://blogs.vmware.com/vsphere/2012/01/automating-datastore-storage-device-detachment-in-vsphere-5.html which explains how to automate datastore removal. It’s a great article and includes a few functions which give you the ability to unmount and detach datastores like described above. I simply used these functions to create the script in this blog.

If the functions are already there then why use this script?

I wrote this script so that it does everything for you in one go. You don’t have to mess around with the functions or cmd-lets yourself. Also, I made a few changes to the mentioned functions which I feel improve them. Basically all you have to do is put in the name of the datastore and run the script.

What does the script do exactly?

1. The script gets the datastore info and then asks for confirmation before beginning the removal process.
2. The datastore is unmounted on each host connected to it.
3. The datastore is detached on each host connected to it.
4. At the end of the script run the datastore name and LUN canonical name is returned. This is useful as you will probably need this information when you have to delete the LUN in your storage system later on.

How do I run the script?

I. Go through the checklist “Unmounting a LUN checklist” here http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2004605 and make sure your datastore is ready for removal.
II.Copy the script to your computer and open it with and editor like powershell_ISE or powerGUI.
III. In the #Parameters section enter the name of your vCenter server and the name of the datastore you wish to remove.
IV. Hit F5 to run the script.

The script itself

I’m not going to go into details about the code as you can see all that in the links to KB articles above. I have simply copied in the script for you here. Please leave a comment if you find the script useful. Enjoy!

#############################################################################################################
##script:			Unmount_Detach-Datastore.ps1
##
##Description:		Unmounts and detaches a given datastore from all its connected hosts.
#+					Before running make sure that the datastore doesn't have any live VMs or templates on it.
#Additional info:	http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2004605
#Additional info:	http://blogs.vmware.com/vsphere/2012/01/automating-datastore-storage-device-detachment-in-vsphere-5.html
#Additional info:	https://communities.vmware.com/docs/DOC-18008
##Created by:		Noam Wajnman
##Creation Date:	August 4, 2014
##############################################################################################################
function Connect-Vcenter {
	#############################################################################################
	##Function:			Connect-Vcenter
	##
	##Description:		Loads the VMWare snap-in for powershell and connects to vcenter.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	20 November, 2012
	##Updated:			31 July, 2014
	##############################################################################################
	param(
		$vcenter
	)
	#Load snap-in
	if (-not (Get-PSSnapin -Name VMware.VimAutomation.Core -erroraction "silentlycontinue" )) {
        Write-Host "Importing VMWare Snap-in VMware.VimAutomation.Core..."    
		Add-PSSnapin VMware.VimAutomation.Core
	}
    Set-PowerCLIConfiguration -DefaultVIServerMode Single -invalidCertificateAction "ignore" -confirm:$false | out-null
	#Connect to vCenter
	if ($global:DefaultVIServers.Count -lt 1) {
        Write-Host "Connecting to vcenter server $vcenter..."		
		Connect-VIServer $vCenter
	}
}
Function Get-DatastoreMountInfo {
	#function copied from https://communities.vmware.com/docs/DOC-18008
	[CmdletBinding()]
	Param (
		[Parameter(ValueFromPipeline=$true)]
		$Datastore
	)
	Process {
		$AllInfo = @()
		if (-not $Datastore) {
			$Datastore = Get-Datastore
		}
		Foreach ($ds in $Datastore) {  
			if ($ds.ExtensionData.info.Vmfs) {
				$hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].diskname
				if ($ds.ExtensionData.Host) {
					$attachedHosts = $ds.ExtensionData.Host
					Foreach ($VMHost in $attachedHosts) {
						$hostview = Get-View $VMHost.Key
						$hostviewDSState = $VMHost.MountInfo.Mounted
						$StorageSys = Get-View $HostView.ConfigManager.StorageSystem
						$devices = $StorageSys.StorageDeviceInfo.ScsiLun
						Foreach ($device in $devices) {
							$Info = '' | Select Datastore, VMHost, Lun, Mounted, State
							if ($device.canonicalName -eq $hostviewDSDiskName) {
								$hostviewDSAttachState = ''
								if ($device.operationalState[0] -eq "ok") {
									$hostviewDSAttachState = "Attached"							
								} elseif ($device.operationalState[0] -eq "off") {
									$hostviewDSAttachState = "Detached"							
								} else {
									$hostviewDSAttachState = $device.operationalstate[0]
								}
								$Info.Datastore = $ds.Name
								$Info.Lun = $hostviewDSDiskName
								$Info.VMHost = $hostview.Name
								$Info.Mounted = $HostViewDSState
								$Info.State = $hostviewDSAttachState
								$AllInfo += $Info
							}
						}
						
					}
				}
			}
		}
		$AllInfo
	}
}
Function Detach-Datastore {
	#function based on code found here https://communities.vmware.com/docs/DOC-18008
	[CmdletBinding()]
	Param (
		[Parameter(ValueFromPipeline=$true)]
		$Datastore
	)
	Process {
		if (-not $Datastore) {
			Write-Host "No Datastore defined as input"
			Exit
		}
		Foreach ($ds in $Datastore) {
			$hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].Diskname
			if ($ds.ExtensionData.Host) {
				$attachedHosts = $ds.ExtensionData.Host
				Foreach ($VMHost in $attachedHosts) {
					$hostview = Get-View $VMHost.Key
					$StorageSys = Get-View $HostView.ConfigManager.StorageSystem
					$devices = $StorageSys.StorageDeviceInfo.ScsiLun
					Foreach ($device in $devices) {
						if ($device.canonicalName -eq $hostviewDSDiskName) {
							#If the device is attached then detach it (I added this to the function to prevent error messages in vcenter when running the script)
							if ($device.operationalState[0] -eq "ok") { 
								$LunUUID = $Device.Uuid
								Write-Host "Detaching LUN $($Device.CanonicalName) from host $($hostview.Name)..."
								$StorageSys.DetachScsiLun($LunUUID);
							}
							#If the device isn't attached then skip it (I added this to the function to prevent error messages in vcenter when running the script)
							else {
								Write-Host "LUN $($Device.CanonicalName) is not attached on host $($hostview.Name)..."
							}
						}
					}
				}
			}
		}
	}
}
Function Unmount-Datastore {
	#function based on code found here https://communities.vmware.com/docs/DOC-18008
	[CmdletBinding()]
	Param (
		[Parameter(ValueFromPipeline=$true)]
		$Datastore
	)
	Process {
		if (-not $Datastore) {
			Write-Host "No Datastore defined as input"
			Exit
		}
		Foreach ($ds in $Datastore) {
			$hostviewDSDiskName = $ds.ExtensionData.Info.vmfs.extent[0].Diskname
			if ($ds.ExtensionData.Host) {
				$attachedHosts = $ds.ExtensionData.Host
				Foreach ($VMHost in $attachedHosts) {
					$hostview = Get-View $VMHost.Key
					$mounted = $VMHost.MountInfo.Mounted
					#If the device is mounted then unmount it (I added this to the function to prevent error messages in vcenter when running the script)
					if ($mounted -eq $true) {
						$StorageSys = Get-View $HostView.ConfigManager.StorageSystem
						Write-Host "Unmounting VMFS Datastore $($DS.Name) from host $($hostview.Name)..."
						$StorageSys.UnmountVmfsVolume($DS.ExtensionData.Info.vmfs.uuid);
					}
					#If the device isn't mounted then skip it (I added this to the function to prevent error messages in vcenter when running the script)
					else {
						Write-Host "VMFS Datastore $($DS.Name) is already unmounted on host $($hostview.Name)..."
					}
				}
			}
		}
	}
}
#VARIABLES
#Parameters 
$vcenter = "Some_Vcenter_Server"
$DSName = "Some_Datastore"
#SCRIPT MAIN
clear
Connect-Vcenter $vcenter
$datastore = Get-Datastore -Name $DSName
$CanonicalName = $datastore.ExtensionData.Info.Vmfs.Extent[0].DiskName
$GoAhead = Read-Host "Are you sure that you want to unmount and detach: `n$DSName `n$CanonicalName`nfrom all its connected hosts?"
if ($GoAhead -eq "yes" -or $GoAhead -eq "y" -or $GoAhead -eq "Y") {
	Write-Host "Unmounting datastore $DSName..."	
	$datastore | Unmount-Datastore
	Write-Host "Detaching datastore $DSName from hosts..."
	$datastore | Detach-Datastore
}
$DSName
$CanonicalName

Get windows time settings from remote servers

Standard

I recently had to deal with some issues with time on our servers and I wanted to get an overview of our environment. To do that I wrote the script in this post which gets the time, timesource (NTP servers) and time zone information from remote windows servers and exports the information to a handy CSV file.

How does the script work?

1. The script loops through all the computers listed in “servers.txt” which much be created and placed in the script directory.
2. Using the function “Test-PortAlive” (I already covered this in a different blog post) the script checks that the ports 135 (rpc) and 445 (smb) are open on the server. This is to prevent the script from timing out and hanging on servers where these ports are blocked. This allows for a quick script run even with many servers.
3. If the ports are open and queries with .net and wmi are possible the script then gets the time, time source and time zone information from the server.
4. When all the information has been retrieved the results are exported to a CSV file in the script directory.

How did I build the script?

I divided the script into three sections Functions, Variables and Script Main to simplify the structure of the script.
The script has four functions which provide the main functionality.
I. Get-TimeSource
This function uses .net to query the remote registry for the NTP server settings.
II. Get-TimeZone
WMI is used to get the time zone configured on the remote server.
III. Get-Time
WMI is used to get the time on the remote server returned as a datetime object.
IV. Test-PortAlive
A function I wrote a while back which checks if given ports are open on a remote server. Here it is used to check if ports 135 and 445 are open on the remote server as this is needed to perform the queries for time configuration information.

In the #script main section I then loop through the servers and for each one execute the above functions to get the info. At then end the information is exported to CSV.

The script itself!

#############################################################################################
##Get-RemoteTime.ps1
##
##Description:		Loops through all the computers listed in "servers.txt" in the script 
#+					directory and for each one gets the timesource, timezone and the time 
#+					using .NET and WMI. Exports the results to a CSV file at the end of the
#+					script run.
##Created by:		Noam Wajnman
##Creation Date:	July 16, 2013
##Updated:			July 31, 2014
##############################################################################################
#FUNCTIONS
function Get-TimeSource {
	#############################################################################################
	##Function:			Get-RemoteTimeSource
	##
	##Description:		Pipeline function. Gets the configured NTP server timesource from the 
	#+					registry of a remote server.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	31 July, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param (
		[Parameter(ValueFromPipeline=$true)]$server
	)
	begin {        
	    $key = "SYSTEM\\CurrentControlSet\\Services\\W32Time\\Parameters" #path/name of the reg key
    }
	process {
		$timesource = '' | select "computer","NTPServer","Type" #Create a custom object properties "computer", "NTPServer" and "Type"		
		$timesource.computer = $server #set the computer property on the custom object		
		$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $server) #connect to the remote registry
		$regKey= $reg.OpenSubKey($key) #get the registry key		
		$str_value1 = $regKey.GetValue("NTPServer")	#get the value of "NTPServer"	
		$value1 = $str_value1 -split ","		
		$timesource.NTPServer = $value1[0] #split the value and set the NTPServer property on the custom object to the first returned timesource.		
		$value2 = $regKey.GetValue("Type") #get the value of "Type"		
		$timesource.Type = $value2 #set the Type property on the custom object		
		$reg.close() #close the remote registry connection when done.
		return $timesource #return the custom object.
	}
}
function Get-TimeZone {
	#############################################################################################
	##Function:			Get-RemoteTimeZone
	##
	##Description:		Pipeline function. Gets the configured time zone from the 
	#+					remote server using WMI.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	31 July, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param (
		$server
	)
	process {
		$timeZone = (gwmi win32_timeZone -ComputerName $server).caption	
		return $timeZone
	}
	
}
function Get-Time {
	#############################################################################################
	##Function:			Get-RemoteTime
	##
	##Description:		Pipeline function. Gets the time from the remote server using WMI.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	31 July, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([datetime])]
	param (
		$server
	)
	process {
		$remoteOSInfo = gwmi win32_OperatingSystem -computername $server	
		[datetime]$remoteDateTime = $remoteOSInfo.convertToDatetime($remoteOSInfo.LocalDateTime)	
		return $remoteDateTime
	}
}
function Test-PortAlive {
	#############################################################################################
	##Function:			Test-PortAlive
	##
	##Description:		Tests connection on a given server on a given port.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	April 02, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([System.boolean])]
	param(
		[Parameter(ValueFromPipeline=$true)][System.String[]]$server,
		[int]$port
	)
	$socket = new-object Net.Sockets.TcpClient
	$connect = $socket.BeginConnect($server, $port, $null, $null)
	$NoTimeOut = $connect.AsyncWaitHandle.WaitOne(500, $false)
	if ($NoTimeOut) {
		$socket.EndConnect($connect) | Out-Null
		return $true				
	}
	else {
		return $false
	}
}
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$servers = gc "$dir\servers.txt"
$CSV = "$dir\ServersTimeInfo.csv"
#SCRIPT MAIN
clear
$TimeInfo = @()
$servers | % {
	$alive_rpc = Test-PortAlive -server $_ -port 135
	$alive_smb = Test-PortAlive -server $_ -port 445
	if ($alive_rpc -and $alive_smb) {
		Write-Host "Getting time info from $_"	
		$RemoteTimeInfo = '' | select "computer","NTPServer","NTP_Type","TimeZone","Time"
		$RemoteTimeInfo.computer = $_
		$timesourceInfo = Get-TimeSource -server $_	
		$RemoteTimeInfo.NTPServer = $timesourceInfo.NTPServer
		$RemoteTimeInfo.NTP_Type = $timesourceInfo.Type
		$RemoteTimeInfo.TimeZone = Get-TimeZone -server $_
		$RemoteTimeInfo.Time = Get-Time -server $_		
		$TimeInfo += $RemoteTimeInfo
	}
	else {
		Write-Host "Error - Couldn't get WMI info from $_"
	}
}
$TimeInfo | Export-Csv $CSV -NoTypeInformation

How do I run the script?

Save the script to your computer as Get-RemoteTime.ps1.
Create servers.txt and place it in the same directory as the script. Populate servers.txt with the names of the servers you wish to get time information from and save. You can now open a powershell prompt, browse to the script directory and execute it. You can also simply open the script in an editor such as powershell ISE or PowerGUI and run it by pressing F5.
That’s it! I hope you find the script useful 🙂

Logoff RDP sessions on multiple servers

Standard

Last year I got a request from a colleague to make a script which can logoff all users (remote desktop sessions) from all windows servers in our environment. I wanted to help and came up with the script in this post. Over time I updated the script a little so it now runs the logoff tasks in different threads to save time (is important if you have hundreds of servers). It also checks port availability so it doesn’t stop or timeout on unavailable servers.

How does the script work?

Basically the script works like this:
1. The script loops through the list of servers in “servers.txt” in the script directory. You must create and populate this file with the names of the servers on which to logoff users before running the script.
2. The script checks if the remote server is available by testing that the ports 135 and 445 are open. If not the server is skipped.
3. A pre-logoff message is sent to the server warning the users that they will be logged off soon. The amount of time to wait before logging off the user sessions can be defined in the $timeleft object in the #variables section of the script.
4. After waiting for the amount of time set in $timeleft the rdp sessions are logged off on the remote server(s).

In case of any problems the script has a lot of debug info that can be viewed/not viewed according to how you set the $debugpreference in the #variables section.

How I wrote the script

I have structured the script in three different parts functions, variables and script main. I will go over and explain these parts now.

Functions

The script uses 4 functions which provide the main functionality.
1. Test-PortAlive

function Test-PortAlive {
	#############################################################################################
	##Function:			Test-PortAlive
	##
	##Description:		Tests connection on a given server on a given port.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	April 02, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([System.boolean])]
	param(
		[Parameter(ValueFromPipeline=$true)][System.String[]]$server,
		[int]$port
	)
	$socket = new-object Net.Sockets.TcpClient
	$connect = $socket.BeginConnect($server, $port, $null, $null)
	$NoTimeOut = $connect.AsyncWaitHandle.WaitOne(500, $false)
	if ($NoTimeOut) {
		$socket.EndConnect($connect) | Out-Null
		return $true				
	}
	else {
		return $false
	}
}

I use this function to check if ports 135 and 445 are open on a server before anything else. If they aren’t open the server will be skipped. I won’t go into details about this function as I have covered it in another separate post on this blog.
2. Send-PreLogoffMessage

function Send-PreLogoffMessage {
	#############################################################################################
	##Function Send-PreLogoffMessage
	##
	##Description:		Pipeline function. Sends a pre-logoff warning to an array of servers given 
	#+					as input. 
	##Created by:		Noam Wajnman
	##Creation Date:	February 2, 2013
	##Updated:			April 06, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$servers
	)
	begin {		
		[int]$timeleftSeconds = 60 * $timeleft
		Write-Debug "Beginning send of warning messages to servers."
	}
	process {
		foreach ($server in $servers) {
			Write-Debug "testing connectivity to $servers on port 135 and 445"
			$alive_rpc = Test-PortAlive -server $servers -port 135
			$alive_smb = Test-PortAlive -server $servers -port 445
			if ($alive_rpc -and $alive_smb) {
				Write-Debug "Connection to $server on ports 135 and 445 was successful"
				Write-Debug "Sending warning message to $server"
				msg * /SERVER:$server "Your remote session will be logged off automatically in $timeleft minutes, you should save your work."
			}
			else {
				Write-Debug "Error - Cannot connect to $server on ports 135 and 445"
				Write-Debug "Skipping $server"
			}
		}
	}
	end {
		Write-Debug "Warning messages sent to all servers. Sleeping for $timeleftSeconds..."
		sleep $timeleftSeconds		
	}
}

I use this function to send a little pop-up box to any server where users will be logged off. This is useful because it gives users a chance to save their work and stop before they are suddenly kicked from the server.
the function first uses the previously mentioned function “test-portalive” and tests if ports 135 and 445 are available. If they are open then a pop-up message is sent to the server using the standard msg.exe windows executable.
3. Get-RDPSessions

function Get-RDPSessions {
	#############################################################################################
	##Name: 			Get-RDPSession
	##Description:		Pipeline function. Retrieves all user sessions from local or remote 
	#+					server/s. Requires query.exe in order to run properly. Takes an array of 
	#+					servers as a parameter.
	##Created by:		Noam Wajnman
	##Link:				Based on code found here - http://poshcode.org/2342
	##Creation Date:	February 2, 2013
	##Updated:			April 06, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$servers
	)
	process {
		Write-Debug "testing connectivity to $servers on port 135 and 445"
		$alive_rpc = Test-PortAlive -server $servers -port 135
		$alive_smb = Test-PortAlive -server $servers -port 445		
		if ($alive_rpc -and $alive_smb) {
			Write-Debug "Connection to $server on ports 135 and 445 was successful"
			# Parse 'query session' and store in $sessions:
			Write-Debug "Getting RDP session info from $servers"
		    $sessions = query session /server:$servers
		    1..($sessions.count -1) | % {
		        $session = "" | Select Computer,SessionName, Username, Id, State, Type, Device
		        $session.Computer = $servers
		        $session.SessionName = $sessions[$_].Substring(1,18).Trim()
		        $session.Username = $sessions[$_].Substring(19,20).Trim()
		        $session.Id = $sessions[$_].Substring(39,9).Trim()
		        $session.State = $sessions[$_].Substring(48,8).Trim()
		        $session.Type = $sessions[$_].Substring(56,12).Trim()
		        $session.Device = $sessions[$_].Substring(68).Trim()
				return $session
		    }
		}		
		else {
			Write-Debug "Error - Cannot connect to $server on ports 135 and 445"
			Write-Debug "Skipping $server"
		}
	}	
}

This functions uses query.exe (comes with windows) to get all the active rdp (remote desktop) sessions on a server. First the function checks that ports 135 and 445 are open (using the previously mentioned test-portalive function) and then gets the session info which is returned. If the ports are closed the server is skipped.
Some of the code used in this function was taken from here http://poshcode.org/2342 and the author deserves credit for doing a great job of parsing the session info.
4. Logoff-RDPSessions

function Logoff-RDPSessions {
	#############################################################################################
	##Function Logoff-RDPSessions
	##
	##Description:		Pipeline function. Logs off the rdp sessions given as the parameter. Use 
	#+					the "asjob" parameter to specify whether to run the logoffs sequentially 
	#+					or in parallel. asjob = $true will start the logoffs in parallel. If the 
	#+					asjob parameter is not given the script will default to an asjob value of 
	#+					$false.
	##Created by:		Noam Wajnman
	##Creation Date:	February 2, 2013
	##Updated:			March 23, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$Sessions,		
		$asjob = $false
	)
	begin {		
		Write-Debug "Beginning logoff of RDP sessions"
		Get-Job | Stop-Job | Remove-Job		
		$script:SB = {
			param (
				$sessionID,
				$server
			)				
			logoff $sessionID /server:$server				
		}
	}
	process {
		$server = $_.computer		
		$sessionID = $_.Id			
		$SessionUserName = $_.UserName		
		if ($SessionUserName -and $sessionID -ne 65536 -and $sessionID -ne $null) {	
			if ($asjob) {
				Write-Debug "Logging off session ID $sessionID for user $SessionUserName on $server..."
				Write-Debug "Starting new job"				
				Start-Job -scriptBlock $SB -ArgumentList $sessionID,$server -Name "LogoffRDPSession"
			}
			else {
				Write-Debug "Logging off session ID $sessionID for user $SessionUserName on $server..."
				Invoke-Command -scriptBlock $SB -ArgumentList $sessionID,$server
			}			
		}		
	}	
}

This function is where the magic happens 🙂 The session info returned by the last function is given as an input parameter and users are logged off using “logoff.exe” (comes with windows). If you provide the “asjob” parameter when running the function it will start each logoff task as a separate process which will save a lot of time because all servers will log of their users in parallel. If “asjob” isn’t given then the logoff tasks will run sequentially which may also be useful on occasion.

Variables

#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$servers = gc "$dir\servers.txt"
#parameters
[int]$timeleft = 5 #Countdown time until Logoff (Mins)
#$DebugPreference = "continue" #uncomment to get debug-info

$dir is an object which always points to the directory in which the script is run. It allows me to easily create paths to files used in/by scripts without having to modify them whenever I move/copy the script to a new location.
$servers is an array of server names which the script loops through to logoff remote desktop sessions. You must create the file “servers.txt” and populate it with the servers on which you want to logoff RDP sessions (one server name per line). Place it in the same directory as the script before running.
$timeleft is the amount of time to wait from starting the script to when the servers will be logged off. Before the RDP sessions are logged off the users will be warned/notified that they will be kicked in this amount of time. Be sure to enter a value fitting your needs before running the script.
$debugpreference can be uncommented (remove the “#” in the beginning of the line) to get the debug info. This is useful if there is trouble with some servers or if you simply want to know what the script is doing at each stage.

Script Main

#SCRIPT MAIN
clear
$servers | Send-PreLogoffMessage
$servers | Get-RDPSessions | Logoff-RDPSessions -asjob $true

I created the functions used in the script to be used from the pipeline. This makes the script main a no-brainer which consists of a few lines.
First I pipe the $servers array into the Send-PreLogoffMessage which sends the warning to the users logged to to the servers. Then I again pipe the $servers array into the Get-RDPSessions and again pipe the retrieved sessions into Logoff-RDPSessions which logs off the remote desktop sessions. I use the “-asjob” parameter so that the logoff tasks will run in parallel and save time but you can omit it if you want to logoff users on servers sequentially.

How do I run the script?

1. Copy the full script and save it as Logoff-RDPSessions.ps1.
2. Create the file servers.txt and place it in the same dir as the script. Populate this file with the names of the servers on which you want to logoff all rdp sessions.
3. Open Logoff-RDPSessions.ps1 in an editor (powerGUI or similar) or simply using notepad. Go to the #parameters section and edit the variables to the values you want and save the file.
4. Run the script from the editor or open a powershell prompt and run it.

That’s it! I hope you find the script useful.

Full script here.

################################################################################################
##Script:			Logoff-RDPSessions.ps1
##
##Description:		Logs off all RDP sessions on servers specified in the input list servers.txt
#+					(Use with caution)
##Created by:		Noam Wajnman
##Creation Date:	February 2, 2013
##Updated:			April 06, 2014
################################################################################################
#FUNCTIONS
function Test-PortAlive {
	#############################################################################################
	##Function:			Test-PortAlive
	##
	##Description:		Tests connection on a given server on a given port.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	April 02, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([System.boolean])]
	param(
		[Parameter(ValueFromPipeline=$true)][System.String[]]$server,
		[int]$port
	)
	$socket = new-object Net.Sockets.TcpClient
	$connect = $socket.BeginConnect($server, $port, $null, $null)
	$NoTimeOut = $connect.AsyncWaitHandle.WaitOne(500, $false)
	if ($NoTimeOut) {
		$socket.EndConnect($connect) | Out-Null
		return $true				
	}
	else {
		return $false
	}
}
function Send-PreLogoffMessage {
	#############################################################################################
	##Function Send-PreLogoffMessage
	##
	##Description:		Pipeline function. Sends a pre-logoff warning to an array of servers given 
	#+					as input. 
	##Created by:		Noam Wajnman
	##Creation Date:	February 2, 2013
	##Updated:			April 06, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$servers
	)
	begin {		
		[int]$timeleftSeconds = 60 * $timeleft
		Write-Debug "Beginning send of warning messages to servers."
	}
	process {
		foreach ($server in $servers) {
			Write-Debug "testing connectivity to $servers on port 135 and 445"
			$alive_rpc = Test-PortAlive -server $servers -port 135
			$alive_smb = Test-PortAlive -server $servers -port 445
			if ($alive_rpc -and $alive_smb) {
				Write-Debug "Connection to $server on ports 135 and 445 was successful"
				Write-Debug "Sending warning message to $server"
				msg * /SERVER:$server "Your remote session will be logged off automatically in $timeleft minutes, you should save your work."
			}
			else {
				Write-Debug "Error - Cannot connect to $server on ports 135 and 445"
				Write-Debug "Skipping $server"
			}
		}
	}
	end {
		Write-Debug "Warning messages sent to all servers. Sleeping for $timeleftSeconds..."
		sleep $timeleftSeconds		
	}
}
function Get-RDPSessions {
	#############################################################################################
	##Name: 			Get-RDPSession
	##Description:		Pipeline function. Retrieves all user sessions from local or remote 
	#+					server/s. Requires query.exe in order to run properly. Takes an array of 
	#+					servers as a parameter.
	##Created by:		Noam Wajnman
	##Link:				Based on code found here - http://poshcode.org/2342
	##Creation Date:	February 2, 2013
	##Updated:			April 06, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$servers
	)
	process {
		Write-Debug "testing connectivity to $servers on port 135 and 445"
		$alive_rpc = Test-PortAlive -server $servers -port 135
		$alive_smb = Test-PortAlive -server $servers -port 445		
		if ($alive_rpc -and $alive_smb) {
			Write-Debug "Connection to $server on ports 135 and 445 was successful"
			# Parse 'query session' and store in $sessions:
			Write-Debug "Getting RDP session info from $servers"
		    $sessions = query session /server:$servers
		    1..($sessions.count -1) | % {
		        $session = "" | Select Computer,SessionName, Username, Id, State, Type, Device
		        $session.Computer = $servers
		        $session.SessionName = $sessions[$_].Substring(1,18).Trim()
		        $session.Username = $sessions[$_].Substring(19,20).Trim()
		        $session.Id = $sessions[$_].Substring(39,9).Trim()
		        $session.State = $sessions[$_].Substring(48,8).Trim()
		        $session.Type = $sessions[$_].Substring(56,12).Trim()
		        $session.Device = $sessions[$_].Substring(68).Trim()
				return $session
		    }
		}		
		else {
			Write-Debug "Error - Cannot connect to $server on ports 135 and 445"
			Write-Debug "Skipping $server"
		}
	}	
}
function Logoff-RDPSessions {
	#############################################################################################
	##Function Logoff-RDPSessions
	##
	##Description:		Pipeline function. Logs off the rdp sessions given as the parameter. Use 
	#+					the "asjob" parameter to specify whether to run the logoffs sequentially 
	#+					or in parallel. asjob = $true will start the logoffs in parallel. If the 
	#+					asjob parameter is not given the script will default to an asjob value of 
	#+					$false.
	##Created by:		Noam Wajnman
	##Creation Date:	February 2, 2013
	##Updated:			March 23, 2014
	#############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][ValidateNotNullOrEmpty()][System.String[]]$Sessions,		
		$asjob = $false
	)
	begin {		
		Write-Debug "Beginning logoff of RDP sessions"
		Get-Job | Stop-Job | Remove-Job		
		$script:SB = {
			param (
				$sessionID,
				$server
			)				
			logoff $sessionID /server:$server				
		}
	}
	process {
		$server = $_.computer		
		$sessionID = $_.Id			
		$SessionUserName = $_.UserName		
		if ($SessionUserName -and $sessionID -ne 65536 -and $sessionID -ne $null) {	
			if ($asjob) {
				Write-Debug "Logging off session ID $sessionID for user $SessionUserName on $server..."
				Write-Debug "Starting new job"				
				Start-Job -scriptBlock $SB -ArgumentList $sessionID,$server -Name "LogoffRDPSession"
			}
			else {
				Write-Debug "Logging off session ID $sessionID for user $SessionUserName on $server..."
				Invoke-Command -scriptBlock $SB -ArgumentList $sessionID,$server
			}			
		}		
	}	
}
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$servers = gc "$dir\servers.txt"
#parameters
[int]$timeleft = 5 #Countdown time until Logoff (Mins)
$DebugPreference = "continue" #uncomment to get debug-info
#SCRIPT MAIN
clear
$servers | Send-PreLogoffMessage
$servers | Get-RDPSessions | Logoff-RDPSessions -asjob $true

Synchronize folder/directory contents

Standard

It is often useful to be able to synchronize the contents of certain folders. Yesterday I needed a way to make sure that files in separate folders on some of my servers are kept up to date. I wrote the script in this post to help me accomplish that. Incidentally it also proved useful in syncing some private files between dropbox and google drive etc.. 🙂

How does the script work?

The basic idea of the script is to sync files from a source dir to a destination dir. Also, since I work with many large files I only want to copy them if there actually is a difference between the file versions in the source and destination dir. To do all this I came up with these logical steps.

1. Loop through all the files in the source dir.
2. If the file doesn’t exist in the destination dir then copy it.
3. If the file exists in the destination dir then calculate the MD5 hashes of both source and destination files and compare. If the hashes match then the files are identical and can be skipped. If not then copy the file.

The script itself

The comments I put in the code should explain how I built the script according to the logical steps above.

###############################################################################
##script:			Sync-Folders.ps1
##
##Description:		Syncs/copies contents of one dir to another. Uses MD5
#+					checksums to verify the version of the files and if they
#+					need to be synced.
##Created by:		Noam Wajnman
##Creation Date:	June 9, 2014
###############################################################################
#FUNCTIONS
function Get-FileMD5 {
    Param([string]$file)
	$md5 = [System.Security.Cryptography.HashAlgorithm]::Create("MD5")
	$IO = New-Object System.IO.FileStream($file, [System.IO.FileMode]::Open)
	$StringBuilder = New-Object System.Text.StringBuilder
	$md5.ComputeHash($IO) | % { [void] $StringBuilder.Append($_.ToString("x2")) }
	$hash = $StringBuilder.ToString() 
	$IO.Dispose()
	return $hash
}
#VARIABLES
$DebugPreference = "continue"
#parameters
$SRC_DIR = 'c:\sourcefolder\'
$DST_DIR = 'C:\destfolder\'
#SCRIPT MAIN
clear
$SourceFiles = GCI -Recurse $SRC_DIR | ? { $_.PSIsContainer -eq $false} #get the files in the source dir.
$SourceFiles | % { # loop through the source dir files
	$src = $_.FullName #current source dir file
	Write-Debug $src
	$dest = $src -replace $SRC_DIR.Replace('\','\\'),$DST_DIR #current destination dir file
	if (test-path $dest) { #if file exists in destination folder check MD5 hash
		$srcMD5 = Get-FileMD5 -file $src
		Write-Debug "Source file hash: $srcMD5"
		$destMD5 = Get-FileMD5 -file $dest
		Write-Debug "Destination file hash: $destMD5"
		if ($srcMD5 -eq $destMD5) { #if the MD5 hashes match then the files are the same
			Write-Debug "File hashes match. File already exists in destination folder and will be skipped."
			$cpy = $false
		}
		else { #if the MD5 hashes are different then copy the file and overwrite the older version in the destination dir
			$cpy = $true
			Write-Debug "File hashes don't match. File will be copied to destination folder."
		}
	}
	else { #if the file doesn't in the destination dir it will be copied.
		Write-Debug "File doesn't exist in destination folder and will be copied."
		$cpy = $true
	}
	Write-Debug "Copy is $cpy"
	if ($cpy -eq $true) { #copy the file if file version is newer or if it doesn't exist in the destination dir.
		Write-Debug "Copying $src to $dest"
		if (!(test-path $dest)) {
			New-Item -ItemType "File" -Path $dest -Force	
		}
		Copy-Item -Path $src -Destination $dest -Force
	}
}

How do I use/run the script?

First of all remember to enter the paths for your source and destination dirs/folders in the #parameters section of the script.
You can run the script manually and synchronize on a need to basis but I recommend using the windows task scheduler to run it regularly and keep your dirs synchronized at all times with minimal effort.
In order to configure a scheduled task which runs the script you can follow the below steps (for windows 2008 R2/windows 7).
1. Start the windows task scheduler.
2. Stand on “task scheduler library” and click “Create Basic Task”.
3. Give your task a name and a description.
Task_Name
4. In the next steps choose a schedule for the task and Click “Next” until you get to “action”.
Task_Action
5. Choose “Start a program” and click “Next”.
6. Under “Program/script:” simply write powershell. In the “Add arguments:” field write -command “& ‘c:\pathtoscript\sync-folders.ps1′”(change c:\pathtoscript\ to where your sync-folders.ps1 file is located). Click “Next”.
Task_StartAProgram
7. You should now see a summary of the task looking sort of like this.
Task_Summary
8. Click “Finish”.

That’s it! You’re done.

I hope you find the script useful. Good luck!

Scripting tips – Point to the dir from which the script was run.

Standard

When working with Powershell you often need to interact with other files when executing your script. Dealing with input files, output files and temp files are usually integral parts of script writing. When working with files you usually need to specify the absolute path to each file. However this makes the script less “robust” as you will need to change these paths if you also change the location of the script by copying it or moving it. This means extra work as you will need to modify the script every time you move it or copy it. Furthermore if you intend on giving the script for someone else to run they might complain that it’s not working because they haven’t got the paths right.

– Creating an object which always points to the script dir.

I have already used the following lines of code in many of my scripts but I find them so useful I thought they deserved their own post.

$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath

The whole thing is as simple as writing the above two lines of code. They will create the object $dir which always point to the path from where this script was run.

– How do I use this when referring to files in my scripts?

Now, when you wish to add a file to your script you can do it like the examples shown below.

$inputFile = "$dir\input.txt"

$outputFile = "$dir\output.txt"
New-Item -ItemType File $outputFile -Force

$logFile = "$dir\logs\logfile.txt"
New-Item -ItemType File $logFile -Force

$inputFile now points to a file called “input.txt” located in your script directory.

The $dir object is also useful when creating new files to make sure they are created in the correct locations. For instance $outputFile is a path that points to a file “output.txt” in the script directory. If it doesn’t exist yet you can then create the file with this line: New-Item -ItemType File $outputFile -Force.

Also, if you for instance want to add logging functionality to your script you can create the log file in a new dir in your script directory called “logs” by using the last example:
$logFile = “$dir\logs\logfile.txt”
New-Item -ItemType File $logFile -Force

– Benefits of using file paths like this

As you can see using the $dir object also allows you to easily maintain a directory structure for your files which will always remain the same no matter where you move the script. If you now create a script and refer to your files/directories like above and give it to a friend/colleague, it will always work no matter where he runs it from and any script output files will be created in the script directory. This will make the script much easier for others to use and save you a lot of trouble.

– Finding the correct scope when using MyInvocation.MyCommand.Path

In most cases creating the $dir object like above will work perfectly without any problems. However in some cases you will need to work a little with the scope of the variable in order to make it work. For instance if you are using it in a child job or from a .NET form you are running you might need to change the code to look like this:

$scriptPath = $script:MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath

If you run into any trouble you can change to code to look like above which should solve the problems.
You can read more about scopes in powershell here:
http://technet.microsoft.com/en-us/library/hh847849.aspx

– That’s it!

I hope this post will be helpful to you and let you easily work with paths and files in your scripts.

Get CPU usage and CPU ready values for all VMs in a cluster

Standard

When working with VMWare it is crucial to monitor the performance of your HA clusters and VMs. It is important to be able to get performance data so you can make sure your VMs have the resources they need and to know if they are sized correctly. To determine this I often use the CPU usage and CPU ready performance counters.

– What are the cpu usage/ready perf. counters and what do they show us?

The CPU usage value is sort of like the value you see in the task manager in windows, the main difference being that this value is not measured by the VM’s OS but by the ESXi host it is running on. As in the task manager this value indicates how much CPU is being used by your VM.
The CPU ready value is the amount of time your VM’s CPU has been “ready”, meaning how much time it has been waiting, doing nothing, for CPU cycles to be assigned to it by the host. If this value is high it is often an indicator of cluster health problems and/or bad VM sizing. I urge you to search for information about these counters and learn about them as they are highly useful when managing a VMWare environment. You can read more about CPU ready time here:
http://blogs.totalcaos.com/understanding-rdy-cpu-ready/

– Why should I use the script found in this blog post?

Gathering data like the performance counters described above can be done with Vcenter Operations Manager or with Veeam if you have this. However these applications are complicated to use and configure (and cost a lot of money). The script I have written below will give you a simple list/csv of the VMs in a given cluster with their CPU usage and CPU ready values in percent. You can then review your VMs and see check for any high/problematic values you find and improve the efficiency of your infrastructure. Any VMs with an average cpu usage over 70 % should probably be assigned more resources. If there are any VMs with ready times of 10% or more it probably means that your cluster/host is overcommitted or that the VM is oversized.

– The script itself and how I wrote it

To simplify the structure of the script I have divided it into three sections variables, functions and script main.

Variables

#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$CSV = "$dir\Average_CPU_Usage_Peak_Hours.csv"
#Parameters
$vcenter = "Some_Vcenter_server" #Name of your vcenter server
$ClusterName = "Some_cluster" #CPU usage and CPU ready values will be collected for all VMs in this cluster.
$DaysBack = 14 #Number of days back to collect performance counters.
$PeakTimeStart = 8 #hour of the day in 24 hour format 
$PeakTimeEnd = 20 #hour of the day in 24 hour format
$rdy_interval = 7200 #interval of rdy time values aggregation/averaging in seconds. This value should be changed according to your vcenter statistics settings.

Before running the script you will need to change the values in the #parameters section according to your needs/environment. $vcenter should be the name of your vcenter server and $ClusterName the name of the cluster you wish to collect VM performance data from. $DaysBack is the amount of time you wish to collect performance from. E.g if it is set to 14 days you will collect performance data going back 14 days to the time you are running the script. Make sure you set this value to a valid number of days (your vcenter DB must retain the info for the amount of time specified).

I have found that performance data is more useful when it is gathered in business hours so the results show values reflecting a busy system. To gather performance counters from the relevant times of day you must change the values for $peakTimeStart and $peakTimeEnd to suit your needs.

The last parameter is the $rdy_interval. When gathering the cpu ready time performance counters the values are returned as a number of milliseconds. To convert this number correctly to a percent value it is necessary to take into account the aggregation interval (the time between the data points stored in the vcenter database). On my vcenter installation the ready time values are aggregated/averaged every two hours and therefore I have set the $rdy_interval to 7200 seconds. You can read more about this here:
http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2002181

Now to the functions.

Functions

1. Connect-Vcenter

function Connect-Vcenter {
	param(
		$vcenter
	)
	#Load snap-in
	if (-not (Get-PSSnapin -Name VMware.VimAutomation.Core)) {
		Add-PSSnapin VMware.VimAutomation.Core
	}
	#Connect to vCenter
	if ($global:DefaultVIServers.Count -lt 1) {
		Connect-VIServer $vCenter
	}
}

Connect-Vcenter is a small function that simply adds the snap-in for VMware cmdlets and connects to a vcenter server making the powershell instance ready to fire commands.
2. Get-VMCPUAverage

function Get-VMCPUAverage {
    param (
        $VM
    )
	$Start = (get-date).AddDays(-$DaysBack)
	$Finish = get-date
    
	$stats = get-vm $VM | get-stat -MaxSamples "1000" -Start $Start -Finish $Finish -Stat "cpu.usage.average" | `
    ? { ($_.TimeStamp).Hour -gt $PeakTimeStart -and ($_.TimeStamp).Hour -lt $PeakTimeEnd }
	$aggr_stats = $stats | Measure-Object -Property Value -Average
	$avg = $aggr_stats.Average
	return $avg
}

This function uses the vmware cmdlet Get-Stat to collect the performance counter cpu.usage.average in the specified timeframe. The average value of all the collected data points are then returned.
3. Get-VMCpuRDY

function Get-VMCpuRDY {
	param (
        $VM
    )
	$Start = (get-date).AddDays(-$DaysBack)
	$Finish = get-date
	
	$stats = get-vm $VM | Get-Stat -MaxSamples "1000" -Start $Start -Finish $Finish -Stat Cpu.Ready.Summation | `
    ? { ($_.TimeStamp).Hour -gt $PeakTimeStart -and ($_.TimeStamp).Hour -lt $PeakTimeEnd -and $_.Instance -eq ""}	
	$aggr_stats = $stats | Measure-Object -Property Value -Average	
	$rdy = [Math]::Round(((($aggr_stats.Average)/1000)/$rdy_interval) * 100,1)
	return $rdy
}

Get-VMCpuRDY collects the average values of the performance counter Cpu.Ready.Summation in the specified timeframe. As explained earlier in the post this counter returns a summation of the milliseconds in which the CPU was waiting for resources. In order to convert this into a percentage value I first divide the amount of milliseconds by 1000 to convert to seconds. I divide this amount of seconds by the $rdy_interval, which is the amount of seconds between each data point, and then multiply by 100. I then use [Math]::Round to round off the value to one decimal which is the number returned by the function.

Script Main

#SCRIPT MAIN
clear
#Load the VMWare module and connect to vCenter
Connect-Vcenter -vcenter $vcenter
$AvgCPUValues = @() #Create array to hold the CPU usage and CPU ready values
Get-Cluster $ClusterName | Get-VM | ? {$_.PowerState -eq "PoweredOn"} | % {	#loop through all powered on VMs in the cluster
	$AvgCPUValue = "" | Select "VM","CpuAvg","CpuRdy" #create a custom object with these properties.
	$AvgCPUValue.VM = $_.Name
	$AvgCPUValue.CpuAvg = "{0:N2}" -f $(Get-VMCPUAverage -VM $_) #Get VM CPU usage and round to two decimals
	$AvgCPUValue.CpuRdy = Get-VMCpuRDY -VM $_
	$AvgCPUValues += $AvgCPUValue
}
$AvgCPUValues | Export-Csv $CSV -NoTypeInformation -Force

In the script main I first connect to the vcenter server using my function Connect-Vcenter. I then create an array $AvgCPUValues. The next step is to loop through all the VMs in the cluster using Get-Cluster $ClusterName | Get-VM. In the loop I create a custom object for each VM with the properties VM, CPUAvg and CPURdy. I use the functions Get-VMCPUAverage and Get-VMCpuRDY to get cpu usage and cpu ready values and then assign these to the corresponding properties on the custom object. The custom object is then added to the array $AvgCPUValues which I created in the beginning of the script main.
When the loop has completed I then export this array to CSV in the directory the script was run.

– That’s it! You’re ready to go!

I really hope you find this script useful. Remember to fill out the #parameters section before you run the script.
I have copied in the full script below.

#############################################################################################################
##script:			Get-VMCPUAverage.ps1
##
##Description:		Gets "CPU usage" and "cpu ready" for all VMs in a given cluster and exports the results
#+					to a CSV file in the script directory.
##Created by:		Noam Wajnman
##Creation Date:	March 11, 2014
##Updated on:		May 20, 2014
##############################################################################################################
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$CSV = "$dir\Average_CPU_Usage_Peak_Hours.csv"
#Parameters
$vcenter = "Some_Vcenter_server" #Name of your vcenter server
$ClusterName = "Some_cluster" #CPU usage and CPU ready values will be collected for all VMs in this cluster.
$DaysBack = 14 #Number of days back to collect performance counters.
$PeakTimeStart = 8 #hour of the day in 24 hour format 
$PeakTimeEnd = 20 #hour of the day in 24 hour format
$rdy_interval = 7200 #interval of rdy time values aggregation/averaging in seconds. This value should be changed according to your vcenter statistics settings.
#FUNCTIONS
function Connect-Vcenter {
	param(
		$vcenter
	)
	#Load snap-in
	if (-not (Get-PSSnapin -Name VMware.VimAutomation.Core)) {
		Add-PSSnapin VMware.VimAutomation.Core
	}
	#Connect to vCenter
	if ($global:DefaultVIServers.Count -lt 1) {
		Connect-VIServer $vCenter
	}
}
function Get-VMCPUAverage {
    param (
        $VM
    )
	$Start = (get-date).AddDays(-$DaysBack)
	$Finish = get-date
    
	$stats = get-vm $VM | get-stat -MaxSamples "1000" -Start $Start -Finish $Finish -Stat "cpu.usage.average" | `
    ? { ($_.TimeStamp).Hour -gt $PeakTimeStart -and ($_.TimeStamp).Hour -lt $PeakTimeEnd }
	$aggr_stats = $stats | Measure-Object -Property Value -Average
	$avg = $aggr_stats.Average
	return $avg
}
function Get-VMCpuRDY {
	param (
        $VM
    )
	$Start = (get-date).AddDays(-$DaysBack)
	$Finish = get-date
	
	$stats = get-vm $VM | Get-Stat -MaxSamples "1000" -Start $Start -Finish $Finish -Stat Cpu.Ready.Summation | `
    ? { ($_.TimeStamp).Hour -gt $PeakTimeStart -and ($_.TimeStamp).Hour -lt $PeakTimeEnd -and $_.Instance -eq ""}	
	$aggr_stats = $stats | Measure-Object -Property Value -Average	
	$rdy = [Math]::Round(((($aggr_stats.Average)/1000)/$rdy_interval) * 100,1)
	return $rdy
}
#SCRIPT MAIN
clear
#Load the VMWare module and connect to vCenter
Connect-Vcenter -vcenter $vcenter
$AvgCPUValues = @() #Create array to hold the CPU usage and CPU ready values
Get-Cluster $ClusterName | Get-VM | ? {$_.PowerState -eq "PoweredOn"} | % {	#loop through all powered on VMs in the cluster
	$AvgCPUValue = "" | Select "VM","CpuAvg","CpuRdy" #create a custom object with these properties.
	$AvgCPUValue.VM = $_.Name
	$AvgCPUValue.CpuAvg = "{0:N2}" -f $(Get-VMCPUAverage -VM $_) #Get VM CPU usage and round to two decimals
	$AvgCPUValue.CpuRdy = Get-VMCpuRDY -VM $_
	$AvgCPUValues += $AvgCPUValue
}
$AvgCPUValues | Export-Csv $CSV -NoTypeInformation -Force

Set advanced settings on all VMs in a cluster

Standard

Recently I was tasked with modifying the VM advanced settings (or VMX settings) on all of our VMs. Since I didn’t want to manually edit the VMX files on 500+ VMs I resolved to make a script which could do the job instead. I eventually made a good and working script which I will go over below.
First off I want to say that I have based a great deal of this script on some code that I found here: https://communities.vmware.com/docs/DOC-18653. Many thanks to Alan Renouf for posting this article!
The script posted here has been improved a bit and is also more complete since it also connects to vcenter on its own. This should hopefully make this script useful even to admins who aren’t experienced with powershell.

###############################################################################
##set-VMAdvancedSettings.ps1
##
##Description:		sets/creates advanced settings on VMs based on CSV input
##Created by:		Noam Wajnman
##Creation Date:	March 11, 2014
###############################################################################
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$PathToCSV = "$dir\vmsettings.csv"
$vcenter = "some_vcenter_server" #script will connect to this vcenter server
$clusterName = "Some_cluster" #advanced settings will be set on all VMs on this cluster
#FUNCTIONS
function ConnectToVcenter {
	param(
		$vcenter
	)
	#Load snap-in and connect to vCenter
	if (-not (Get-PSSnapin -Name VMware.VimAutomation.Core)) {
		Add-PSSnapin VMware.VimAutomation.Core
	}
	if ($global:DefaultVIServers.Count -ne 1) {
		Connect-VIServer $vCenter
	}
}
function Set-VMAdvancedConfiguration { 
	###############################################################################
	##Function set-VMAdvancedConfiguration
	##
	##Description:		sets/creates advanced settings on VMs
	##Created by:		Noam Wajnman
	##Creation Date:	March 11, 2014
	##Credits:			Based on code written by alan renouf found here: 
	#+					https://communities.vmware.com/docs/DOC-18653
	###############################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
  	param(  
    	[Parameter(Mandatory=$true,ValueFromPipeline=$true)]$vm,  
      	[string]$PathToCSV
    )
	begin {		
		$vmConfigSpec = new-object VMware.Vim.VirtualMachineConfigSpec  
		$vmsettings = Import-Csv $PathToCSV -Header Key,Value
		$vmsettings | Foreach {  
        	$Value = new-object vmware.vim.optionvalue  
        	$Value.key = $_.key  
        	$Value.value = $_.value  
        	$vmConfigSpec.ExtraConfig += $Value  
        	Write-Host "Adding $($_.Key) = $($_.Value) to configuration specifications"  
		}
	}
  	process {    	  
    	$Task = ($vm.ExtensionData).ReconfigVM_Task($vmConfigSpec)  
      	Write-Host "Setting Advanced configuration for $($VM.Name)"    	 
  	}
	end {
		Write-Host "Finished applying advanced settings to VMs."
	}
}

#SCRIPT MAIN
ConnectToVcenter -vcenter $vcenter
Get-cluster $clusterName | get-vm | Set-VMAdvancedConfiguration -PathToCSV $PathToCSV

All you need to do to run the script is:
1. Create a CSV file in the same directory as the script. The CSV must have two “columns”, the name of the settings and the desired value. I did it in excel and my example CSV file looks like this:
vmsettings_in_excel
2. Edit two values in the #Variables section of the script. Set $vcenter to the name of your vcenter server and $clusterName to the name of your cluster (all VMs on this cluster will have their advanced settings changed according to what you wrote in the CSV file).

You can then run the script and see all the specified settings being changed automatically on your VMs. Yay 🙂

I hope you will find the script useful!!

Powershell and .NET forms. Write your own GUI – File Checksum Tool

Standard

I often encapsulate my scripts in a small GUI after I write them. This makes the scripts much easier to use for other people as it provides a more intuitive way to give the input parameters and also interpret the results.
I am writing this post in order to showcase how you can easily create a small GUI and create a tool which you and your colleagues can use. As an examples I have chosen to create a simple tool which can compute the checksum of a file using a few of the most common hash algorithms out there.
When creating this GUI I used primalforms to help build it. Primalforms is a great free program which allows you to construct a .NET form complete with the needed objects and the layout that you want.

Creating the form using PrimalForms

PrimalFormsScreenDump
To create the file checksum tool I need two fields for input. One for the path of the file to check and the other to choose the hash algorithm used. For the file name field I decided to use an object of the type “TextBox” and for the algorithm field I chose a “ComboBox” which is a dropdown menu. To describe the fields I added some “Label” objects over the fields. To get the file path input I decided to add a “Button” object which I will later use to open a small window and allow users to browse and select files. I added a second button which will later be used to calculate the file checksum with the given inputs. Lastly I added a “RichTextBox” object which I will use to display the output (the computed hash string).
After placing the objects where I wanted them I populated the text properties of the objects with the strings I wanted e.g. “Select File:”, “Browse” etc. I then populated the “ComboBox” dropdown with list of choices for hash algorithms MD5, SHA1, SHA256 etc.
After this has been done the form is basically ready. Use the “Export Powershell” function in PrimalForms and copy the form code to your powershell editor. the exported code looks like this:

#Generated Form Function
function GenerateForm {
########################################################################
# Code Generated By: SAPIEN Technologies PrimalForms (Community Edition) v1.0.10.0
# Generated On: 5/7/2014 2:02 PM
# Generated By: noam wajnman
########################################################################

#region Import the Assemblies
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
#endregion

#region Generated Form Objects
$form1 = New-Object System.Windows.Forms.Form
$label3 = New-Object System.Windows.Forms.Label
$richTextBox1 = New-Object System.Windows.Forms.RichTextBox
$label2 = New-Object System.Windows.Forms.Label
$label1 = New-Object System.Windows.Forms.Label
$comboBox1 = New-Object System.Windows.Forms.ComboBox
$textBox1 = New-Object System.Windows.Forms.TextBox
$button2 = New-Object System.Windows.Forms.Button
$button1 = New-Object System.Windows.Forms.Button
$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
#endregion Generated Form Objects

#----------------------------------------------
#Generated Event Script Blocks
#----------------------------------------------
#Provide Custom Code for events specified in PrimalForms.
$button1_OnClick= 
{
#TODO: Place custom script here

}

$button2_OnClick= 
{
#TODO: Place custom script here

}

$OnLoadForm_StateCorrection=
{#Correct the initial state of the form to prevent the .Net maximized form issue
	$form1.WindowState = $InitialFormWindowState
}

#----------------------------------------------
#region Generated Form Code
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 364
$System_Drawing_Size.Width = 430
$form1.ClientSize = $System_Drawing_Size
$form1.DataBindings.DefaultDataSourceUpdateMode = 0
$form1.Font = New-Object System.Drawing.Font("Arial",9.75,0,3,1)
$form1.Name = "form1"
$form1.Text = "File Checksum Tool"

$label3.DataBindings.DefaultDataSourceUpdateMode = 0
$label3.Font = New-Object System.Drawing.Font("Arial",8.25,0,3,1)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 24
$System_Drawing_Point.Y = 330
$label3.Location = $System_Drawing_Point
$label3.Name = "label3"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 36
$System_Drawing_Size.Width = 202
$label3.Size = $System_Drawing_Size
$label3.TabIndex = 7
$label3.Text = "Created by Noam Wajnman May 2014. https://noamwajnman.wordpress.com"

$form1.Controls.Add($label3)

$richTextBox1.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 24
$System_Drawing_Point.Y = 199
$richTextBox1.Location = $System_Drawing_Point
$richTextBox1.Name = "richTextBox1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 107
$System_Drawing_Size.Width = 388
$richTextBox1.Size = $System_Drawing_Size
$richTextBox1.TabIndex = 6
$richTextBox1.Text = ''

$form1.Controls.Add($richTextBox1)

$label2.DataBindings.DefaultDataSourceUpdateMode = 0
$label2.Font = New-Object System.Drawing.Font("Arial Black",12,0,3,1)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 24
$System_Drawing_Point.Y = 97
$label2.Location = $System_Drawing_Point
$label2.Name = "label2"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 23
$System_Drawing_Size.Width = 261
$label2.Size = $System_Drawing_Size
$label2.TabIndex = 5
$label2.Text = "Select Algorithm"

$form1.Controls.Add($label2)

$label1.DataBindings.DefaultDataSourceUpdateMode = 0
$label1.Font = New-Object System.Drawing.Font("Arial Black",12,0,3,1)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 24
$System_Drawing_Point.Y = 13
$label1.Location = $System_Drawing_Point
$label1.Name = "label1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 23
$System_Drawing_Size.Width = 261
$label1.Size = $System_Drawing_Size
$label1.TabIndex = 4
$label1.Text = "Select file:"

$form1.Controls.Add($label1)

$comboBox1.DataBindings.DefaultDataSourceUpdateMode = 0
$comboBox1.FormattingEnabled = $True
$comboBox1.Items.Add("MD5")|Out-Null
$comboBox1.Items.Add("SHA1")|Out-Null
$comboBox1.Items.Add("SHA256")|Out-Null
$comboBox1.Items.Add("SHA384")|Out-Null
$comboBox1.Items.Add("SHA512")|Out-Null
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 24
$System_Drawing_Point.Y = 133
$comboBox1.Location = $System_Drawing_Point
$comboBox1.Name = "comboBox1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 24
$System_Drawing_Size.Width = 261
$comboBox1.Size = $System_Drawing_Size
$comboBox1.TabIndex = 3

$form1.Controls.Add($comboBox1)

$textBox1.DataBindings.DefaultDataSourceUpdateMode = 0
$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 24
$System_Drawing_Point.Y = 46
$textBox1.Location = $System_Drawing_Point
$textBox1.Name = "textBox1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 22
$System_Drawing_Size.Width = 261
$textBox1.Size = $System_Drawing_Size
$textBox1.TabIndex = 2

$form1.Controls.Add($textBox1)


$button2.DataBindings.DefaultDataSourceUpdateMode = 0
$button2.Font = New-Object System.Drawing.Font("Arial Black",8.25,0,3,1)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 304
$System_Drawing_Point.Y = 114
$button2.Location = $System_Drawing_Point
$button2.Name = "button2"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 57
$System_Drawing_Size.Width = 108
$button2.Size = $System_Drawing_Size
$button2.TabIndex = 1
$button2.Text = "Get File Checksum"
$button2.UseVisualStyleBackColor = $True
$button2.add_Click($button2_OnClick)

$form1.Controls.Add($button2)


$button1.DataBindings.DefaultDataSourceUpdateMode = 0
$button1.Font = New-Object System.Drawing.Font("Arial Black",8.25,0,3,1)

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 304
$System_Drawing_Point.Y = 46
$button1.Location = $System_Drawing_Point
$button1.Name = "button1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 22
$System_Drawing_Size.Width = 108
$button1.Size = $System_Drawing_Size
$button1.TabIndex = 0
$button1.Text = "Browse"
$button1.UseVisualStyleBackColor = $True
$button1.add_Click($button1_OnClick)

$form1.Controls.Add($button1)

#endregion Generated Form Code

#Save the initial state of the form
$InitialFormWindowState = $form1.WindowState
#Init the OnLoad event to correct the initial state of the form
$form1.add_Load($OnLoadForm_StateCorrection)
#Show the Form
$form1.ShowDialog()| Out-Null

} #End Function

#Call the Function
GenerateForm

All that remains now is to write the button scripts so they execute the code you want when you press them.

Writing the button scripts

If you look in the form code exported in primalforms you will notice a few places where it says “#TODO: Place custom script here”. It is here that we must place our custom scripts which will be executed when the buttons are pressed. I chose to simply write the button scripts in separate files and then “dot source” the files as shown below.

#----------------------------------------------
#Generated Event Script Blocks
#----------------------------------------------	
$button1_OnClick= 
{
#########################################################################################
#Dot Source button script
$ScriptPath = $script:MyInvocation.MyCommand.Path
$ScriptDir = Split-Path $scriptpath
. "$ScriptDir\BrowseFileDialogue.ps1"
#########################################################################################
}

$button2_OnClick= 
{
#########################################################################################
#Dot Source button script
$ScriptPath = $script:MyInvocation.MyCommand.Path
$ScriptDir = Split-Path $scriptpath
. "$ScriptDir\Get-FileChecksum.ps1"
#########################################################################################
}

These two lines of code:
$ScriptPath = $script:MyInvocation.MyCommand.Path
$ScriptDir = Split-Path $scriptpath
are very useful. They enable me to create the object $ScriptDir which always points to the folder in which the form/script was executed. I can then create a script and copy it there. All I now need to do to execute it with the button in the form, is to add:
. “$ScriptDir\some_script.ps1
I will now briefly go over the two button scripts I use in the form.
1. BrowseFileDialogue.ps1

###############################################################################
##script:			BrowseFileDialogue.ps1
##
##Description:		Opens a window with a browse file dialogue and returns the
#+					filename selected.
##Created by:		Noam Wajnman
##Creation Date:	May 7, 2014
###############################################################################
#VARIABLES
$initialDirectory = "c:\"
#SCRIPT MAIN
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $initialDirectory
$OpenFileDialog.filter = "All files (*.*)| *.*"
$OpenFileDialog.ShowHelp = $true
$OpenFileDialog.ShowDialog() | Out-Null
$fileSelected = $OpenFileDialog.filename
$textbox1.text = $fileSelected

Here I simply create an object of the type System.Windows.Forms.OpenFileDialog. I then use the ShowDialog() method on this object to make the browse file window appear. I then save the selected file name in $fileSelected. Lastly I add the selected filepath/filename to the first input field in the GUI/Form like this:
$textbox1.text = $fileSelected
Voila! now the filepath will appear in the textbox when you have selected a file.
2. Get-FileChecksum

###############################################################################
##script:			Get-FileChecksum.ps1
##
##Description:		Takes two parameters filename and algorithm and returns the
#+					computed file checksum hash.
##Created by:		Noam Wajnman
##Creation Date:	May 7, 2014
###############################################################################
#VARIABLES
$algorithm = $combobox1.text
$filename = $textbox1.text
#FUNCTIONS
function Update-RichTextBox {	
	$RichTextBox1.refresh()
	$RichTextBox1.SelectionStart = $RichTextBox1.Text.Length
    $RichTextBox1.ScrollToCaret()
}
function Write-RichTextBox {
	param (
		[string]$message
	)
	$time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"	
	$RichTextBox1.AppendText("$time`n$message`n")
	Update-RichTextBox
}
function Show-ErrorBox {
    param (        
        $errorTitle,
        $errorText
    )    
    [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
    [System.Windows.Forms.MessageBox]::Show($errorText,$errorTitle)
}
function Get-FileMD5 {
    Param([string]$file)
	$md5 = [System.Security.Cryptography.HashAlgorithm]::Create("MD5")
	$IO = New-Object System.IO.FileStream($file, [System.IO.FileMode]::Open)
	$StringBuilder = New-Object System.Text.StringBuilder
	$md5.ComputeHash($IO) | % { [void] $StringBuilder.Append($_.ToString("x2")) }
	$hash = $StringBuilder.ToString() 
	$IO.Dispose()
	return $hash
}
function Get-FileSHA1 {
	Param([string]$file)	
	$sha1 = new-object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider
	$IO = [System.IO.File]::Open($file,[System.IO.Filemode]::Open, [System.IO.FileAccess]::Read)
	$hash = ([System.BitConverter]::ToString($sha1.ComputeHash($IO)) -replace '-','').ToLower()
	$IO.Dispose()
	return $hash
}
function Get-FileSHA384 {
	Param([string]$file)	
	$sha384 = new-object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider
	$IO = [System.IO.File]::Open($file,[System.IO.Filemode]::Open, [System.IO.FileAccess]::Read)
	$hash = ([System.BitConverter]::ToString($sha384.ComputeHash($IO)) -replace '-','').ToLower()
	$IO.Dispose()
	return $hash
}
function Get-FileSHA512 {
	Param([string]$file)	
	$sha512 = new-object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider
	$IO = [System.IO.File]::Open($file,[System.IO.Filemode]::Open, [System.IO.FileAccess]::Read)
	$hash = ([System.BitConverter]::ToString($sha512.ComputeHash($IO)) -replace '-','').ToLower()
	$IO.Dispose()
	return $hash
}
#SCRIPT MAIN
if ((test-path -PathType Leaf -Path $filename) -and $algorithm) {
	switch ($algorithm) {
		"MD5" { $hash = Get-FileMD5 -file $filename }
		"SHA1" { $hash = Get-FileSHA1 -file $filename }
		"SHA256" { $hash = Get-FileSHA256 -file $filename }
		"SHA384" { $hash = Get-FileSHA384 -file $filename }
		"SHA512" { $hash = Get-FileSHA512 -file $filename }
	}
	$result = "$algorithm hash of $filename is:`n$hash"
	Write-RichTextBox -message $result
}
else {
	$errorTitle = "Error - File name and algorithm not selected."
	$errorText = "Please select a valid file name and a hash algorithm and try again."
	Show-ErrorBox -errorText $errorText -errorTitle $errorTitle
}

To simplify the structure of this button script I have divided it into three sections variables, functions and script main. In the #VARIABLES section I just collect the input from the input fields (textbox and combobox) like so:
$algorithm = $combobox1.text
$filename = $textbox1.text
In the #FUNCTIONS section I have written the functions I need to work with the form output and calculate the hashes/checksums. I won’t go over them in detail here as the post will become far too long. I will however explain how I use them below in the walkthrough of the #SCRIPT MAIN section.

#SCRIPT MAIN
if ((test-path -PathType Leaf -Path $filename) -and $algorithm) {
	switch ($algorithm) {
		"MD5" { $hash = Get-FileMD5 -file $filename }
		"SHA1" { $hash = Get-FileSHA1 -file $filename }
		"SHA256" { $hash = Get-FileSHA256 -file $filename }
		"SHA384" { $hash = Get-FileSHA384 -file $filename }
		"SHA512" { $hash = Get-FileSHA512 -file $filename }
	}
	$result = "$algorithm hash of $filename is:`n$hash"
	Write-RichTextBox -message $result
}
else {
	$errorTitle = "Error - File name and algorithm not selected."
	$errorText = "Please select a valid file name and a hash algorithm and try again."
	Show-ErrorBox -errorText $errorText -errorTitle $errorTitle
}

The first thing I do in the button script main is to validate the input a little. I use an “If” statement to make sure both inputs are given in the form. If not the script goes into the “Else” script block and runs the “Show-ErrorBox” function which makes a little error window popup which asks the user to provide valid input. Assuming the input is ok I then use a “switch” statement on the $algorithm object in order to run different functions based on the selected hash algorithm. For instance if “MD5” was selected then the function “Get-FileMD5” will be run on the $filename object given as input. The result is then saved in the $hash object. Finally, after the switch block I use the function “Write-RichTextBox” to display the $hash string object in the RichTextBox in the form. Voila! now the file checksum will be displayed as output on the form.

Well that pretty much covers everything. All you need to do is create the form script and the two button scripts and “dot source” them as explained above. You can now simply run the form code script and your GUI will appear, ready to use!
PrimalFormsScreenDump2
I have copied the full code of all three scripts below. I hope this helps you to write your own small GUIs in the future. Good luck 🙂

###############################################################################
##script:			FileChecksumTool.ps1
##
##Description:		Small GUI tool which can calculate the checksum of a file
#+					using a selection of the most common hash algorithms.
##Created by:		Noam Wajnman
##Creation Date:	May 7, 2014
###############################################################################
function GenerateForm {
	#region Import the Assemblies
	[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
	[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
	#endregion

	#region Generated Form Objects
	$form1 = New-Object System.Windows.Forms.Form
	$label3 = New-Object System.Windows.Forms.Label
	$richTextBox1 = New-Object System.Windows.Forms.RichTextBox
	$label2 = New-Object System.Windows.Forms.Label
	$label1 = New-Object System.Windows.Forms.Label
	$comboBox1 = New-Object System.Windows.Forms.ComboBox
	$textBox1 = New-Object System.Windows.Forms.TextBox
	$button2 = New-Object System.Windows.Forms.Button
	$button1 = New-Object System.Windows.Forms.Button
	$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState
	#endregion Generated Form Objects

	#----------------------------------------------
	#Generated Event Script Blocks
	#----------------------------------------------	
	$button1_OnClick= 
	{
	#########################################################################################
	#Dot Source button script
	$ScriptPath = $script:MyInvocation.MyCommand.Path
	$ScriptDir = Split-Path $scriptpath
	. "$ScriptDir\BrowseFileDialogue.ps1"
	#########################################################################################
	}

	$button2_OnClick= 
	{
	#########################################################################################
	#Dot Source button script
	$ScriptPath = $script:MyInvocation.MyCommand.Path
	$ScriptDir = Split-Path $scriptpath
	. "$ScriptDir\Get-FileChecksum.ps1"
	#########################################################################################
	}

	$OnLoadForm_StateCorrection=
	{#Correct the initial state of the form to prevent the .Net maximized form issue
		$form1.WindowState = $InitialFormWindowState
	}

	#----------------------------------------------
	#region Generated Form Code
	$System_Drawing_Size = New-Object System.Drawing.Size
	$System_Drawing_Size.Height = 364
	$System_Drawing_Size.Width = 430
	$form1.ClientSize = $System_Drawing_Size
	$form1.DataBindings.DefaultDataSourceUpdateMode = 0
	$form1.Font = New-Object System.Drawing.Font("Arial",9.75,0,3,0)
	$form1.Name = "form1"
	$form1.Text = "File Checksum Tool"

	$label3.DataBindings.DefaultDataSourceUpdateMode = 0
	$label3.Font = New-Object System.Drawing.Font("Arial",8.25,0,3,0)

	$System_Drawing_Point = New-Object System.Drawing.Point
	$System_Drawing_Point.X = 24
	$System_Drawing_Point.Y = 330
	$label3.Location = $System_Drawing_Point
	$label3.Name = "label3"
	$System_Drawing_Size = New-Object System.Drawing.Size
	$System_Drawing_Size.Height = 36
	$System_Drawing_Size.Width = 202
	$label3.Size = $System_Drawing_Size
	$label3.TabIndex = 7
	$label3.Text = "Created by Noam Wajnman May 2014. https://noamwajnman.wordpress.com"

	$form1.Controls.Add($label3)

	$richTextBox1.DataBindings.DefaultDataSourceUpdateMode = 0
	$System_Drawing_Point = New-Object System.Drawing.Point
	$System_Drawing_Point.X = 24
	$System_Drawing_Point.Y = 199
	$richTextBox1.Location = $System_Drawing_Point
	$richTextBox1.Name = "richTextBox1"
	$System_Drawing_Size = New-Object System.Drawing.Size
	$System_Drawing_Size.Height = 107
	$System_Drawing_Size.Width = 388
	$richTextBox1.Size = $System_Drawing_Size
	$richTextBox1.TabIndex = 6
	$richTextBox1.Text = ''

	$form1.Controls.Add($richTextBox1)

	$label2.DataBindings.DefaultDataSourceUpdateMode = 0
	$label2.Font = New-Object System.Drawing.Font("Arial Black",12,0,3,0)

	$System_Drawing_Point = New-Object System.Drawing.Point
	$System_Drawing_Point.X = 24
	$System_Drawing_Point.Y = 97
	$label2.Location = $System_Drawing_Point
	$label2.Name = "label2"
	$System_Drawing_Size = New-Object System.Drawing.Size
	$System_Drawing_Size.Height = 23
	$System_Drawing_Size.Width = 261
	$label2.Size = $System_Drawing_Size
	$label2.TabIndex = 5
	$label2.Text = "Select Algorithm"

	$form1.Controls.Add($label2)

	$label1.DataBindings.DefaultDataSourceUpdateMode = 0
	$label1.Font = New-Object System.Drawing.Font("Arial Black",12,0,3,0)

	$System_Drawing_Point = New-Object System.Drawing.Point
	$System_Drawing_Point.X = 24
	$System_Drawing_Point.Y = 13
	$label1.Location = $System_Drawing_Point
	$label1.Name = "label1"
	$System_Drawing_Size = New-Object System.Drawing.Size
	$System_Drawing_Size.Height = 23
	$System_Drawing_Size.Width = 261
	$label1.Size = $System_Drawing_Size
	$label1.TabIndex = 4
	$label1.Text = "Select file:"

	$form1.Controls.Add($label1)

	$comboBox1.DataBindings.DefaultDataSourceUpdateMode = 0
	$comboBox1.FormattingEnabled = $True
	$comboBox1.Items.Add("MD5")|Out-Null	
	$comboBox1.Items.Add("SHA1")|Out-Null	
	$comboBox1.Items.Add("SHA256")|Out-Null
	$comboBox1.Items.Add("SHA384")|Out-Null
	$comboBox1.Items.Add("SHA512")|Out-Null
	$System_Drawing_Point = New-Object System.Drawing.Point
	$System_Drawing_Point.X = 24
	$System_Drawing_Point.Y = 133
	$comboBox1.Location = $System_Drawing_Point
	$comboBox1.Name = "comboBox1"
	$System_Drawing_Size = New-Object System.Drawing.Size
	$System_Drawing_Size.Height = 24
	$System_Drawing_Size.Width = 261
	$comboBox1.Size = $System_Drawing_Size
	$comboBox1.TabIndex = 3

	$form1.Controls.Add($comboBox1)

	$textBox1.DataBindings.DefaultDataSourceUpdateMode = 0
	$System_Drawing_Point = New-Object System.Drawing.Point
	$System_Drawing_Point.X = 24
	$System_Drawing_Point.Y = 46
	$textBox1.Location = $System_Drawing_Point
	$textBox1.Name = "textBox1"
	$System_Drawing_Size = New-Object System.Drawing.Size
	$System_Drawing_Size.Height = 22
	$System_Drawing_Size.Width = 261
	$textBox1.Size = $System_Drawing_Size
	$textBox1.TabIndex = 2

	$form1.Controls.Add($textBox1)


	$button2.DataBindings.DefaultDataSourceUpdateMode = 0
	$button2.Font = New-Object System.Drawing.Font("Arial Black",8.25,0,3,0)

	$System_Drawing_Point = New-Object System.Drawing.Point
	$System_Drawing_Point.X = 304
	$System_Drawing_Point.Y = 114
	$button2.Location = $System_Drawing_Point
	$button2.Name = "button2"
	$System_Drawing_Size = New-Object System.Drawing.Size
	$System_Drawing_Size.Height = 57
	$System_Drawing_Size.Width = 108
	$button2.Size = $System_Drawing_Size
	$button2.TabIndex = 1
	$button2.Text = "Get File Checksum"
	$button2.UseVisualStyleBackColor = $True
	$button2.add_Click($button2_OnClick)

	$form1.Controls.Add($button2)


	$button1.DataBindings.DefaultDataSourceUpdateMode = 0
	$button1.Font = New-Object System.Drawing.Font("Arial Black",8.25,0,3,0)

	$System_Drawing_Point = New-Object System.Drawing.Point
	$System_Drawing_Point.X = 304
	$System_Drawing_Point.Y = 46
	$button1.Location = $System_Drawing_Point
	$button1.Name = "button1"
	$System_Drawing_Size = New-Object System.Drawing.Size
	$System_Drawing_Size.Height = 22
	$System_Drawing_Size.Width = 108
	$button1.Size = $System_Drawing_Size
	$button1.TabIndex = 0
	$button1.Text = "Browse"
	$button1.UseVisualStyleBackColor = $True
	$button1.add_Click($button1_OnClick)

	$form1.Controls.Add($button1)

	#endregion Generated Form Code

	#Save the initial state of the form
	$InitialFormWindowState = $form1.WindowState
	#Init the OnLoad event to correct the initial state of the form
	$form1.add_Load($OnLoadForm_StateCorrection)
	#Show the Form
	$form1.ShowDialog()| Out-Null

} #End Function

#Call the Function
GenerateForm



###############################################################################
##script:			BrowseFileDialogue.ps1
##
##Description:		Opens a window with a browse file dialogue and returns the
#+					filename selected.
##Created by:		Noam Wajnman
##Creation Date:	May 7, 2014
###############################################################################
#VARIABLES
$initialDirectory = "c:\"
#SCRIPT MAIN
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $initialDirectory
$OpenFileDialog.filter = "All files (*.*)| *.*"
$OpenFileDialog.ShowHelp = $true
$OpenFileDialog.ShowDialog() | Out-Null
$fileSelected = $OpenFileDialog.filename
$textbox1.text = $fileSelected


###############################################################################
##script:			Get-FileChecksum.ps1
##
##Description:		Takes two parameters filename and algorithm and returns the
#+					computed file checksum hash.
##Created by:		Noam Wajnman
##Creation Date:	May 7, 2014
###############################################################################
#VARIABLES
$algorithm = $combobox1.text
$filename = $textbox1.text
#FUNCTIONS
function Update-RichTextBox {	
	$RichTextBox1.refresh()
	$RichTextBox1.SelectionStart = $RichTextBox1.Text.Length
    $RichTextBox1.ScrollToCaret()
}
function Write-RichTextBox {
	param (
		[string]$message
	)
	$time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"	
	$RichTextBox1.AppendText("$time`n$message`n")
	Update-RichTextBox
}
function Show-ErrorBox {
    param (        
        $errorTitle,
        $errorText
    )    
    [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
    [System.Windows.Forms.MessageBox]::Show($errorText,$errorTitle)
}
function Get-FileMD5 {
    Param([string]$file)
	$md5 = [System.Security.Cryptography.HashAlgorithm]::Create("MD5")
	$IO = New-Object System.IO.FileStream($file, [System.IO.FileMode]::Open)
	$StringBuilder = New-Object System.Text.StringBuilder
	$md5.ComputeHash($IO) | % { [void] $StringBuilder.Append($_.ToString("x2")) }
	$hash = $StringBuilder.ToString() 
	$IO.Dispose()
	return $hash
}
function Get-FileSHA1 {
	Param([string]$file)	
	$sha1 = new-object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider
	$IO = [System.IO.File]::Open($file,[System.IO.Filemode]::Open, [System.IO.FileAccess]::Read)
	$hash = ([System.BitConverter]::ToString($sha1.ComputeHash($IO)) -replace '-','').ToLower()
	$IO.Dispose()
	return $hash
}
function Get-FileSHA384 {
	Param([string]$file)	
	$sha384 = new-object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider
	$IO = [System.IO.File]::Open($file,[System.IO.Filemode]::Open, [System.IO.FileAccess]::Read)
	$hash = ([System.BitConverter]::ToString($sha384.ComputeHash($IO)) -replace '-','').ToLower()
	$IO.Dispose()
	return $hash
}
function Get-FileSHA512 {
	Param([string]$file)	
	$sha512 = new-object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider
	$IO = [System.IO.File]::Open($file,[System.IO.Filemode]::Open, [System.IO.FileAccess]::Read)
	$hash = ([System.BitConverter]::ToString($sha512.ComputeHash($IO)) -replace '-','').ToLower()
	$IO.Dispose()
	return $hash
}
#SCRIPT MAIN
if ((test-path -PathType Leaf -Path $filename) -and $algorithm) {
	switch ($algorithm) {
		"MD5" { $hash = Get-FileMD5 -file $filename }
		"SHA1" { $hash = Get-FileSHA1 -file $filename }
		"SHA256" { $hash = Get-FileSHA256 -file $filename }
		"SHA384" { $hash = Get-FileSHA384 -file $filename }
		"SHA512" { $hash = Get-FileSHA512 -file $filename }
	}
	$result = "$algorithm hash of $filename is:`n$hash"
	Write-RichTextBox -message $result
}
else {
	$errorTitle = "Error - File name and algorithm not selected."
	$errorText = "Please select a valid file name and a hash algorithm and try again."
	Show-ErrorBox -errorText $errorText -errorTitle $errorTitle
}

Set VMTools time synchronization mode on all VMs in a cluster

Standard

When changing the time synchronization settings in your environment you may have to also make modifications in your VMWare infrastructure. Among other things it can be necessary to control if the VM should synchronize its time from the host server or not. Normally it’s a manual process where you have to click into the VM settings and go to the Options tab and select VMWare Tools and there check the box or not. This script will do all that for you and set the VMTools host time synchronization to the value you choose, on all VMs in a given cluster. This will save you a lot of time especially if you, like me, manage a VMWare infrastructure with hundreds of VMs.
Although the script is quite short I have divided it into three sections variables, functions and script main to simplify its structure.

Variables

#VARIABLES
$DebugPreference = "continue" #comment out to disable debug info
#Parameters
$vcenter = "some_vcenter_server" #your vcenter server name
$clusterName = "Some_Cluster" #advanced settings will be set on all VMs on this cluster
$TimeSync = $true #script will enable/disable time sync on host on the VMs based on this value. Valid values are $true and $false

Before you run the script you will need to fill out the parameters in the variables section. $clustername is the name of the cluster on which you want to the change the settings for your VMs. All VMs running on the cluster will have their VMTools host time sync value set to what you specify in $TimeSync.

Functions

#FUNCTIONS
function ConnectToVcenter {
	param(
		$vcenter
	)
	#Load snap-in and connect to vCenter
	if (-not (Get-PSSnapin -Name VMware.VimAutomation.Core)) {
		Add-PSSnapin VMware.VimAutomation.Core
	}
	if ($global:DefaultVIServers.Count -lt 1) {
		Connect-VIServer $vCenter
	}
}
function Set-SyncValue {
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)]$VM,
		[bool]$SyncValue
	)
	begin {
		Write-Debug "Creating configuration specification object"
		$ConfigSpec = New-Object VMware.Vim.VirtualMachineConfigSpec 
		$ConfigSpec.tools = New-Object VMware.Vim.ToolsConfigInfo 
		$ConfigSpec.tools.syncTimeWithHost = $SyncValue
	}
	process {
		#configure the VM
		Write-Debug "Setting VMTools time sync to $SyncValue on $($VM.Name)"
		$View = get-view -viewtype virtualmachine -Filter @{'name'=$VM.Name} 
		$View.ReconfigVM_task($ConfigSpec)
		sleep 3 #wait for the configuration to complete
		#Check if the VM was configured as desired.
		$AfterView = get-view -viewtype virtualmachine -Filter @{'name'=$VM.Name} 
		[bool]$result = [System.Convert]::ToBoolean($AfterView.Config.Tools.syncTimeWithHost)		
		if ($result -eq $SyncValue) {
			Write-Debug "Successfully set the Tools.syncTimeWithHost on $VM to $SyncValue"
		}
		else {
			Write-Debug "Error - $VM - Couldn't set the Tools.syncTimeWithHost to the desired value of $SyncValue"
		}
	}
	end {
		Write-Debug "Finished setting time sync values on VMs."
	}
}

The script uses two functions which I will go over now.
1. Function ConnectToVcenter
This function takes one parameter $vcenter which is the name of the vcenter server. If not already done the function adds the VMWare snap-in for powershell and then connects to vcenter to make ready for the commands we will run after.
2. Function Set-SyncValue
Set-SyncValue takes two parameters $VM and $SyncValue. $VM can be passed from the pipeline which makes it easy to use in combination with other VMWare commands. The function works by first creating a configuration specification object with the value given in $SyncValue. After this it loops though the given VMs and reconfigures them accordingly. After the command to configure a VM has been sent, the script waits 3 seconds and then checks if the setting was updated to the value you wanted.

Script Main

#SCRIPT MAIN
ConnectToVcenter -vcenter $vcenter
Get-Cluster $clusterName | Get-VM | Set-SyncValue -SyncValue $TimeSync 

The script main is very short and simple. First I connect to vcenter using the function ConnectToVcenter. The actual work is then done in a one liner where I simply pipe all the VMs from the Get-Cluster and Get-VM cmdlets to the Set-SyncValue function.

That’s it. I hope you find the script useful. I have copied the full version of the script in below.

#####################################################################################################
##Script:			Set-VMTools_TimeSync.ps1
##
##Description:		enables/disables the VMTools time sync mode on all VMs in a given cluster.
##Created by:		Noam Wajnman
##Credits:			Part of this script was based on this article: 
#+					https://psvmware.wordpress.com/tag/disable-vmware-tools-time-sync-using-powercli/
##Creation Date:	April 28, 2014
#####################################################################################################
#VARIABLES
$DebugPreference = "continue" #comment out to disable debug info
#Parameters
$vcenter = "some_vcenter_server"
$clusterName = "Some_Cluster" #advanced settings will be set on all VMs on this cluster
$TimeSync = $true #script will enable/disable time sync on host on the VMs based on this value. Valid values are $true and $false
#FUNCTIONS
function ConnectToVcenter {
	param(
		$vcenter
	)
	#Load snap-in and connect to vCenter
	if (-not (Get-PSSnapin -Name VMware.VimAutomation.Core)) {
		Add-PSSnapin VMware.VimAutomation.Core
	}
	if ($global:DefaultVIServers.Count -lt 1) {
		Connect-VIServer $vCenter
	}
}
function Set-SyncValue {
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)]$VM,
		[bool]$SyncValue
	)
	begin {
		Write-Debug "Creating configuration specification object"
		$ConfigSpec = New-Object VMware.Vim.VirtualMachineConfigSpec 
		$ConfigSpec.tools = New-Object VMware.Vim.ToolsConfigInfo 
		$ConfigSpec.tools.syncTimeWithHost = $SyncValue
	}
	process {
		#configure the VM
		Write-Debug "Setting VMTools time sync to $SyncValue on $($VM.Name)"
		$View = get-view -viewtype virtualmachine -Filter @{'name'=$VM.Name} 
		$View.ReconfigVM_task($ConfigSpec)
		sleep 3 #wait for the configuration to complete
		#Check if the VM was configured as desired.
		$AfterView = get-view -viewtype virtualmachine -Filter @{'name'=$VM.Name} 
		[bool]$result = [System.Convert]::ToBoolean($AfterView.Config.Tools.syncTimeWithHost)		
		if ($result -eq $SyncValue) {
			Write-Debug "Successfully set the Tools.syncTimeWithHost on $VM to $SyncValue"
		}
		else {
			Write-Debug "Error - $VM - Couldn't set the Tools.syncTimeWithHost to the desired value of $SyncValue"
		}
	}
	end {
		Write-Debug "Finished setting time sync values on VMs."
	}
}
#SCRIPT MAIN
ConnectToVcenter -vcenter $vcenter
Get-Cluster $clusterName | Get-VM | Set-SyncValue -SyncValue $TimeSync