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

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
}

Get HBA device info from remote servers and export to CSV

Standard

If you work with SAN storage and fibre channel adapters it can be very useful to get an overview of all your HBAs (host bus adapters) on your servers. This script uses WMI to get HBA info like WWN, driver, version, model etc. from remote servers and then export it to a CSV file. You will then have a consolidated view of all your HBA devices with detailed information about them.
You will need to create the file servers.txt in the script directory and enter the names (one per line) of the servers you want to get the info from.
I have divided this script into three sections variables, functions and script main. I will now briefly explain how I created each section.

Variables

#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath #path to the directory in which the script is run.
$servers = @(gc "$dir\servers.txt") #create servers.txt in the script directory and enter the names of servers you wish to query for HBA info.
$CSV = "$dir\HBAs.csv" #results will be exported to a csv file with this path

$servers is an array of server names populated by running Get-Content on the file servers.txt. $CSV is the path of the CSV file which will be created at the end of the script run. Both objects $servers and $CSV are defined using the object $dir which always points to the directory which the script was started in. This is practical as it allows me to copy and move the script around without having to change any paths.

Functions

1. Function Get-HBAInfo

#FUNCTIONS
function Get-HBAInfo {
	[CmdletBinding()]
	[OutputType([System.String])]
	param(  
		[parameter(ValueFromPipeline = $true)]$server  
	)	
	process {
		$WMI = Get-WmiObject -class MSFC_FCAdapterHBAAttributes -namespace "root\WMI" -computername $server		
		$WMI | % {
			$HBA = "" | select "server","WWN","DriverName","DriverVersion","FirmwareVersion","Model","ModelDescription"
			$HBA.server = $server
			$HBA.WWN = (($_.NodeWWN) | % {"{0:x}" -f $_}) -join ":"				
			$HBA.DriverName       = $_.DriverName  
			$HBA.DriverVersion    = $_.DriverVersion  
			$HBA.FirmwareVersion  = $_.FirmwareVersion  
			$HBA.Model            = $_.Model  
			$HBA.ModelDescription = $_.ModelDescription
			$HBA
		}			
	}	
}

This function takes a single parameter $server and runs a WMI query on it to get the HBA information. For each HBA device found on the server A custom object called $HBA is created and returned. The function can take input from the pipeline which is practical as you can simply pass the server name to the function using another script or cmdlet if you want.

Script Main

#SCRIPT MAIN
clear
$HBAs = @($servers | Get-HBAInfo)
$HBAs | Export-Csv $CSV -NoTypeInformation -Force

The script main is very simple consisting of only two lines. First I use the Get-HBAInfo function to get the HBA information from the given servers. Then, in the second line, the results are exported to CSV.

I have copied in the full script below. I hope you find it useful. Enjoy!!

################################################################################################
##Script:			Get-HBAInfo.ps1
##
##Description:		Gets information about HBAs on the given servers using WMI and exports it to 
#+					CSV. Remember to create the file servers.txt in the script directory and
#+					enter the names (one per line) of the servers you want to get the info from.
##Created by:		Noam Wajnman
##Creation Date:	February 28, 2013
##Updated:			April 08, 2014
################################################################################################
#FUNCTIONS
function Get-HBAInfo {
	[CmdletBinding()]
	[OutputType([System.String])]
	param(  
		[parameter(ValueFromPipeline = $true)]$server  
	)	
	process {
		$WMI = Get-WmiObject -class MSFC_FCAdapterHBAAttributes -namespace "root\WMI" -computername $server		
		$WMI | % {
			$HBA = "" | select "server","WWN","DriverName","DriverVersion","FirmwareVersion","Model","ModelDescription"
			$HBA.server = $server
			$HBA.WWN = (($_.NodeWWN) | % {"{0:x}" -f $_}) -join ":"				
			$HBA.DriverName       = $_.DriverName  
			$HBA.DriverVersion    = $_.DriverVersion  
			$HBA.FirmwareVersion  = $_.FirmwareVersion  
			$HBA.Model            = $_.Model  
			$HBA.ModelDescription = $_.ModelDescription
			$HBA
		}			
	}	
}
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath #path to the directory in which the script is run.
$servers = @(gc "$dir\servers.txt") #create servers.txt in the script directory and enter the names of servers you wish to query for HBA info.
$CSV = "$dir\HBAs.csv" #results will be exported to a csv file with this path
#SCRIPT MAIN
clear
$HBAs = @($servers | Get-HBAInfo)
$HBAs | Export-Csv $CSV -NoTypeInformation -Force

