Creating Shortcuts on Redirected Read-Only Desktops

Recently, I had a customer request that their RDS servers have the desktop locked down. They didn’t want users to make any changes to the desktops. After some google searches, it was clear that most people redirected the desktop and then changed the user’s permissions to read only. This worked, but prevented us from using our standard practice of creating/refreshing shortcuts via GPO Preferences. Because the user had read only rights, the GPO couldn’t create the shortcuts. After some blood, sweat and tears, I developed a PowerShell script that would accomplish this task. To set it up, you have to populate the application information the same as you would the GPO, then tell the script what OU to look in for users (you could also set it to the domain root).

The script relies on Windows Scripting Host to create the shortcuts, so I created a function to do this. Its inputs are the shortcut target path, optional arguments, alternate icon path and AD Group Name if you have shortcuts that you want limited to specific users or for licensing purposes.

<#
TITLE: 		Create shortcuts on a roaming desktop based on AD membership
AUTHOR: 	Michael Kenning (https://powerscripter.wordpress.com)
VERSION: 	1.3 (release)
DATE:		2 AUG 2016
NOTE:		Change the variables to match your organization and the Application Definitions to match your apps. Also, add the Application variable to the $app Array!
#>

### VARIABLES ###
$userRoot = "\\SERVERFS1\Users$\"
$ou = "OU=Organization,DC=domain,DC=local"
$domainName = "domain"
$properties = "ProfilePath","HomeDrive","HomeDirectory","Enabled"
# Permission Levels: "R" = Read, "W" = Write, "M" = Modify, "RX" = Read and Execute, "F" = Full Control
$accessLevelModify = '(M,W,R,RX)'
$accessLevelRead = '(R,RX)'
$accessLevelFull = "F"
# Inheritance levels: (OI) - object inherit, (CI) - container inherit, (IO) - inherit only,(NP) - don’t propagate inherit
$inheritance = "(OI)(CI)"
$adminName = "BUILTIN\Administrators"
### END VARIABLES ###

### APPLICATION DEFINITIONS ###
$msWord = @{Name="Microsoft Word";Target="C:\Program Files (x86)\Microsoft Office\Office16\WINWORD.EXE";Arguments="";GroupName=""}
$msExcel = @{Name="Microsoft Excel";Target="C:\Program Files (x86)\Microsoft Office\Office16\EXCEL.EXE";Arguments="";GroupName=""}
$msPpnt = @{Name="Microsoft PowerPoint";Target="C:\Program Files (x86)\Microsoft Office\Office16\POWERPNT.EXE";Arguments="";GroupName=""}
$msOutlook = @{Name="Microsoft Outlook";Target="C:\Program Files (x86)\Microsoft Office\Office16\OUTLOOK.EXE";Arguments="";GroupName=""}
$msVisio = @{Name="Microsoft Visio";Target="C:\Program Files (x86)\Microsoft Office\Office16\VISIO.EXE";Arguments="";GroupName=""}
$msPub = @{Name="Microsoft Publisher";Target="C:\Program Files (x86)\Microsoft Office\Office16\MSPUB.EXE";Arguments="";GroupName=""}
$acrobatPro = @{Name="Adobe Acrobat Pro";Target="C:\Program Files (x86)\Adobe\Acrobat 2015\Acrobat\Acrobat.exe";Arguments="";GroupName="Adobe Acrobat Pro";WorkingDir="C:\Program Files (x86)\Adobe\Acrobat 2015\Acrobat\"}
$url1 = @{Name="URL Shortcut 1";Target="C:\Program Files (x86)\Internet Explorer\iexplore.exe";Arguments="https://website.com/sublevel/page.asp";GroupName="";IconPath="C:\icons\url1.ico"}

# -- Build the array of applications to create
$apps = $msWord,$msExcel,$msPpnt,$msOutlook,$msVisio,$msPub,$acrobatPro,$url1
### END APPLICATIONS DEFINITIONS ###

function Create-Shortcut {

	param (
        [Parameter(Mandatory=$true,Position=0)]
        [ValidateNotNullOrEmpty()]
        [String]
		$Path,
        [Parameter(Mandatory=$true,Position=1)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Target,
        [Parameter(Mandatory=$false,Position=2)]
        [String]
        $IconPath,
        [Parameter(Mandatory=$false,Position=3)]
        [String]
        $Arguments,
        [Parameter(Mandatory=$false,Position=4)]
        [String]
        $WorkingDir
    )
     
	$WshShell = New-Object -comObject WScript.Shell
	$shortcut = $WshShell.CreateShortcut($path)
	$shortcut.TargetPath = $target
	$shortcut.Arguments = $arguments
	if ($iconPath -ne "") {
		$shortcut.IconLocation = $iconPath
	}
	$shortcut.WorkingDirectory = $workingDir
	$shortcut.Save()
}

# - Get a list of enabled users in the OU and sub-OUs specified above
$users = Get-ADUser -Filter * -SearchBase $ou -Properties $properties | where {$_.enabled -eq "True"}

# - Loop through each user and create the shortcuts
ForEach ($user in $users) {
	Write-Host "Working on user " $user.Name -ForegroundColor Green
	$userPath = $userRoot + $user.SamAccountName + "\Desktop"
	$userName = $domainName + "\" + $user.Name
	Write-Host $userName

	# - If the Desktop folder does not exists, create it and grant the correct permissions
	if (!(Test-Path $userPath)) {
		New-Item -Path $userPath -Type Directory
		# -- Reset Folder Permissions and Inheritance on the Home Folder
		$command1 = "ICACLS.exe " + "`"" + $userPath + "`" /RESET /T"
		$command2 = "ICACLS.exe " + "`"" + $userPath + "`" /inheritance:d /inheritance:r"
		# -- Grant permissions to the Administrator User/Group and the SYSTEM account
		$command3 = "ICACLS.exe " + "`"" + $userPath + "`" /GRANT:r " + "`"" + $adminName + "`":" + $inheritance + $accessLevelFull
		$command4 = "ICACLS.exe " + "`"" + $userPath + "`" /GRANT:r " + "`"" + "SYSTEM" + "`":" + $inheritance + $accessLevelFull
		# -- Grant permissions to the user
		$command5 = "ICACLS.exe " + "`"" + $userPath + "`" /GRANT:r " + "`"" + $userName + "`":" + $inheritance + $accessLevelRead
		
		cmd /c $command1
		cmd /c $command2
		cmd /c $command3
		cmd /c $command4
		cmd /c $command5
	}

	# -- Delete what's already in the Desktop (Optional)
	# Remove-Item $userPath\*.* -Confirm:$false
	
	# -- Loop through each app and create shortcuts
	ForEach ($app in $apps) {
		Write-Host "Working on app " $app.Name -Foregroundcolor Green
		# -- All Users get these shortcuts
		If ($app.GroupName -eq "") {
			$path = $userPath + "\" + $app.Name + ".lnk"
			Write-Host "Creating Shortcut for " $app.Name -Foregroundcolor Yellow
			Create-Shortcut -Path $path -Target $app.Target -IconPath $app.IconPath -Arguments $app.arguments -WorkingDir $app.workingDir
		}
		# -- AD Group Specific shortcuts
		If ($app.GroupName -ne "") {
			If ($app.GroupName -eq "Application1") {
			}
			Else {
				If ((Get-ADGroupMember -Identity $app.GroupName -Recursive | Select -ExpandProperty Name) -contains $user.Name) {
					Write-Host "Creating Shortcut for " $app.Name -Foregroundcolor Yellow
					$path = $userPath + "\" + $app.Name + ".lnk"
					Create-Shortcut -Path $path -Target $app.Target -IconPath $app.IconPath -Arguments $app.arguments -WorkingDir $app.workingDir
				}
			}
		}
		If ($app.GroupName -eq "Application 1") {
			If ((Get-ADGroupMember -Identity $app.GroupName -Recursive | Select -ExpandProperty Name) -contains $user.Name) {
				Write-Host "Creating Shortcut for " $app.Name -Foregroundcolor Yellow
				Copy-Item \\SERVERFS1\Shortcuts$\Application1.lnk $userPath
			}
		}
	}
}

Tagged with: , ,
Posted in Powershell

Provision New Virtual Hard Disks

When deploying a new hard drive or drives on a virtual machine, there are several steps to enable this storage in the OS of the VM. For Windows, these steps can all be accomplished via PowerShell. Granted, this is not a huge time saver on its own, but used in conjunction with a full configuration script, it’s one less thing you have to do manually to get your VM up and running.


# Detect, initialize and format unitialized drives
# -- Get a list of disks from the OS
$disks = get-disk
# -- Loop through each disk and do stuff
foreach ($disk in $disks) {
	# -- Get the size of the disk to use later
	$maxsize = $disk.Size
	# -- If the disk is offline and is cleared, set it online and do other stuff
	if ($disk.operationalstatus -eq &quot;Offline&quot; -and $disk.PartitionStyle -eq &quot;RAW&quot;) {
		set-disk $disk.Number -IsOffline $False
		# -- If the disk is less than two terabytes, initialize as an MBR partition and format NTFS, if not, initialize as GPT and format NTFS.
		if ($maxsize -lt 2TB) {
			Initialize-Disk -Number $disk.Number -PartitionStyle MBR
			New-Partition -DiskNumber $disk.Number -UseMaximumSize -AssignDriveLetter | Format-Volume -FileSystem NTFS -confirm:$false
		}
		Else {
			Initialize-Disk -Number $disk.Number -PartitionStyle GPT
			New-Partition -DiskNumber $disk.Number -UseMaximumSize -AssignDriveLetter | Format-Volume -FileSystem NTFS -confirm:$false
		}
	}
}

Tagged with: ,
Posted in Powershell

Change “Upgrade at Power Cycle” to enable automatic VMTools upgrades

VM Maintenance is a chore for most IT shops, but there are ways to make things easier! If your policies allow, you can set all VMs to upgrade VMTools on reboot. Since we usually reboot most VMs on a weekly or monthly basis, this makes it easy to allow the Tools to update whenever there is an ESXi update. This script will set the flag for all VMs in a datacenter, so keep that in mind.

<#
Title: 		Sets the Tools Upgrade Policy to "Upgrade at Power Cycle"
Author: 	Michael Kenning (mjkenning@gmail.com)
Version: 	0.2 (beta)
Usage: 		
Created:	08 SEP 2015
Updated: 	09 SEP 2015
NOTE:		
#>

### VARIABLES ###
$cluster = 'CLUSTERNAME'
$vcserver = "VCNAME"
### END VARIABLES ###

Connect-VIserver $vcserver

$spec = New-Object VMware.Vim.VirtualMachineConfigSpec 
$spec.tools = New-Object VMware.Vim.ToolsConfigInfo 
$spec.tools.toolsUpgradePolicy = "upgradeAtPowerCycle"
Foreach($vmview in get-view -ViewType virtualmachine -SearchRoot (get-Cluster $cluster).id -Filter @{'Config.Tools.ToolsUpgradePolicy' = 'manual' } ) {
	$vmview.ReconfigVM_task($spec)
}
Tagged with: ,
Posted in PowerCLI

Programmatically add printers to a Windows server

We install servers for organizations that sometimes have hundreds of printers. Gathering information on those printers and then creating new ones on the new print server can be tedious at best. To get a list of printers, you have to run the following script on the local print server. If you have more than one print server, you will need to run this on each one and compile the information manually.

<#
Purpose: To get a list of printers, printer ports, share name, comments, location and driver names from a Windows server.
Author: Michael Kenning (mjkenning@gmail.com)
Date: 23 OCT 2014
Version: 1.0 (release)

Usage: ./getprinters.ps1 ./FILENAME.CSV
#>

$printserver = "localhost"
Get-WMIObject -class Win32_Printer -computer $printserver | Select Name,DriverName,PortName,ShareName,Comment,Location | Export-CSV -path '.\printers.csv'

When you’ve compiled your information from this script into a CSV file (you’ll need to add the IP Address), you are ready to run the script to add the new printers. But first, you will need to verify that the driver names are the same!

<#
Purpose: To create printers, printer ports, and add drivers to a Windows 2012/2012R2 server.
Author: Michael Kenning (mjkenning@gmail.com)
Date: 23 JUL 2015
Version: 1.32 (release)

Usage: ./addprinters.ps1 ./FILENAME.CSV
#>

param (
	[Parameter(Mandatory=$true,Position=0)]
	[ValidateNotNullOrEmpty()]
	[String]
	$path
)

$printers_csv = Import-CSV $path
$drivers = get-printerdriver | select -expand name
$ports = get-printerport | select -expand name
$instPrinters = get-printer | select -expand name


foreach ($printer in $printers_csv) {
	
	$portInstalled = $null
	$driverInstalled = $null
	$printerInstalled = $null
	$printerName = $printer.Name
	$driverName = $printer.driverName
	$portName = $printer.portName
	$shareName = $printer.shareName
	$location = $printer.location
	$comment = $printer.comment
	$ipAddress = $printer.ipAddress

	write-host "Checking" $printerName "..."
	foreach ($instPrinter in $instPrinters) {
		if ($instPrinter -eq $printerName) {
			write-host $printerName "already installed"
			$printerInstalled = $true
			break
		}
	}

	foreach ($driver in $drivers) {
		if ($driverName	-eq $driver) {
			$driverInstalled = $true
			write-host $driver "already installed"
			break
		}
		else {$driverInstalled = $false}

	}
	if ($driverInstalled -eq $false) {
		Add-PrinterDriver -Name $driverName -Confirm:$false
		write-host "Driver" $driverName "installed."
		$driverInstalled = 1
	}
	foreach ($port in $ports) {
		if ($portName -eq $port) {
			$portInstalled = $true
			write-host $port "is already created."
			break
		}
		else {$portInstalled = $false}
	}
	if ($portInstalled -ne $true) {
		Add-PrinterPort -Name $portName -PrinterHostAddress $ipAddress -Confirm:$false
		write-host "Portname" $portName "created."
		$portInstalled = $true
	}

	if (($driverInstalled) -and ($portInstalled) -and ($printerInstalled -ne $true)) {
		Add-Printer -Name $printerName -DriverName $driverName -PortName $portName -Comment $comment -Location $location -Shared -Sharename $shareName -Published -Confirm:$false
	}
}
Tagged with: , ,
Posted in Powershell

Stop all Exchange Services

When migrating from Exchange 2007/2010 to Exchange 2013, one of the steps is to test the environment with the old server out of the mix. To do this, you can simply unplug the server (virtual or physical) from the network and see what happens. However, there are times when the server is not physically accessible to you and you need to perform this test anyway. With this script, you can stop all Exchange services and test your environment (mail flow, client connectivity, etc).

<#
Purpose:	Script to stop all exchange services
Author:		Michael Kenning (mjkenning@gmail.com)
Version:	0.2 (beta) (9 JUN 2015)
Usage: 		./stopservice.ps1
#>

$Services = Get-Service | where {$_.Name -like "MSExchange*"}

foreach ($service in $services) {
	if ($service.Status -eq "Running") {
		Stop-Service -InputObj $service
	}
}
Tagged with: , ,
Posted in Powershell

Change vRAM for multiple VMs in vCenter

Video RAM is one of those funny things you don’t think about too often, but if you need more than the default 4MB, it can be a pain to change. VM vRAM size affects the size of the console window among other things. This console size is important for VDI, using remote access tools like WebEx and GotoMeeting and for viewing the console in the VMware Workstation interface.

So, when I install a new set of VMs, I usually set the vRAM size to a larger number to accommodate at least 1920 x 1080 (the size of my main monitor) which is about 256MB.

I found this great function by Al Renouf and wrote a script to apply it to my VMs. Needless to say, you will need PowerCLI to make this work. Also, as the notes in the script caution, this will change all VMs in a given datacenter, except the vCenter server and one other specified server. USE WITH CAUTION! I usually run this after I deployed a fresh Datacenter, so it’s not a problem.

<#
Title: 		Set the video RAM size on all VMs in a Datacenter
Author: 	Michael Kenning (mjkenning@gmail.com) -- Function source: http://virtu-al.net
Version: 	1.12 (release)
Updated: 	6 MAY 2015

Usage: 		.\set-VMvRAM.ps1 [VCENTER IP or FQDN] [vRAM size in MB as int]
NOTES:		Change variables as needed. WARNING, this script will power off multiple VMs. USE WITH CAUTION!
#>

param (
	[Parameter(Mandatory=$true,Position=0)]
	[ValidateNotNullOrEmpty()]
	[String]
	$vCenter,
	[Parameter(Mandatory=$true,Position=1)]
	[ValidateNotNullOrEmpty()]
	[int]
	$vRAMsize
)

### VARIABLES ###
$noUpdate = "DCVM1"
$dataCenter = "DATACENTER"
### END VARIABLES ###

### BEGIN FUNCTIONS ###
function Set-VMVideoMemory {
<# .SYNOPSIS   Changes the video memory of a VM
	.DESCRIPTION   This function changes the video memory of a VM 
	.NOTES   Source: http://virtu-al.net   Author: Alan Renouf   Version: 1.1 
	.PARAMETER VM   Specify the virtual machine 
	.PARAMETER MemoryMB   Specify the memory size in MB 
	.EXAMPLE   PS> Get-VM VM1 | Set-VMVideoMemory -MemoryMB 4 -AutoDetect $false
#>
 
  Param (
	[parameter(valuefrompipeline = $true, mandatory = $true, HelpMessage = "Enter a vm entity")]
	[VMware.VimAutomation.ViCore.Impl.V1.Inventory.VirtualMachineImpl]$VM,
	[int64]$MemoryMB,
	[bool]$AutoDetect,
	[int]$NumDisplays
	)
 
  Process {
   $VM | Foreach {
      $VideoAdapter = $_.ExtensionData.Config.Hardware.Device | Where {$_.GetType().Name -eq "VirtualMachineVideoCard"}
      $spec = New-Object VMware.Vim.VirtualMachineConfigSpec
      $Config = New-Object VMware.Vim.VirtualDeviceConfigSpec
      $Config.device = $VideoAdapter
      If ($MemoryMB) {
         $Config.device.videoRamSizeInKB = $MemoryMB * 1KB
      }
      If ($AutoDetect) {
         $Config.device.useAutoDetect = $true
      } Else {
         $Config.device.useAutoDetect = $false
      }
      Switch ($NumDisplays) {
         1{ $Config.device.numDisplays = 1}
         2{ $Config.device.numDisplays = 2}
         3{ $Config.device.numDisplays = 3}
         4{ $Config.device.numDisplays = 4}
         Default {}
      }
      $Config.operation = "edit"
      $spec.deviceChange += $Config
      $VMView = $_ | Get-View
      Write-Host "Setting Video Display for $($_)"
      $VMView.ReconfigVM($spec)
   }
  }
}

### END FUNCTIONS ###

# -- Connect to the vCenter server
Connect-VIServer -Server $vCenter

# -- Get a list of VMs from the specified Datacenter
$vms = Get-Datacenter $dataCenter | Get-VM

# -- Loop through the list of VMs and do stuff
Foreach ($vm in $vms) {
	$vmname = $vm.Name
	$vmStatus1 = $null
	
	# -- Don't update the vCenter server!
	if ($vmname -ne $vCenter) {
		
		# -- Don't update the specified server (i.e. domain controller)
		if ($vmname -ne $noUpdate) {
			
			# -- Check to see if the VM is powered On
			$vmStatus = Get-VM $vmname | ForEach{$_.PowerState}
			
			# -- If it's powered on, shut it down gracefully
			if ($vmStatus -eq "PoweredOn") {
				$vmStatus1 = $vmStatus
				Shutdown-VMGuest $vmname -Confirm:$false
				do {
					$vmStatus = Get-VM $vmname | ForEach{$_.PowerState}
					write-host "Checking power status for $vm. Please Wait."
					sleep 5
				}
			until ($vmstatus -eq "PoweredOff")
			}
			
			# -- Change the vRAM size to the specified amount
			Get-VM $vm | Set-VMVideoMemory -MemoryMB $vRAMsize -AutoDetect $false
			
			# -- If the VM was powered on when we started, power it back on
			If ($vmStatus1 -eq "PoweredOn") {
				start-vm $vmname
			}
		}
	}
}
Tagged with: ,
Posted in PowerCLI

Get the status for a scheduled task from a list of computers

One of our standards is to have customer computers reboot on a scheduled basis (for windows updates and for system stability for highly used server, i.e. RDS servers). However, we also need them to do certain things when they reboot, like stop certain services, clean out print queue folders, etc. We’ve setup a scheduled task to do this and one of the problems with this is we sometimes lose track of what server is set to do what, when.

Powershell to the rescue!

This script will take a list of computers (one name per line) and get the Scheduled Task Info for each one.

<#
Title: 		Get the Scheduled Task Status for a list of remote computers
Author: 	Michael Kenning (mjkenning@gmail.com)
Version: 	1.0 (release)
Usage: 		.\getRemoteTasks.ps1 [PATH TO LIST OF SERVERS]

Updated: 	26 MAR 2015
#>

param (
	[Parameter(Mandatory=$true,Position=0)]
	[ValidateNotNullOrEmpty()]
	[String]
	$path
)

# -- Import list of servers
$computers = Get-Content $path

# -- Set the task name to seach for
$taskName = "Reboot"

# -- Loop through the list of computers
Foreach ($computer in $computers) {
	# -- Create a new PowerShell Session to the specified computer
	$session = New-PSSession -ComputerName $computer
	# -- Invoke a remote command with a parameter to pass the Task Name to the command
	Invoke-Command -Session $session -ScriptBlock {
		param($taskName)
		Get-ScheduledTask -TaskName $taskName | Get-ScheduledTaskInfo
	} -Args $taskName
}
Tagged with: ,
Posted in Powershell