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

PoSH: Send-Prowl

So I’ve been playing around with Prowl on my iPhone and since the Prowl API is really simple to use why now try some Powershell :)

Here is a function that i wrote:

Function Send-Prowl {
	PARAM (
		[string] [Parameter(Mandatory=$True) ][ValidateLength(1, 1024)]  $Event,
		[string] [Parameter(Mandatory=$False)][ValidateLength(0, 10000)] $Description = "",
		[string] [Parameter(Mandatory=$False)][ValidateLength(1, 256)]   $ApplicationName = "PoSH: Send-Prowl",
		[int]    [Parameter(Mandatory=$False)][ValidateRange(-2, 2)]     $Priority = 0,
		[string] [Parameter(Mandatory=$False)][ValidateLength(0, 512)]   $url,
		[string] [Parameter(Mandatory=$True) ][ValidateScript({ $_.Length -ge 40})] $apiKey
	)

	# URL-encode some strings
	[Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
	$Event = [web.httputility]::urlencode($Event.Trim())
	$Description = [web.httputility]::urlencode($Description.Trim())
	$ApplicationName = [web.httputility]::urlencode($ApplicationName.Trim())
	$url = [web.httputility]::urlencode($url.Trim())

	# Compose the complete URL
    $apiBaseUrl = "https://prowl.weks.net/publicapi/add"
	$ProwlUrl = "$($apiBaseUrl)?apikey=$($apiKey)&application=$($ApplicationName)&event=$($Event)&Description=$($Description)&priority=$($Priority)&url=$($url)"	
	Write-Verbose "Complete URL: $($ProwlUrl)"

	# Try to send message
    TRY {
	   $webReturn = ([String] (New-Object Net.WebClient).DownloadString($ProwlUrl))
    }
    CATCH {
        Write-Verbose "Error sending Prowl Message: $($error[0])"
        Return $false
    }

    # Output what comes back from the API
	Write-Verbose $webReturn

    If ( (1$webReturn).prowl.success.code -eq 200 ) {
        Write-Verbose "Prowl message sent OK"
        Return $true
    } else {        
        Write-Verbose "Error sending Prowl Message: $((1$webReturn).prowl.error.code) - $((1$webReturn).prowl.error.innerXml)"
        Return $false
    }
}

Just download the Prowl APP (from iTunes Store), register an API-key and you are good to go.

Some examples:

Send-Prowl -apiKey "1234567890123456789012345678901234567890" -Event "Sent from Powershell"
Send-Prowl -apiKey "1234567890123456789012345678901234567890" -Event "Server room on fire!!!" -Priority 2 -Description "Call 112 and get you ass here with an fire extinguisher" -url "tel:112"
Send-Prowl -apiKey "1234567890123456789012345678901234567890" -Event "New Alert" -Priority 1 -Description "Some info..." -url "https://opsmgr.company.com/"

Yes, I knwo that there are other ways to do it… but most of them requires that you load a DLL or use an external EXE-file.

Anyway kudos to Janssen Jones article that inspired me…

Update ConfigMgr Site Description

I did an install of a secondary site and accidently put in the wrong description…

Anyway, easy to fix with some SQL commands.

If you use this you can see the current sitenames and descriptions (change CM_XYZ to your database name):

SELECT SiteCode, SiteName, SiteServerName
FROM CM_XYZ..SC_SiteDefinition

And here is the way to update it (Change to you database name and the site code of the site you want to change)

UPDATE SC_SiteDefinition
SET SiteName = 'My Site Description'
WHERE CM_XYZ.SiteCode = 'ABC'

FYI: When running this you will se “Updated X rows… Updated Y rows…” and so on.

Set ConfigMgr 2012 Working Hours with Powershell

The “working hours” defined in ConfigMgr 2012 Software Center on the client is a bit off for our environment…

Did a bit of digging and found some examples in VBScript, but I like Powershell… :)