Powershell – Get DNS A records and export to CSV

Standard

When managing and cleaning up your IP addresses/ranges it can be very useful to get some lists of the records you have in your DNS zones. Here’s a simple script which gets the IP addresses/hostnames of the given DNS zones and exports the results to CSV.
The script is split into two parts Variables and script main. I will walk through and explain both parts below.

Variables

#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$CSV = "$dir\DNS_A_records.csv"
#parameters
$DNSServer = "Some_DNS_server"
$Zone1 = "myzone.local"
$Zone2 = "me.myzone.local"

I use the $dir object in the paths of my scripts as this object always points to the directory which the script has been run from. This is practical as it allows you to move/copy the script around without having to change any paths. $CSV is the path to the CSV file which will be created at the end of the script run.
Remember to fill out the parameters section before you run the script! $DNSServer is the name of your DNS server. $Zone1, $Zone2… etc are the names of the DNS zones which you want to get A records from. If you have more than 2 zones to get A records from then just add more objects to match the number of zones you need and add the zone names.

Script Main

#SCRIPT MAIN
clear
$DNS_Zones = @()
$DNS_Zones += $Zone1
$DNS_Zones += $Zone2
$hosts = @()
$DNS_Zones | % {
	$zone = $_
	Write-Host "Getting DNS A records from $zone"	
	$DNS_A_records = @(Get-WmiObject -Class MicrosoftDNS_AType -NameSpace Root\MicrosoftDNS -ComputerName $DNSServer -Filter "ContainerName = `'$zone`'")
	$DNS_A_records | % {
		$hostA = "" | select "hostname","IPAddress"
		$hostA.hostname = $_.OwnerName
		$hostA.IPAddress = $_.IPAddress
		$hosts += $hostA
	}
}
$hosts = $hosts | Sort-Object @{Expression={[Version]$_.IPAddress}}
$hosts | Export-Csv $CSV -NoTypeInformation -Force

In the script main I first create the array $DNS_Zones which will hold the different zone names. Then the zone names ($Zone1, $Zone2… etc.) are added to the array. If you created more than two zone objects in the variables section you must add them here too.
I now create the $hosts array which will hold the records we will wish export later. The next thing that happens is that we loop through the $DNS_Zones array. For each DNS Zone we get all A records using WMI and for each of these, a custom object with the properties hostname and IPAddress is created and added to the $hosts array.
I then sort the $hosts array by IP Address using the following code:
$hosts | Sort-Object @{Expression={[Version]$_.IPAddress}}
The @{Expression} argument allows you to add the [version] type declaration on the IPAddress property on the array elements/objects which in turn enables you to easily sort the IPs.
Finally the $hosts array is exported to CSV using the great Export-CSV powershell cmdlet.
I have copied in the full script below.I hope you find it useful. Enjoy!

################################################################################################
##Script:			Get-DNS_A_Records.ps1
##
##Description:		Gets all DNS A records from a given DNS server and exports the information 
#+					to a CSV file.
##Created by:		Noam Wajnman
##Creation Date:	April 07, 2014
################################################################################################
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$CSV = "$dir\DNS_A_records.csv"
#parameters
$DNSServer = "Some_DNS_server"
$Zone1 = "myzone.local"
$Zone2 = "me.myzone.local"
#SCRIPT MAIN
clear
$DNS_Zones = @()
$DNS_Zones += $Zone1
$DNS_Zones += $Zone2
$hosts = @()
$DNS_Zones | % {
	$zone = $_
	Write-Host "Getting DNS A records from $zone"	
	$DNS_A_records = @(Get-WmiObject -Class MicrosoftDNS_AType -NameSpace Root\MicrosoftDNS -ComputerName $DNSServer -Filter "ContainerName = `'$zone`'")
	$DNS_A_records | % {
		$hostA = "" | select "hostname","IPAddress"
		$hostA.hostname = $_.OwnerName
		$hostA.IPAddress = $_.IPAddress
		$hosts += $hostA
	}
}
$hosts = $hosts | Sort-Object @{Expression={[Version]$_.IPAddress}}
$hosts | Export-Csv $CSV -NoTypeInformation -Force

