The Collection

A collection of useful information.

PowerShell: Get First Open Drive Letter

Three lines of code to return the first in an array of available drive letters in the correct format for piping to New-PSDrive.

$letters = 65..90 | ForEach-Object{ [char]$_ + ":" }
$taken = Get-WmiObject Win32_LogicalDisk | select -expand DeviceID
$avail = ((Compare-Object -ReferenceObject $letters -DifferenceObject $taken)[1].InputObject).Replace(":","")

PowerShell: Random Password Generator.

Quick little script to generate a password meeting simple complexity (guaranteed one upper and one number at random positions in the string) guidelines.

$upper = 65..90 | ForEach-Object{ [char]$_ }
$lower = 97..122 | ForEach-Object{ [char]$_ }
$number = 48..57 | ForEach-Object{ [char]$_ }
$total += $upper
$total += $number
$total += $lower
$rand = ""
$i = 0
while($i -lt 10){ $rand += $total | Get-Random; $i++ }
"Initial: "+$rand
$rand = $rand.ToCharArray()

$num = 1..$rand.Count | Get-Random
$up = $num
while($up -eq $num){ $up = 1..$rand.Count | Get-Random }

"num "+$num
"up "+$up
$rand[($num-1)] = $number | Get-Random
$rand[($up-1)] = $upper | Get-Random
$rand = $rand -join ""
"Final: "+$rand

PowerShell: Get Remote Desktop Services User Profile Path from AD.

Bit goofy this, when trying to get profile path information for a user you may think Get-ADUser will provide you anything you would likely see in the dialog in AD Users and Computers, but you would be wrong. Get-ADUser -Identity <username> -Properties * does not yield a 'terminalservicesprofilepath' attribute.

Instead you must do the following:


You can use Get-ADUser to retrieve the DN for a user or any other method you prefer.

Citrix: Presentation Server 4.5, list applications and groups.

Quick script to connect to a 4.5 farm and pull a list of applications and associate them to the groups that control access to them. You will need to do a few things before this works:

Download the 4.5 SDK

If you are running this remotely you need to be in the "Distributed COM Users" group (Server 2k3) and will need to setup DCOM for impersonation (you can do this by running "Component Services" drilling down to the "local computer", right click and choose properties, clicking General properties and the third option should be set to Impersonate).

Finally you will need View rights to the Farm. If you are doing this remotely there is a VERY strong chance of failure is the account you are LOGGED IN AS is not a "View Only" or higher admin in Citrix. RunAs seems to be incredibly hit or miss, mostly miss.

