Category: Series

Logon Scripts in Powershell – Part4: Trigger ConfigMgr client actions

With the ConfigMgr agent in place on your client’s you probably want to force it to start some client actions on logon to speed up deployment of new applications.

This function will work with the ConfigMgr 2007 (on x86) and 2012 agent. (The reason for not working on a Win7 x64 and ConfigMgr 2007 is the lack of X64 support).

Function Run-ConfigMgrActions {
	<#
	.SYNOPSIS 
		Trigger ConfigMgr client actions
	#>
	PARAM (
		[string] $actionFilter1 = "Application Global Evaluation Task*",
		[string] $actionFilter2 = "Request ? Evaluate*"
	)

	Write-Verbose "Run-ConfigMgrActions: Start actions with filter '$($actionFilter1)' or '$($actionFilter2)'"
	TRY {
		(New-Object -ComObject CPApplet.cpAppletMgr).GetClientActions() | Where-Object {$_.Name -like $actionFilter1 -or $_.Name -like $actionFilter2} | Sort-Object Name | ForEach-Object {
			Write-Verbose "Run-ConfigMgrActions: Starting ConfigMgr action: $($_.Name)"
			$_.PerformAction()
		}
	}
	CATCH {
		Write-Verbose "Run-ConfigMgrActions: Can't find and/or trigger ConfigMgr Agent"
	}
}

Logon Scripts in Powershell – Part3: Storing and retrieving shares to map to/from AD

To keep you logon scripts static one way of storing information on who sould get what mapped is to use the Active Directory.

First of, talk to the AD-guys and “reserve” 3 attributes on groups (or extend the schema and add your own attributes). You need attributes for:
– Share Path
– Display Name
– Type of mapping

In the example I will use:
– displayName = Share Path
– displayNamePrintable = Display Name
– extensionAttribute3 = Type of mapping (I will use a 1 for “Network Location”, 0 for not in use and letters to map to a specific drive)

Then, populate the groups with information:

Import-Module ActiveDirectory

Get-Adgroup "MyGroupName" | Set-ADGroup -Replace @{
	DisplayName="\\SERVER\PathToMapAsNetworkLocation";
	DisplayNamePrintable="Some Description";
	extensionAttribute3=1
}
Get-Adgroup "SomeOtherGroupName" | Set-ADGroup -Replace @{
	DisplayName="\\SERVER\PathToMapAsDrive";
	DisplayNamePrintable="Some Description";
	extensionAttribute3=X
}

Get-Adgroup "ProjXDocs" | Set-ADGroup -Replace @{
	DisplayName="\\SERVER\Projects\ProjectXdocuments";
	DisplayNamePrintable="ProjectX Documents";
	extensionAttribute3=1
}

Then, retrieve the groups for the current user with a recursive LDAP-query.

Function Get-SharesToMap {
	<#
	.SYNOPSIS
		Read groups in AD for a user, then collect information to use when mapping disk
	#>
	PARAM (
		$UserDN = (Get-UserDN)
	)

	$mappedShares = @()
	$ldapFilter = "(&(member:1.2.840.113556.1.4.1941:=$($UserDN))(displayName=*)(extensionAttribute3=*)(!extensionAttribute3=0))"

	Run-LdapQuery -ldapFilter $ldapFilter | ForEach-Object {
		Write-Verbose "Get-SharesToMap: $($ssabMappedSharePath) - Mapping: $($ssabMappedShare)"
		$shareInfo = New-Object -TypeName System.Object
		$shareInfo | add-Member -memberType NoteProperty -name Group -Value $_.Properties.Item("cn")
		$shareInfo | add-Member -memberType NoteProperty -name Path -Value $_.Properties.Item('displayName')
		$shareInfo | add-Member -memberType NoteProperty -name DisplayName -Value $_.Properties.Item('displayNamePrintable')
		$shareInfo | add-Member -memberType NoteProperty -name MappedShare -Value $_.Properties.Item('extensionAttribute3')
		
		$mappedShares += $shareInfo
	}

	Return $mappedShares
}

Now you have a list of all shares to take care of…