Function Set-CmClientBusinessHours {
	PARAM (
		[int] $StarTime = 7,
		[int] $EndTime = 18,
		[switch] $Sunday,
		[switch] $Monday,
		[switch] $Tuesday,
		[switch] $Wednesday,
		[switch] $Thursday,
		[switch] $Friday,
		[switch] $Saturday
	)
	
	$WorkingDays += ([int]$Sunday.IsPresent    * 1)
	$WorkingDays += ([int]$Monday.IsPresent    * 2)
	$WorkingDays += ([int]$Tuesday.IsPresent   * 4)
	$WorkingDays += ([int]$Wednesday.IsPresent * 8)
	$WorkingDays += ([int]$Thursday.IsPresent  * 16)
	$WorkingDays += ([int]$Friday.IsPresent    * 32)
	$WorkingDays += ([int]$Saturday.IsPresent  * 64)

	TRY {
		$cliUX = [WmiClass]"\\.\ROOT\CCM\ClientSDK:CCM_ClientUXSettings"
		$Params = $cliUX.PSBase.GetMethodParameters("SetBusinessHours")
		$Params.StartTime = $StarTime
		$Params.EndTime = $EndTime
		$Params.WorkingDays = $WorkingDays
		$cliUX.PSBase.InvokeMethod("SetBusinessHours", $Params, $null) | Out-Null
		Write-Verbose "Business Hours set to $($WorkingDays), between $($StarTime) - $($EndTime)"
		Return $true
	}
	CATCH {
		Write-Verbose "Can't set Business Hours"
		Return $false
	}
}

And this is how you use it (setting work days Mon-Fri between 08:00 and 17:00)

Set-CmClientBusinessHours -Monday -Tuesday -Wednesday -Thursday -Friday -StarTime 8 -EndTime 17

There are a few other methods that you can use with (almost) the same code…

# First connect to ClientUX
$cliUX = [WmiClass]"\\.\ROOT\CCM\ClientSDK:CCM_ClientUXSettings"

# Get a list of all methods
$cliUX.PSBase.methods | Format-Table Name

# Get list of parameters to a method
$cliUX.PSBase.GetMethodParameters("SetSuppressComputerActivityInPresentationMode") | Get-Member
$cliUX.PSBase.GetMethodParameters("SetAutoInstallRequiredSoftwaretoNonBusinessHours") | Get-Member
$cliUX.PSBase.GetMethodParameters("SetBusinessHours") | Get-Member

Keep a list of user/computer groups in registry

In the migration to ConfigMgr 2012 we are aiming at just keeping one collection for software distribution and instead rely on global conditions.
We do want to keep the option to add a user to a group and with that do a automagic distribution.

So, the examples below are snippets from the logonscript.

Here is the short version:
- Recursive search for group membership for user
- If group-list changed, update the reg key
(Then do the same for computer groups)
- If there are any changes to groups, trigger the ConfigMgr agent

- Create a global condition in ConfigMgr
- Use the condition like “If MyGlobalCondition contains SwDistGroupName”

$userDN = "CN=MyUser,OU=Users,DC=snowland,DC=se"
$computerDN = "CN=CyComputer,OU=Computers,DC=snowland,DC=se"

Function Get-DistGroups {
	PARAM (
		$ObjectDN,
		$nameFilter = "SwDist_*"
	)

	$distGroups = @()
	$AdSearch=([ADSISearcher]"LDAP://")
	$AdSearch.pagesize=512
	$AdSearch.Filter="(&(member:1.2.840.113556.1.4.1941:=$($ObjectDN))(name=$($nameFilter)))"
	$AdSearch.findAll() | ForEach-Object {
		Write-Verbose "Found group: $($_.Properties.Item('Name'))"
		$distGroups += $_.Properties.Item("Name")
	}

	Return $distGroups
}

# Standard setting
$runConfigMgrActions = $false

# Store DIST groups in registry
Write-Host "Reading User DIST groups"
$userDistGroups = Get-DistGroups -ObjectDN $userDN | Sort-Object
$CurrKeys = Read-RegKey -Key "HKCU:\Software\SSAB\SoftwareDistribution" -Name "UserGroups" | Sort-Object
If ((Compare-Object -ReferenceObject $CurrKeys -DifferenceObject $userDistGroups) -ne $null) {
	Write-Host "Found added/removed user groups, writing new list to registry"
	New-ItemProperty "HKCU:\Software\snowland\SoftwareDistribution" -Name "UserGroups" -Value $userDistGroups -PropertyType "MultiString" -Force | Out-Null

	$runConfigMgrActions = $true
} else {
	Write-Verbose "No changes in user groups"
}

Write-Host "Reading computer groups"
$computerDistGroups = Get-DistGroups -ObjectDN $computerDN | Sort-Object
$CurrKeys = Read-RegKey -Key "HKLM:\Software\SSAB\SoftwareDistribution" -Name "ComputerGroups" | Sort-Object