$start = Get-Date
$farm = New-Object -ComObject MetaFrameCOM.MetaFrameFarm
$apps = New-Object 'object[,]' $farm.Applications.Count,2
$row = 0
[regex]$reg = "(?[^/]*)$"
foreach($i in $farm.Applications) {
	[string]$groups = ""
	$clean = $reg.Match($i.DistinguishedName).Captures.Value
	$apps[$row,0] = $clean
	foreach($j in $i.Groups) {
		if($groups.Length -lt 1){ $groups = $j.GroupName }else{ $groups = $groups+","+$j.GroupName }
	$apps[$row,1] = $groups

$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true
$excel.DisplayAlerts = $false
$workbook = $excel.Workbooks.Add()
$sheet = $workbook.Worksheets.Item(1)
$sheet.Name = "Dashboard"
$range = $sheet.Range("A1",("B"+$farm.Applications.Count))
$range.Value2 = $apps
$(New-TimeSpan -Start $start -End (Get-Date)).TotalMinutes

App-V 5.0: PowerShell VE launcher.

Quick little script to enable you to launch local apps into a VE. Can be run of two ways:

Prompts the user for an App-V app and then the local executable to launch into the VE.

Accepts command line arguments to launch the specified exe into the specified VE.

Import-Module AppvClient
if($args.Count -ne 2) {
	$action = Read-Host "App-V app to launch into (type 'list' for a list of apps):"

	while($action -eq "list") {
		$apps = Get-AppvClientPackage
		foreach($i in $apps){ $i.Name }
		$action = Read-Host "App-V app to launch into (type 'list' for a list of apps):"

	try {
		$apps = Get-AppvClientPackage $action
	catch {
		Write-Host ("Failed to get App-V package with the following error: "+$_)

	$strCmd = Read-Host "Local app to launch into VE:"

	try {
		Start-AppvVirtualProcess -AppvClientObject $app -FilePath $strCmd
	catch {
		Write-Host ("Failed to launch VE with following error: "+$_)
	$app = Get-AppvClientPackage $args[0]
	Start-AppvVirtualProcess -AppvClientObject $app -FilePath $args[1]


  1. Prompt-mode: AppV-Launcher.ps1
  2. CMDLine Mode: AppV-Launcher.ps1 TortoiseHg C:\Windows\Notepad.exe

Note: The arguments are positional, so it must be Virtual App then Local Executable in that order otherwise it will fail. There is no try/catch on the CMDLine mode as it expects you to know what you are doing (and want as much information about what went wrong as possible) and there is no risk of damage.

PowerShell: Date Picker.

Quick function to prompt a user to select a date. Usage is pretty straighforward.

$var = $(DatePicker "<title>").ToShortDateString()
function DatePicker($title) {
	[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
	[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
	$global:date = $null
	$form = New-Object Windows.Forms.Form
	$form.Size = New-Object Drawing.Size(233,190)
	$form.StartPosition = "CenterScreen"
	$form.KeyPreview = $true
	$form.FormBorderStyle = "FixedSingle"
	$form.Text = $title
	$calendar = New-Object System.Windows.Forms.MonthCalendar
	$calendar.ShowTodayCircle = $false
	$calendar.MaxSelectionCount = 1
	$form.TopMost = $true
		if($_.KeyCode -eq "Escape") {
			$global:date = $false
		$global:date = $calendar.SelectionStart
	return $global:date

Write-Host (DatePicker "Start Date")

Citrix: Creating Reports.

A bit of a different gear here, but here are a couple examples, one using Citrix 4.5 (Resource Manager) andone using Citrix 6.0 (EdgeSight).



$start = Get-Date

Import-Module ActiveDirectory
function SQL-Connect($server, $port, $db, $userName, $passWord, $query) {
	$conn = New-Object System.Data.SqlClient.SqlConnection
	$ctimeout = 30
	$qtimeout = 120
	$constring = "Server={0},{5};Database={1};Integrated Security=False;User ID={2};Password={3};Connect Timeout={4}" -f $server,$db,$userName,$passWord,$ctimeout,$port
	$conn.ConnectionString = $constring
	$cmd = New-Object System.Data.SqlClient.SqlCommand($query, $conn)
	$cmd.CommandTimeout = $qtimeout
	$ds = New-Object System.Data.DataSet
	$da = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
	return $ds

function Graph-Iterate($arList,$varRow,$varCol,$strPass) {
	Write-Host $arList[$i].depName
	foreach($i in $arList.Keys) {
		if($arList[$i].duration -ne 0) {
			if($arList[$i].depName.Length -gt 1) {
				if($arList[$i].depName -eq $null){ $arList[$i].depName = "UNKNOWN" }
				$sheet.Cells.Item($varRow,$varCol) = $arList[$i].depName
				$sheet.Cells.Item($varRow,$varCol) = ("{0:N1}" -f $arList[$i].duration)
				if($master -ne $true){ Iterate $arList[$i] $strPass }
	return $varcol

function Iterate($arSub, $strCom) {
	$indSheet = $workbook.Worksheets.Add()
	$sheetName = ("{0}-{1}" -f $strCom,$arSub.depName)
	Write-Host $sheetName
	$nVar = 1
	if($sheetName -eq "CSI-OPP MAX")
		Write-Host "The Var is:"
		Write-Host $nVar
		$sheetName = "{0} {1}" -f $sheetName,$nVar
	$strip = [System.Text.RegularExpressions.Regex]::Replace($sheetName,"[^1-9a-zA-Z_-]"," ");
	if($strip.Length -gt 31) { $ln = 31 }else{ $ln = $strip.Length }
	$indSheet.Name = $strip.Substring(0, $ln)
	$count = $arSub.Keys.Count
	$array = New-Object 'object[,]' $count,2
	$arRow = 0
	foreach($y in $arSub.Keys) {
		if($y -ne "depName" -and $y -ne "duration" -and $y.Length -gt 1) {
			$t = 0
			$array[$arRow,$t] = $y
			$array[$arRow,$t] = $arSub[$y]
	$rng = $indSheet.Range("A1",("B"+$count))
	$rng.Value2 = $array

function Create-Graph($lSheet,$lTop,$lLeft,$range, $number, $master, $catRange) {
	# Add graph to Dashboard and configure.
	$chart = $lSheet.Shapes.AddChart().Chart
	$chartNum = ("Chart {0}" -f $cvar3)
	$sheet.Shapes.Item($chartNum).Placement = 3
	$sheet.Shapes.Item($chartNum).Top = $top
	$sheet.Shapes.Item($chartNum).Left = $left
	if($master -eq $true) {
			$sheet.Shapes.Item($chartNum).Height = 500
			$sheet.Shapes.Item($chartNum).Width = 1220
			$sheet.Shapes.Item($chartNum).Height = 325
			$sheet.Shapes.Item($chartNum).Width = 400
		$chart.ChartType = 69
		$chart.SeriesCollection(1).XValues = $catRange

$port = "<port>"
$server = "<sqlserver>"
$db = "<db>"
$user = "<db_user>"
$password = "<pass>"
$query = "SELECT p.prid, p.account_name, p.domain_name, p.dtfirst, cs.instid, cs.sessid, cs.login_elapsed, cs.dtlast, cs.session_type, s.logon_time, s.logoff_time
FROM         dbo.principal AS p INNER JOIN
                      dbo.session AS s ON s.prid = p.prid INNER JOIN
                      dbo.ctrx_session AS cs ON cs.sessid = s.sessid"
#WHERE		p.account_name LIKE 'a[_]%'

$userlist = SQL-Connect $server $port $db $user $password $query
$users = @{}
foreach($i in $userlist.Tables) {
	if($i.account_name -notlike "h_*" -and $i.account_name -notlike "a_*" -and $i.account_name -ne "UNKNOWN" -and ([string]$i.logon_time).Length -gt 1 -and ([string]$i.logoff_time).Length -gt 1) {
		try {
			$info = Get-ADUser -Identity $i.account_name -Properties DepartmentNumber, Department, Company
		catch {
			$info = @{"Company"="Terminated";"Department"="Invalid";"DepartmentNumber"="0000"}
		if($info.Company.Length -lt 2) {
			$info = @{"Company"="Terminated";"Department"="Invalid";"DepartmentNumber"="0000"}
		if($users.Contains($info.Company) -eq $false) {
			$users[$info.Company] = @{}
			$users[$info.Company]['duration'] = (New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
			$users[$info.Company]['duration'] = $users[$info.Company]['duration']+(New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
		if($users[$info.Company].Contains(([string]$info.DepartmentNumber)) -eq $false) {
			$users[$info.Company][([string]$info.DepartmentNumber)] = @{}
			$users[$info.Company][([string]$info.DepartmentNumber)]['depName'] = $info.Department
			$users[$info.Company][([string]$info.DepartmentNumber)]['duration'] = (New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
			$users[$info.Company][([string]$info.DepartmentNumber)]['duration'] = $users[$info.Company][([string]$info.DepartmentNumber)]['duration']+(New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
		if($users[$info.Company][([string]$info.DepartmentNumber)].Contains($i.account_name) -eq $false) {
			$users[$info.Company][([string]$info.DepartmentNumber)][$i.account_name] = (New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
			$users[$info.Company][([string]$info.DepartmentNumber)][$i.account_name] = $users[$info.Company][([string]$info.DepartmentNumber)][$i.account_name]+(New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
	}elseif($i.account_name -ne "UNKNOWN" -and ([string]$i.logon_time).Length -gt 1 -and ([string]$i.logoff_time).Length -gt 1) {
		if($i.account_name -like "a_*") {
			$info = @{"Company"="Administrators";"Department"="Elevated IDs (A)";"DepartmentNumber"="1111"}
			$info = @{"Company"="Administrators";"Department"="Elevated IDs (H)";"DepartmentNumber"="2222"}
		if($users.Contains("Administrators") -eq $false) {
			$users['Administrators'] = @{}
			$users['Administrators']['duration'] = (New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
			$users['Administrators']['duration'] = $users['Administrators']['duration']+(New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
		if($users['Administrators'].Contains($info.DepartmentNumber) -eq $false) {
			$users['Administrators'][$info.DepartmentNumber] = @{}
			$users['Administrators'][$info.DepartmentNumber]['depName'] = $info.Department
			$users['Administrators'][$info.DepartmentNumber]['duration'] = (New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
			$users['Administrators'][$info.DepartmentNumber]['duration'] = $users['Administrators'][$info.DepartmentNumber]['duration']+(New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
		if($users['Administrators'][$info.DepartmentNumber].Contains($i.account_name) -eq $false) {
			$users['Administrators'][$info.DepartmentNumber][$i.account_name] = (New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
			$users['Administrators'][$info.DepartmentNumber][$i.account_name] = $users['Administrators'][$info.DepartmentNumber][$i.account_name]+(New-TimeSpan $i.logon_time $i.logoff_time).TotalHours
		if(([string]$i.logon_time).Length -lt 1 -and $i.account_name -ne "UNKNOWN"){ "No logon time: "+$i.account_name }
		if(([string]$i.logoff_time).Length -lt 1 -and $i.account_name -ne "UNKNOWN"){ "No logoff time: "+$i.account_name }

# Create Excel object, setup spreadsheet, name main page.
$excel = New-Object -ComObject excel.application
$excel.Visible = $true
$excel.DisplayAlerts = $false
$workbook = $excel.Workbooks.Add()
$row = 1
$col = 1
$sheet = $workbook.Worksheets.Item(1)
$sheet.Name = "Dashboard"

# Populate tracking vars.
# $row is the starting row to begin entering data into text cells.
# $cvar tracks $left position, resets when it reaches 3.
# $cvar3 tracks $top position, after every third graph it increments +340.
$row = 202
$col = 2
$cvar = 1
$cvar3 = 1
$top = 10
$left = 10
# Iterate through main element (Companies), $z returns company name (MGTS, MR, etc.).

$min = ($sheet.Cells.Item(($row)-1,1).Address()).Replace("$", "")
$tmin = ($sheet.Cells.Item(($row)-1,2).Address()).Replace("$", "")
foreach($q in $users.Keys) {
	$sheet.Cells.Item($row,1) = "Maritz Total Citrix Usage (by hours)"
	if($q -eq "114"){ $q = "Training IDs" }
	$sheet.Cells.Item($row,$col) = $q
	$sheet.Cells.Item($row,$col) = ("{0:N1}" -f $users[$q].duration)
$max = ($sheet.Cells.Item($row,($col)-1).Address()).Replace("$", "")
$range = $sheet.Range($min,$max)
$range2 = $sheet.Range($tmin,$max)
Create-Graph $sheet $top $left $range $cvar3 $true $range2
$col = 2
$top = ($top)+510

foreach($z in $users.Keys) {
	if($z.Length -gt 1 -and $z -ne "112 MAS"){
		# Setup chart location vars.
		if($cvar -eq 1) {
			$left = 10
		}elseif($cvar -eq 2){
			$left = 420
		}elseif($cvar -eq 3) {
			$left = 830
		$col = 2
		$sheet.Cells.Item($row,1) = $z
		# Track chart range minimum cell address.
		$min = ($sheet.Cells.Item(($row)-1,1).Address()).Replace("$", "")
		$tmin = ($sheet.Cells.Item(($row)-1,2).Address()).Replace("$", "")
		# Iterate through secondary element (Departments), $i returns department name.

		# Graph-Iterate Here
		$vLoc = Graph-Iterate $users[$z] $row $col $z
		# Track chart range maximum cell address.
		$max = ($sheet.Cells.Item($row,($vLoc)-1).Address()).Replace("$", "")
		$range = $sheet.Range($min,$max)
		$range2 = $sheet.Range($tmin,$max)
		Create-Graph $sheet $top $left $range $cvar3 $false $range2
		# Increment or reset tracking vars.
		if($cvar -eq 3) {
			$top = ($top)+340
		if($cvar -eq 1 -or $cvar -eq 2){ $cvar++ }elseif($cvar -eq 3){ $cvar = 1}
# Show dashboard page rather than some random department.

New-TimeSpan -Start $start -End (Get-Date)

PowerShell: I'm Going To Replace You With A Script.

We've all said it, some of us have gone to great lengths to actually DO it.

$screen = [System.Windows.Forms.SystemInformation]::VirtualScreen
[int]$target = 60
[int]$i = 1
while($i -lt $target){
    $randx = Get-Random -Minimum 1 -Maximum $screen.Width
    $randy = Get-Random -Minimum 1 -Maximum $screen.Height
    [Windows.Forms.Cursor]::Position = "$($randx), $($randy)"
	Start-Sleep -Seconds 10

Initial testing indicates this would replace approx. 80% of corporate IT. Welcome your new overlords.

Well, I supposed you'd need to up the duration...


PowerShell: XenServer Get VM IP Address

This like all of these PowerShell XenServer examples requires you load the snapin and an active connection to a XenServer.

First you need to get a reference to your VM of choice, in this case I know the exact VM by name so I do this:

$vms = Get-XenServer:VM | Where-Object {$_.name_label -eq "<nameofVM>"}

Now I want to read the guest metrics on this VM to find the IP Address, note that this requires XSTools to be installed and the IP address wont be available right away, typically it seems to only be available after the machine has both booted, and had time for the XSTools to report it's IP to guest metrics, but once it's available you can get it by doing this:

Get-XenServer:VM_guest_metrics.Networks -VMGuestMetrics $vms.guest_metrics

Be aware that if you want to get a list of IP's for more than one VM you will need to foreach through $vms and run the command for each one.

One potential use of this is to, say, clone a new VM from a template, start it, wait for it to establish a network connection then Test-Connection until you get a result, at which time you can proceed to do whatever else you need to do (via WinRM for instance, if you have it enabled in the template).

PowerShell: XenServer Messages (Since)

If you are expecting a message to occur after an action you are taking the following may be of use to you.

$messages = Get-XenServer:Message.Since -Since [System.DateTime]::Now
foreach($i in $messages.Values) { $i }

Obviously this doesn't do anything intelligent like look for the specific event, but once you see the output you can customize it to do whatever you want.

PowerShell: XenServer Recreate ISO Store

A little too much code to past directly on SquareSpace (haven't had time to figure out a way around that) but here it is.

PowerShell: Destroy CIFS ISO Library

This is step one of two in destroying (and then re-creating) a CIFS ISO library in XenServer, the reasons you may need to do this are varied, this is personally useful to me in a lab environment when the repo location or user credentials may change often. Everythign below requires the XenServer PSSnapin.

First thing we want to do is snag the info for the SR we want to delete:

$sr = Get-XenServer:SR | Where-Object {$_.content_type -eq "iso" -and $_.type -ne "udev" -and $_.name_label -ne "XenServer Tools"}

This filters out the CD-ROM (udev) and XS Tools and just returns us SR's whose content type is ISO, so it shouldn't matter what you name your repo (um, as long as you don't name it XenServer Tools I guess).

Now we need to unplug it:

foreach($i in $sr.PBDs){Invoke-XenServer:PBD.Unplug -SR $i}

Do you need to foreach this? Probably not. But if you DO happen to set it up with multiple PBD's then it will still fail on the next step because you only unplugged some of the PBD's, when in doubt, be thorough.

Now lets remove the SR:

Invoke-XenServer:SR.Forget -SR $sr.name_label

There you go, the next step will be to ask for a path, user and password to create a new ISO Library, which I'll cover next time.


App-V 5.0: Package Conversion Script

A quick PowerShell script with logging to convert a directory full of App-V packages.

$src = "<source path>\"
$dst = "<destination path>"
$logdir = "<logfile location>\ConversionLog.txt"
Import-Module AppvPkgConverter
$list = dir $src|where {$_.mode -match "d"}
If((Test-Path $logdir) -eq $false)
	New-Item($logdir) -Type File
foreach($i in $list)
	Write-Host $src$i
	$conv = ConvertFrom-AppvLegacyPackage -SourcePath $src$i -DestinationPath $dst
	If($conv.Error -ne $null -or $conv.Warnings -ne $null)
		Add-Content -Path $logdir -Value ($conv.Source+" appears to have failed...`n")
		Add-Content -Path $logdir -Value ("Error: "+$conv.Errors+"`nWarning: "+$conv.Warnings+"`n")
	}elseif($conv.Information -ne $null){
		Add-Content $logdir $conv.Information"`n"
		Add-Content -Path $logdir -Value ($conv.Source + " completed ok, no Errors or Warnings...`n")

XenServer Appliance Import Fails

If you find your import sitting at "Connecting..." with a reference to the specific VDI it is trying to create at the end, 99% chance it failed to get an IP. At the last phase of the import just specify a static IP, in my case for whatever reason DHCP wasn't responding fast enough so even though it LOOKED like it was getting an IP it just wasn't making the connection. Manually setting an IP solved it.

Speaking Of Things Nobody Tested...

Confirmed this with a few people now.

If you have a sequence, and you update it, and then say, you delete the _2.sft after making a _3.sft (because why have them all lying around wasting space) and you open the sequence, make another changer, then save it. You will get a _2.sft 90% of the time.

Which when put into the console will say it's not the right version lineage.

Thereby giving you the choice of keeping all your SFT's around, forever, or jumping through a bunch of hoops trying to get it back into a correct lineage (like, say, dumping the entire app out of the console and reimporting it, which will again 90% of the time give you all kinds of problems because the clients will see they aren't supposed to have it, but do have it, but it's the wrong version, so the only way to fix it is to manually runn SFTMIME commands on each machine to clear it out).

Let's call it what it is.

Really poor software. In no way, shape, or form acceptable for a paid product from one of the largest software companies on the planet.

PowerShell: Persistent Environment Variables.

This comes up pretty often and if you haven't dealt with env. variables much in scripting (or even batch files) you may be confused as to why your variables don't stick around. For example:

$Env:SFT_SOFTGRIDSERVER = "appserver"

Perfectly valid, but only for that PowerShell session. Not sure why but they make you use .Net to set it permanently, and, while syntactically more complex, it isn't difficult.

[Environment]::SetEnvironmentVariable("SFT_SOFTGRIDSERVER", "appvserver", "Machine")

Likewise you can get EV's:

[Environment]::GetEnvironmentVariable("SFT_SOFTGRIDSERVER", "Machine")

It is important to note that once you set it with .Net you wont see it in $Env until you start a new session, you HAVE to retrieve it via .Net in order to see it in the same session. If you really need some functionality of $Env you can add it both ways, but I honestly can't think of a good reason you would need to do that. But it wont harm anything.