Powershell – Get last boot time on remote servers and export results to CSV

Standard

Here’s a script to help you get the last boot time from remote windows servers. If you need to check the uptime of servers or troubleshoot unexpected restarts etc. then this script can be very useful.
The script uses WMI to get the information from the remote servers and then exports the results to a CSV file in the directory where the script was run. I have structured the script in three sections variables, functions and script main. I will go over these sections one by one now.

Variables

#VARIABLES

#$DebugPreference = "continue" #uncomment to get debug info
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath #path to the directory in which the script is run.
$servers = "$dir\Servers.txt" #last boot time will be retrieved on all the servers given in this file.
$CSVPath = "$dir\LastBootTimes.csv" #results will be exported to csv to a file with this path

The object $dir always points to the directory in which the script is run and is used in the paths to files I work with in the script. This makes the script more robust because I can copy the script wherever I want without having to change any paths at all or edit the script.
$servers is the path to “servers.txt” file. You must create this file before you run the script and enter the names (one per line) of the servers you want to get the last boot time from.
$CSVPath is the path to CSV file which will be created when the script is run.

Functions

1. Get-LastBootTime

#FUNCTIONS
function Get-LastBootTime {
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][System.String[]]$server = $env:COMPUTERNAME
	)
	begin {
		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
			)
			$socket = new-object Net.Sockets.TcpClient
			$connect = $socket.BeginConnect($server, 135, $null, $null) #port set to 135 (RPC)
			$NoTimeOut = $connect.AsyncWaitHandle.WaitOne(500, $false) #timeout value set to 500 ms
			if ($NoTimeOut) {
				$socket.EndConnect($connect) | Out-Null
				return $true				
			}
			else {
				return $false
			}
		}
	}
	process {		
		$BootTime = $null
		$server = $($server).toUpper()	
		$alive = Test-PortAlive -server $server
		if ($alive) {
			Write-Debug "connection to $server is open on port $port"			
			$OSInfo = Get-WmiObject win32_operatingsystem -ComputerName $server #get the info with WMI
			$BootTime = $OSInfo.ConvertToDateTime($OSInfo.LastBootUpTime) #convert to datetime
			$BootTime = '{0:yyyy-MM-dd HH:mm:ss}' -f $BootTime #parse time to sortable format
			if ($BootTime) {
				Write-Debug "$server was rebooted last time at:`t$BootTime"					
				$result = "" | select "Server","LastBootTime" #creating a custom object
				$result.Server = $server
				$result.LastBootTime = $BootTime
				$result #return the $result object
			}
			else {
				Write-Debug "couldn't get boot time from server $server"
			}
		}			
		else {
			Write-Debug "Error - connection to $server is not open on port $port"			
		}
	}	
}

the function Get-LastBootTime takes one parameter $server which can also be passed to it via the pipeline.
Pipeline functions usually have three parts “begin”, “process” and “end” (this function only uses “begin” and “process”). In the begin section I have included another function “Test-PortAlive” but I won’t go into details about it as I have covered this in a previous post dedicated to that function. In this script I use it to test if the server is alive and the RPC port 135 is open as this is needed to run the WMI query.
The “process” part of the function is where last boot time information is retrieved using WMI. Basically the function attempts to get the info. If successful then a custom object with the properties “server” and “LastBootTime” is created and returned.

Script Main

#SCRIPT MAIN

clear
$BootTimes = @(gc $servers | Get-LastBootTime) #create and populate array with the last boot times of the given servers.
$BootTimes = $BootTimes | Sort-Object -Property "LastBootTime" #Sort array by last boot time
$BootTimes #Prints the $bootTimes array to the console 
$BootTimes | Export-Csv $CSVPath -NoTypeInformation -Force #Export the results to CSV