If ((Compare-Object -ReferenceObject $CurrKeys -DifferenceObject $computerDistGroups) -ne $null) {
	Write-Host "Found added/removed computer groups, writing new list to registry"
	New-ItemProperty "HKLM:\Software\snowland\SoftwareDistribution" -Name "ComputerGroups" -Value $computerDistGroups -PropertyType "MultiString" -Force | Out-Null
	$runConfigMgrActions = $true
} else {
	Write-Verbose "No changes in computer groups"
}

# Trigger ConfigMgr client actions if groups have changed
If ($runConfigMgrActions -eq $true) {
	Write-Verbose "Changes to DIST groups, running ConfigMgr Actions"
	(New-Object -ComObject CPApplet.cpAppletMgr).GetClientActions() | Where-Object {$_.Name -like "Application Global Evaluation Task*" -or $_.Name -like "Request & Evaluate*"} | Sort-Object Name | ForEach-Object {
		Write-Host "Starting ConfigMgr action: $($_.Name)"
		$_.PerformAction()
	}
}

(To do this you need to set a security GPO to the HKLM-key so that your users are allowed to write there.)

MDT and Webservices – Small bug

Long time no blog…

Working on a move to CM2012 from CM2007, and with that we are trying to move to Webservices instead of client side VBScript.

I found a small bug in MDT (Looked at version 2010U1 and 2012U1 and it’s the same)

You will see this bug if you have a webservice call from CustomSettings where a parameter value have a space (or any other “strange” char).
Since the script ZTIDataAccess.vbs doesn’t URL-encode the parameters some of them will get messed up…

Heres how to fix it:
Search for the text “Handle it appropriately” in ZTIDataAccess.vbs and you can replace the code with this bugfixed version:

' Handle it appropriately
If IsObject(tmpValue) then
	oLogging.CreateEntry "Only the first " & sElement & " value will be used in the web service call.", LogTypeInfo
	tmpArray = tmpValue.Keys
	If UCase(sMethod) = "REST" then
		sEnvelope = sEnvelope & sColumn & " eq '" & tmpArray(0) & "' and "
	Else
' riro Buggfix
' Org: sEnvelope = sEnvelope & sColumn & "=" & tmpArray(0) & "&"
		sEnvelope = sEnvelope & sColumn & "=" & Escape(tmpArray(0)) & "&"
	End if
Else
	If UCase(sMethod) = "REST" then
		sEnvelope = sEnvelope & sColumn & " eq '" & tmpValue & "' and "
	Else
' riro Buggfix
' Org: sEnvelope = sEnvelope & sColumn & "=" & tmpValue & "&"
		sEnvelope = sEnvelope & sColumn & "=" & Escape(tmpValue) & "&"
	End If
End If

As you can see I have just added Escape() to the values. :)

Get ConfigMgr Collection rules

I’m in the process of installing Forefront Endpoint Protection and wanted to look at some of the collection queries that was created… but with the ConfigMgr console you cant view them…

So Powershell it is.

Did a function that you can use on any collection (with subcollections) to view the WQL.

Import-Module SCCM\SCCM-Functions -Force
Function Get-CollectionRules {
	PARAM (
		$parentCollection,
		$spacer,
		$sccm
	)

	$subCollections = Get-SCCMSubCollections -SccmServer $sccm -CollectionID $parentCollection
	
	if ($subCollections -ne $null) {
		$subCollections | ForEach-Object {
			$collection = Get-SCCMCollection -Filter "CollectionID='$($_.subCollectionID)'" -SccmServer $sccm
			Write-Host "$($spacer) Name: " -ForegroundColor Yellow -NoNewline
			Write-Host "$($collection.CollectionID) - $($collection.Name)"

			$collectionRule = (Get-SCCMCollectionRules -SccmServer ( Connect-SCCMServer ) -CollectionID $collection.CollectionID)
			if ($collectionRule -ne $null) {
				Write-Host "$($spacer)Limit: " -ForegroundColor Yellow -NoNewline
				if ($collectionRule.LimitToCollectionID.Length -gt 0) {
					Write-Host "$($collectionRule.LimitToCollectionID)" -ForegroundColor White
				} else {
					Write-Host "<No limit to collection>" -ForegroundColor Gray
				}

				Write-Host "$($spacer)  WQL: " -ForegroundColor Yellow -NoNewline
				Write-Host "$($collectionRule.QueryExpression)"
			} else {
				Write-Host "$($spacer)<no rule present on collection>" -ForegroundColor Gray
			}
			Write-Host ""

			Get-CollectionRules -parentCollection $_.subCollectionID -spacer "   $($spacer)" -sccm $sccm
		}
	}
}