Get-SharesToMap | Sort-Object Path | ForEach-Object {
	$Path        = $_.Path
	$MappedShare = $_.MappedShare
	$DisplayName = $_.DisplayName

	# Map UNC path once
	If ($lastMappedPath -ne $Path) {
		Switch -regex ($MappedShare) {
			0        { Write-Verbose "Loginscript:Main: Do not map UNC path $($UncPath)" }
			1        { Create-NethoodShortcut -LinkTarget $Path -LinkName $DisplayName -Description $DisplayName }
			"[A-Z]"  { Map-UncPathToDrive -UncPath $Path -DriveLetter $MappedShare }
			default  { Write-Verbose "Loginscript:Main: $($MappedShare) Do not map path $($Path)" }
		}
		$lastMappedPath = $Path
	}
}

With this in place, no one needs to edit the logonscript to change mappings. 🙂

Sidenote 1:
To speed up the LDAP-queries add indexes on the attributes.
Start MMC
Add the snapin “Active Directory Schema”
Search for the attribute(s)
On the properties-page, check the box “Index this attribute”

Sidenote 2:
To make it easy to search the attributes, check the box “Ambigous Name Resolution (ANR)” in the properties page for the attribute
With ANR enabled you can do a LDAP-query like:
(ANR=SomeNameOfTheShare)
So, if someone wants to be member of the group for the share “ProjectX” just query like (ANR=ProjectX) and the group for that share will show up.

Logon Scripts in Powershell – Part2: LDAP-Queries

Next up… running LDAP in pure Powershell.
It would of course be nice to use ActiveDirectory cmdlets in the logonscript… but you probably don’t deploy those to all machines.

So, running LDAP queries can be done using a ADSI Searcher. One drawback with using this techniqe is the lack of site awareness… so if you have a larger network (or user out on low performing WAN-links) you need to take care of this “manually”.

The easy way to do this is by using the environment variable LogonServer… and if you have your AD-Sites set up right, this should be the closest server.

Here is how you can run LDAP-Queries on the LogonServer:

Function Get-AdLogonServer {
	<#
	.SYNOPSIS 
		Returns the AD logon server
	#>

	Return [string] (Get-ChildItem ENV:LogonServer).Value.ToUpper().Replace("\", "")
}

Function Run-LdapQuery {
	<#
	.SYNOPSIS
		Run a LDAP-query on logonserver
	#>
	PARAM (
		[string] $ldapFilter,
		[int] $pageSize = 512,
		[switch] $findOne
	)

	Write-Verbose "Run-LdapQuery: Running: LDAP://$(Get-AdLogonServer)/$($ldapFilter)"

	$AdSearch=([ADSISearcher]([ADSI]("LDAP://$(Get-AdLogonServer)")))
	$AdSearch.Filter = $ldapFilter
	if ($findOne.IsPresent) {
		$AdSearch.pagesize = 1
		Return $AdSearch.FindOne()
	} else {
		$AdSearch.pagesize = $pageSize
		Return $AdSearch.FindAll()
	}
}

And a quick sample to get the current users DN-string:

Function Get-UserDN {
	<#
	.SYNOPSIS
		Return a DN-string for a user (default to current user)
	#>
	PARAM (
		[string] $UserName = (Get-UserName)
	)

	Run-LdapQuery -ldapFilter "(sAMAccountName=$($UserName))" -findOne | ForEach-Object {
		$userDN = $_.Properties.Item("DistinguishedName")
	}

	Return $userDN
}

Logon Scripts in Powershell – Part1: Network Locations

Long time no blog…

Since I have written the logonscript we use (of course in Powershell).. why not share some snippets. 🙂

Starting out with the most common ones that “everyone” use. Map shares on file servers…
I prefer to create Network Shortcuts instead of using mapped drives.
Why?
Take a “standard” service desk call:
User: Hey, I want to access X:
Operator: Ok… where does it point?
User: I don’t know, but John Doe has a X: and I need it.
* Operator search for John Doe, checks all groups and how they are configured *
Operator: Ok… found it, log off and on and you are good to go.
User: But what will happen to my X: that I have today?

So, if you use drive letters you are bound to a specific number of letters and when you use X: for multiple of shares you will sooner or later end up in big mess…

Anyway, I will start of by showing you how to create the Network Locations.

(I will continue with another part on how the AD can be used to make this a lot easier than writing the shares, names and groups directly in the script)

So, here we go… a few good-to-have functions:

Function Get-NethoodPath {
	<#
	.SYNOPSIS 
		Returns path to "Network Locations"
		C:\Users\riro\AppData\Roaming\Microsoft\Windows\Network Shortcuts
	#>

	$folderNETHOOD = 0x13
	Return ((New-Object -com Shell.Application).Namespace($folderNETHOOD)).Self.Path
}

Function Delete-NethoodShortcut {
	<#
	.SYNOPSIS 
		Deletes a shortcut under Network Locations
	#>
	PARAM (
		[string] $LinkName
	)

	$NetLocLocalPath = Join-Path (Get-NethoodPath) $LinkName

	If (Test-Path $NetLocLocalPath) {
		Write-Verbose "Delete-NethoodShortcut: Removing: $($NetLocLocalPath)"
		Remove-Item $NetLocLocalPath -Force -Recurse
	}
}

Function Create-NethoodShortcut {
	<#
	.SYNOPSIS 
		Creates a shortcut under Network Locations
	#>
	PARAM (
		[string] $LinkTarget,
		[string] $LinkName,
		[string] $Description = "Folder on the network",
		$IconLocation = "%SystemRoot%\system32\SHELL32.DLL",
		$IconIndex = 9
	)
	
	Write-Verbose "Create-NethoodShortcut: Nethood shortcut: '$($LinkName)' targeting '$($LinkTarget)'"

	$NetLocLocalPath = Join-Path (Get-NethoodPath) $LinkName

	If (Test-Path "$($NetLocLocalPath).lnk") {
		Write-Verbose "Create-NethoodShortcut: Removing old link: $($NetLocLocalPath).lnk"
		Remove-Item "$($NetLocLocalPath).lnk" -Force
	}

	New-Item $NetLocLocalPath -Type directory -Force | Out-Null
	attrib "$NetLocLocalPath" +R

	if ( !(Test-Path "$NetLocLocalPath\Desktop.ini" -pathType leaf) ) {
		$oFile = new-Item "$NetLocLocalPath\Desktop.ini" -type file -force
		add-Content $oFile "[.ShellClassInfo]"
		add-Content $oFile "CLSID2={0AFACED1-E828-11D1-9187-B532F1E9575D}"
		add-Content $oFile "Flags=2"
		attrib "$NetLocLocalPath\Desktop.ini" +H +S -A
	}

	$wshShell = new-object -comobject wscript.shell
	$shortCut = $wshShell.CreateShortcut($NetLocLocalPath + "\target.lnk")
	$shortCut.TargetPath = $LinkTarget
	$shortCut.IconLocation = "$($IconLocation), $($IconIndex)"
	$shortCut.Description = $Description
	$shortCut.Save()
}

OK.. so you want to map diskdrives instead? This can also be done…

Function Test-DriveLetterInUse {
	<#
	.SYNOPSIS
		Return True if a disk is in use
		Test-Path doesnt work to good on UNC-mapped disks
	#>
	PARAM (
		[char] $DriveLetter
	)
	If ((Get-PSDrive -PSProvider FileSystem | Where {$_.Root -eq "$($DriveLetter):\"}) -eq $null) {
		Return $false
	} else {
		Return $true
	}
}

Function Remove-MappedDrive {
	PARAM (
		[char] $DriveLetter
	)

	If ( (Test-DriveLetterInUse -DriveLetter $DriveLetter) -eq $true ) {
		$wshNetwork = New-Object -com WScript.Network
		Write-Verbose "Remove-MappedDrive: $($DriveLetter): in use, removing drivemap"
		$wshNetwork.RemoveNetworkDrive("$($DriveLetter):")
	}
}

Function Map-UncPathToDrive {
	PARAM (
		[string] $UncPath,
		[char] $DriveLetter,
		[bool] $OverwriteExisting = $true
	)
	$wshNetwork = New-Object -com WScript.Network

	If ( (Test-DriveLetterInUse -DriveLetter $DriveLetter) -eq $true -and $OverwriteExisting -eq $true) {
		Write-Verbose "Map-UncPathToDrive: $($DriveLetter): in use, removing current drivemap"
		$wshNetwork.RemoveNetworkDrive("$($DriveLetter):")
	}
	
	If ((Test-DriveLetterInUse -DriveLetter $DriveLetter) -eq $false) {
		Write-Verbose "Map-UncPathToDrive: Mapping drive $($DriveLetter): to $($UncPath)"

		$wshNetwork.MapNetworkDrive("$($DriveLetter):", "$($UncPath)")
	} else {
		Write-Verbose "Map-UncPathToDrive: $($DriveLetter): allready in use"
	}
}