In the script main the first thing which happens is to create the array $BootTimes and fill it with the data from Get-LastBootTime function. This is done by running Get-Content on the servers.txt file and then piping the server names to the Get-LastBootTime function.
After this I sort the $BootTimes array by last boot time.
Finally the $BootTimes array is exported to CSV in the script directory.
I have copied in the full script below. I hope you will find it useful.
Enjoy!!

################################################################################################
##Script:			Get-LastBootTime.ps1
##
##Description:		Gets the time of the last boot on the servers given in "servers.txt" and
#+					exports the results to a csv file in the script dir.
##Created by:		Noam Wajnman
##Creation Date:	May 21, 2013
##Updated:			April 02, 2014
################################################################################################
#FUNCTIONS
function Get-LastBootTime {
	[CmdletBinding()]
	[OutputType([System.String])]
	param(
		[Parameter(ValueFromPipeline=$true)][System.String[]]$server = $env:COMPUTERNAME
	)
	begin {
		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
			)
			$socket = new-object Net.Sockets.TcpClient
			$connect = $socket.BeginConnect($server, 135, $null, $null) #port set to 135 (RPC)
			$NoTimeOut = $connect.AsyncWaitHandle.WaitOne(500, $false) #timeout value set to 500 ms
			if ($NoTimeOut) {
				$socket.EndConnect($connect) | Out-Null
				return $true				
			}
			else {
				return $false
			}
		}
	}
	process {		
		$BootTime = $null
		$server = $($server).toUpper()	
		$alive = Test-PortAlive -server $server
		if ($alive) {
			Write-Debug "connection to $server is open on port $port"			
			$OSInfo = Get-WmiObject win32_operatingsystem -ComputerName $server #get the info with WMI
			$BootTime = $OSInfo.ConvertToDateTime($OSInfo.LastBootUpTime) #convert to datetime
			$BootTime = '{0:yyyy-MM-dd HH:mm:ss}' -f $BootTime #parse time to sortable format
			if ($BootTime) {
				Write-Debug "$server was rebooted last time at:`t$BootTime"					
				$result = "" | select "Server","LastBootTime" #creating a custom object
				$result.Server = $server
				$result.LastBootTime = $BootTime
				$result #return the $result object
			}
			else {
				Write-Debug "couldn't get boot time from server $server"
			}
		}			
		else {
			Write-Debug "Error - connection to $server is not open on port $port"			
		}
	}	
}
#VARIABLES
#$DebugPreference = "continue" #uncomment to get debug info
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath #path to the directory in which the script is run.
$servers = "$dir\Servers.txt" #last boot time will be retrieved on all the servers given in this file.
$CSVPath = "$dir\LastBootTimes.csv" #results will be exported to csv to a file with this path
#SCRIPT MAIN
clear
$BootTimes = @(gc $servers | Get-LastBootTime) #create and populate array with the last boot times of the given servers.
$BootTimes = $BootTimes | Sort-Object -Property "LastBootTime" #Sort array by last boot time
$BootTimes #Prints the $bootTimes array to the console 
$BootTimes | Export-Csv $CSVPath -NoTypeInformation -Force #Export the results to CSV

Powershell – Test TCP ports on remote servers

Standard

From time to time it is necessary to check if specific TCP ports are open on remote servers. If you have many servers to check it can be a hassle to use telnet or other tools and check each server one by one. It is also often useful in other scripts to test if a remote server/port is alive before running code on them. To accomplish this I have written this little function.

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
	}
}

The function takes two parameters $server and $port. $server is the name of the remote server to test and $port is the number of the TCP port to check the status of. The $server parameter can even be passed via the pipeline making it very easy to run. The function returns either $true or $false depending on whether the port is open or not. To avoid long wait times due to closed ports I have included a relatively short timeout value of 500 ms before the result is determined.
I have included a few examples below of how to call the function.
1. Normal

 
Test-PortAlive -port 135 "some_server" 

Here I just run the function as normal and pass both params. I chose port 135 in this example but it could be any port.
2. Pipeline

 
$Array_of_Server_Names | Test-PortAlive -port 135

In this example I am using an array to pass the server names to the function via the pipeline. I again chose port 135 in this example.

That’s it. I hope you find this function useful. Enjoy!!