Get-CollectionRules -parentCollection "XYZ00123" -spacer "" -sccm (Connect-SCCMServer)

A small warning: It will loop all of the subcollections, and the subcollections subcollections, and so on…

Win7 Themes and Screensaver

If you want to enforce users to use a specific screensaver you can do most of it via standard group policies. But if you run Windows 7 and a user changes the current theme… the screensaver will be blank until the next group policy refresh.
This is due to that the default .theme-files have no screensaver defined.

With Group Policy Preferences you can change this…

First we need to change the current ACL on the themes directories since SYSTEM cant write there.

Edit or create a Group Policy.
Browse to Computer Configuration – Windows Settings – Security Settings – File System
Right Click and select Add File… then write %SystemRoot%\Resources\Themes in the Folder-box.
Set the security rights as you want them, but remember to give SYSTEM the rights to Modify.
In the dialog “Add Object” that pops up when you press OK, select Replace existing permissions on all suboflders and files with inheritable permissions, this option is not selected as a default.
Repeat that for %SystemRoot%\Resources\Ease of Access Themes directory.

Then browse to Computer Configuration – Preferences – Windows Settings – Ini Files
Right Click and select New – Ini File
Give the following options:

(I would recommend that you set a item level targeting to check that the file exist)

Now to the boring part, repeat that for all Theme-files in the directories %SystemRoot%\Resources\Themes and %SystemRoot%\Resources\Ease of Access Themes

If there is something else you want to change you can find loads of options for themes in this reference http://msdn.microsoft.com/en-us/library/bb773190%28v=vs.85%29.aspx

Bulk import of SNMP devices to OpsMgr

If you want to import a larger bunch of SNMP-devices in to OpsMgr you will probably go thru the disovery wizard way to many times.

Instead of walking thru that wizard every time I asked the network team to write a CSV-file with all the devices and then used this function to import them.

Function Add-SnmpDevice {
	PARAM (
		[Parameter(Mandatory=$true )][string] $FromIpAddress,
		[Parameter(Mandatory=$true )][string] $MonitoringProxy,
		[string] $ManagementServer,
		[string] $ToIpAddress = "",
		[string] $SnmpCommunityString = "public",
		[int32] $SnmpVersion = 2
	)

	# Single ip ?
	If ($ToIpAddress.Length -eq 0) {
		$ToIpAddress = $FromIpAddress
	}
	
	# Check SNMP version
	if ($SnmpVersion -ne 1 -and $SnmpVersion -ne 2) {
		Throw "Only SNMP version 1 and 2 supported"
	}

	Write-Host "Setting up discovery for SNMP-devices..."
	Write-Host "       From: $($FromIpAddress)"
	Write-Host "         To: $($ToIpAddress)"
	Write-Host "  Community: $($SnmpCommunityString)"
	Write-Host "   SNMP ver: $($SnmpVersion)"

	$networkDeviceClass = Get-MonitoringClass -name "System.NetworkDevice"
	$DeviceDiscoveryConfig = New-DeviceDiscoveryConfiguration -MonitoringClass $networkDeviceClass -FromIpAddress $FromIpAddress -ToIpAddress $ToIpAddress

	# Set Community String
	$encoding = New-Object System.Text.UnicodeEncoding
	$encodedCommunityString = $encoding.GetBytes($SnmpCommunityString)
	$DeviceDiscoveryConfig.ReadOnlyCommunity = [System.Convert]::ToBase64String($encodedCommunityString)

	# Set SNMP version
	$DeviceDiscoveryConfig.SnmpVersion = $SnmpVersion

	# Get management server
	If ($ManagementServer.Length -eq 0) {
		$mgmtServer = Get-RootManagementServer
	} else {
		$mgmtServer = Get-ManagementServer | Where-Object {$_.Name -eq $ManagementServer}
	}
	If ($mgmtServer -eq $null) {
		Throw "Cant find management server named $($ManagementServer)"
	} else {
		Write-Host "Found management server: $($mgmtServer.name)"
	}

	# Find proxy agent
	Write-Host "Lookup of proxy agent named $($MonitoringProxy) ..."
	$ProxyAgent = Get-Agent | Where-Object {$_.Name -eq $MonitoringProxy}
	If ($ProxyAgent -eq $null) {
		Write-Host "No agent named $($MonitoringProxy) found, checking managementservers"
		$ProxyAgent = Get-ManagementServer | Where-Object {$_.Name -eq $MonitoringProxy}
		$ProxyIsMgmtServer = $true
	} else {
		$ProxyIsMgmtServer = $false
	}

	If ($ProxyAgent -eq $null) {
		Throw "Can't find agent or managementserver named $($MonitoringProxy)"
	} else {
		Write-Host "Found $($ProxyAgent.Name)"
	}


	Write-Host "Starting discovery..."
	$DiscResults = Start-Discovery -ManagementServer: $mgmtServer -DeviceDiscoveryConfiguration: $DeviceDiscoveryConfig

	If ($DiscResults.CustomMonitoringObjects.Count -eq 0) {
		Write-Host "Cant discover any objects"
		Return 0
	} else {
		$ObjectCount = 0
		Write-Host "Found objects"
		$discresults | select-object -expandproperty CustomMonitoringObjects | Select-Object Name | Format-Table -HideTableHeaders

		$DiscResults | ForEach-Object {
			Write-Host "Adding object to proxy..."
			if ($ProxyIsMgmtServer -eq $true) {
				$ProxyAgent.InsertRemotelyManagedDevices($_.CustomMonitoringObjects) | Format-Table SnmpDevice, ProxyAgentPrincipalName, ManagementGroup -HideTableHeaders
			} else {
				Add-RemotelyManagedDevice -proxyagent $ProxyAgent -device $_.CustomMonitoringObjects
			}
			$ObjectCount++
		}
		Return $ObjectCount
	}
}

And two examples on how you can use it to add some devices:

# Add a devices in the range 192.168.100.240-.254 with the community "SomeSecret"
# Use mgmtserver.snowland.demo to do the discovery and add the devices with snmpmonitor.snowland.demo as monitoring proxy
Add-SnmpDevice -FromIpAddress "192.168.100.240" -ToIpAddress "192.168.100.254" -SnmpCommunityString "SomeSecret" -ManagementServer "mgmtserver.snowland.demo" -MonitoringProxy "snmpmonitor.snowland.demo"

# Add a single SNMPv1 device with the "public" community, use the RMS to do discovery
Add-SnmpDevice -FromIpAddress "192.168.100.10" -MonitoringProxy "snmpmonitor.snowland.demo" -SnmpVersion 1

So we ended up with something like this:

Import-Csv ".\snmplist.csv" -Delimiter ";" | ForEach-Object {
	Add-SnmpDevice -FromIpAddress $_.IpFrom -ToIpAddress $_.IpTo -SnmpCommunityString $_.Community -ManagementServer $_.MgmtServer -MonitoringProxy $_.Proxy
}

InstallShield – No Log

Ok… It’s kind of hard to find any information on how to completely remove the need for a logfile when installing a InstallShield based setup file.

You can use:
-f2[some\path\to\a\LogFile]
To say where you want the logfile to go.

But… If you completely want to silence it use:
-f2x

It’s that simple, but it isn’t that simple to find the information.

Change Source-paths in ConfigMgr

I’m in the process of moving tons of packages to a new source.

So… I did a few new functions to my Powershell Module http://www.snowland.se/sccm-posh/ :-)

Update-SCCMDriverPkgSourcePath -sccmserver (Connect-SCCMServer) -currentPath "\\OLDSERVER\Source\DriverPackages" -newPath "\\NEWSERVER\Source\DriverPackages"
Update-SCCMPackageSourcePath -sccmserver (Connect-SCCMServer) -currentPath "\\OLDSERVER\Source\Packages" -newPath "\\NEWSERVER\Source\Packages"
Update-SCCMDriverSourcePath -sccmserver (Connect-SCCMServer) -currentPath "\\OLDSERVER\Source\DriverImport" -newPath "\\NEWSERVER\Source\DriverImport"

Oh… and I found some additional updates posted in German by Stefan Ringler … I don’t understand that many words of German, but I can read Powershell. :-P

Anyway, I included the updates in the module… thanks Stefan for sharing.

Create GPOs with Powershell

We are in the process of migrating to a brand spankin new Active Directory … and since it’s new there are no GPOs yet.

To automate and keep a strict naming convention we will use a self service portal to create GPOs.
This portal will have a few dropdown-boxes with options to minimize the risk of an user not creating the GPO as we want…

Anyway. This portal will fire a Powershell script that actualy creates the GPO and sets a bunch of things on it.

This script will:

  • Creates an AD-group
  • Creates an GPO
  • Remove Authenticated Users from GPO Security Filtering
  • Add a Administrator-group to the GPO
  • Adds a group with editing access to the GPO
  • Add the AD-Group created in the first step to Security Filtering on GPO
  • Disable Policy Computer/User Settings depending on the GPO scope
  • Add GPO-link to a Computer- or User-OU

Actually our script will give a few other groups and services (Advanced Group Policy Management – AGPM – to give one example) access to the GPOs and we create a Test-GPO as well… but I guess this is a good start for many of you.

PARAM (
	[string] $gpoScope = "U",
	[string] $gpoDescription = "PowershellTesting01",
	[string] $groupPrefix = "MyPrefix_L_",

	[string] $groupPath = "OU=All Groups,DC=snowland,DC=se",
	[string] $gpoLinkPathC = "OU=All Computers,DC=snowland,DC=se",
	[string] $gpoLinkPathU = "OU=All Users,DC=snowland,DC=se",
	
	[string] $gpoAdminsitrators = "MyPrefix_L_Role-GPO-Administrators",
	[string] $gpoEditors = "MyPrefix_L_Role-GPO-Editors"
)

Import-Module GroupPolicy
Import-Module ActiveDirectory

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
$gpoName = "GPO-$($gpoScope)-$($gpoDescription)"
$adGroupName = "$($groupPrefix)$($gpoName)"
$domainName = (Get-ADDomain).NetBIOSName
$dcServer = (Get-ADDomaincontroller).HostName

Write-Host "Settings:" -ForegroundColor Cyan
Write-Host "   AD GroupName       : $($adGroupName)" -ForegroundColor Cyan
Write-Host "   GPO Name           : $($gpoName)" -ForegroundColor Cyan
Write-Host "   GPO Prod           : $($gpoNameProd)" -ForegroundColor Cyan
Write-Host "   GPO Scope          : $($gpoScope)" -ForegroundColor Cyan
Write-Host "   Domain Controller  : $($dcServer)" -ForegroundColor Cyan
Write-Host "   Domain Name        : $($domainName)" -ForegroundColor Cyan
Write-Host "" -ForegroundColor Cyan

Write-Host "AD: Create AD group -" -ForegroundColor Cyan
New-ADGroup -Name $adGroupName -Description "GPO $($gpoScope) $($gpoDescription)" -GroupScope DomainLocal -Path $groupPath -Server $dcServer

Write-Host "Policy: Create policy" -ForegroundColor Cyan
New-GPO -Name $gpoName -Comment "$($gpoScope) $($gpoDescription)" -Server $dcServer

Write-Host "10 second pause to give AD a chanse to catch up" -ForegroundColor Cyan
Start-Sleep -Seconds 10

Write-Host "Remove Authenticated Users from GPO Security Filtering" -ForegroundColor Cyan
Set-GPPermissions -Name $gpoName -PermissionLevel None -TargetName "Authenticated Users" -TargetType Group -Server $dcServer

Write-Host "Add Administrators to GPO" -ForegroundColor Cyan
Set-GPPermissions -Name $gpoName -PermissionLevel GpoEditDeleteModifySecurity -TargetName $gpoAdminsitrators -TargetType group -Server $dcServer

Write-Host "Add Editors to GPO" -ForegroundColor Cyan
Set-GPPermissions -Name $gpoName -PermissionLevel GpoEdit -TargetName $gpoEditors -TargetType group -Server $dcServer

Write-Host "Add AD-Group to Security Filtering on GPO" -ForegroundColor Cyan
Set-GPPermissions -Name $gpoName -PermissionLevel GpoApply -TargetName "$($adGroupName)" -TargetType Group -Server $dcServer

If ($gpoScope -eq "C") {
	Write-Host "Disable Policy User Settings" -ForegroundColor Cyan
	(Get-GPO -Name $gpoName -Server $dcServer).GpoStatus = "UserSettingsDisabled"

	Write-Host "Add GPO-link to Computer OU" -ForegroundColor Cyan
	New-GPLink -Name $gpoName -Target $gpoLinkPathC -LinkEnabled Yes -Server $dcServer
} else {
	Write-Host "Disable Policy Computer Settings" -ForegroundColor Cyan
	(Get-GPO -Name $gpoName -Server $dcServer).GpoStatus = "ComputerSettingsDisabled"

	Write-Host "Add GPO-link to User OU" -ForegroundColor Cyan
	New-GPLink -Name $gpoName -Target $gpoLinkPathU -LinkEnabled Yes -Server $dcServer
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Write-Host "" -ForegroundColor Cyan
Write-Host "Done!" -ForegroundColor Cyan

Now I only need to figure out how to get AGPM to take control of the GPO …