Category: PowerShell

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…

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.)

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…

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
}

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.

Maintenance Mode via Powershell Remoting

There are loads of scripts and GUIs that you can use to set Maintenance Mode in OpsMgr, but if you want to do this from a server that doesn’t have the OpsMgr-snapins for Powershell it’s a bit harder…

But then there is Powershell v2 and Remoting… It gives you the option to run a scriptblock on another computer…

Just enable remoting on your RMS and then try this script from another machine:

Function setMaintMode {
	PARAM (
		[string] $rmsHostname,
		[string] $agentName,
		[string] $Comment,
		[string] $Reason,
		[int] $Time
	)

	Invoke-Command -ComputerName $rmsHostname -scriptblock {
		PARAM (
			[string] $agentName,
			[string] $Comment,
			[string] $Reason,
			[int] $Time
		)

		Add-PSSnapin "Microsoft.EnterpriseManagement.OperationsManager.Client"
		Set-Location "OperationsManagerMonitoring::"
		New-ManagementGroupConnection -ConnectionString:localhost | Out-Null
	
		$computerClass = Get-MonitoringClass -name:Microsoft.Windows.Computer
		$computerCriteria = "PrincipalName='" + $agentName + "'"
		$computer = get-monitoringobject -monitoringclass:$computerClass -criteria:$computerCriteria

		if ($computer.InMaintenanceMode -eq $false) {
			$startTime = [System.DateTime]::Now
			$endTime = $startTime.AddMinutes($Time)
			
			New-MaintenanceWindow -startTime $startTime -endTime $endTime -Comment $comment -Reason $Reason -monitoringObject $computer
			return $true
		}  else {
			# Allready in maintenance mode
			return $false
		}

	} -ArgumentList $agentName, $Comment, $Reason, $Time
}

setMaintMode -rmsHostname "rmsserver.domain.local" -agentName "currentserver.domain.local" -Comment "Some comment" -Time 30 -Reason "PlannedOperatingSystemReconfiguration"

What it does is that it run’s the OpsMgr-specific parts on the RMS instead on your local machine… so with that in place it’s easy to create a GUI around it and then spread a shortcut to all your servers that have Powershell v2 installed.

Notes:
The quick and dirty way to enable remoting on your rms, start cmd as an administrator and run winrm quickconfig
Here can you find a quick intro to PS Remoting.

Change a folder LastWriteTime based on files within it

A few days ago I wrote a script that copies some files. Did notice that everything except the date on the folders were ok. So I added a few more lines of powershell code.

Did find a few suggestions on the web, but I like this one…. Since I wrote it. :-P

Function Set-FolderDate {
	Param (
		[string] $Path
	)
	Trap  [Exception] {
		Write-Debug $("TRAPPED: " + $_.Exception.Message); 
		Write-Verbose "Could not change date on folder (Folder open in explorer?)"
		Continue
	}

	# Get latest filedate in folder
	$LatestFile = Get-ChildItem $Path | Sort-Object LastWriteTime -Descending | Select-Object -First 1

	# Change the date, if needed
	$Folder = Get-Item $path
	if ($LatestFile.LastWriteTime -ne $Folder.LastWriteTime) {
		Write-Verbose "Changing date on folder '$($Path)' to '$($LatestFile.LastWriteTime)' taken from '$($LatestFile)'"
		$Folder.LastWriteTime = $LatestFile.LastWriteTime
	}

	Return $Folder
}

Set-FolderDate -Path "D:\temp"

Unzip multiple files via Powershell

Simple and effective.

PARAM (
	[string] $ZipFilesPath = "X:\Somepath\Full\Of\Zipfiles",
	[string] $UnzipPath = "X:\Somepath\to\extract\to"
)

$Shell = New-Object -com Shell.Application
$Location = $Shell.NameSpace($UnzipPath)

$ZipFiles = Get-Childitem $ZipFilesPath -Recurse -Include *.ZIP

$progress = 1
foreach ($ZipFile in $ZipFiles) {
	Write-Progress -Activity "Unzipping to $($UnzipPath)" -PercentComplete (($progress / ($ZipFiles.Count + 1)) * 100) -CurrentOperation $ZipFile.FullName -Status "File $($Progress) of $($ZipFiles.Count)"
	$ZipFolder = $Shell.NameSpace($ZipFile.fullname)


	$Location.Copyhere($ZipFolder.items(), 1040) # 1040 - No msgboxes to the user - http://msdn.microsoft.com/en-us/library/bb787866%28VS.85%29.aspx
	$progress++
}

Btw… Watch out… there is a -Recurse on gci… :-)

ACT filling up your disks?

When you have ACT (The Microsoft Application Compatibility Toolkit) running in your environment you might get disks filled up with XML-files on your ACT-server.

The logprocessor uses those files to populate the database with information. So, when those are processed you do not need them anymore. (If you have a backup of your database…)

So, I did it the easy way, scheduled a powershell command to remove files older than 7 days:

Get-ChildItem D:\ACTLogs -Recurse -Include *.xml* | where {$_.CreationTime -lt (Get-Date).AddDays(-7)} | Remove-Item

If you want a nicer look and feel to your script you can use this instead:

Get-ChildItem D:\ACTLogs -Recurse -Include *.xml* | where {$_.CreationTime -lt (Get-Date).AddDays(-7)} | Sort-Object CreationTime | ForEach-Object {
	Write-Host "Processing: " -ForegroundColor Yellow -NoNewline
	Write-Host $_.FullName -ForegroundColor White -NoNewline
	
	$span = New-TimeSpan $_.CreationTime $(get-date)
	Write-Host " $($span.Days) days old" -ForegroundColor Yellow -NoNewline

	Remove-Item $_.FullName

	Write-Host " [del]" -ForegroundColor Red
}

Manufacturer / Model Collections with hierarchy

The last post made a flat structure of collections with “Manufacturer – Model”, in this post the script creates a hierarchy with (almost) the same collections.


The Model-collections queries are limited to the parent Manufacturer-collection.

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#                                                                                                                            Rikard Ronnkvist / snowland.se
#  Usage:
#   Download and install http://www.snowland.se/sccm-posh/
#   Save the file as CreateMM-collections-Hierarchy.ps1
#   PS:>.\CreateMM-collections-Hierarchy.ps1 -rootCollectionName "Name Of Some Collection"
#
#  2010-03-24   Rikard Ronnkvist    First snowland.se release
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PARAM (
	[string] $rootCollectionName = $(throw "rootCollectionName required."),
	[string] $hostName = (Get-Content env:computername),
	[switch] $Verbose,
	[Switch] $WhatIf
)

if ($verbose.IsPresent) {
	$VerbosePreference = 'Continue'
} Else {
	$VerbosePreference = 'SilentlyContinue'
}

Import-Module SCCM\SCCM-Functions -Force

Write-Verbose "Connect to SCCM-server $($hostName)"
$sccm = Connect-SCCMServer -HostName $hostName

Write-Host "Get root collection: ""$($rootCollectionName)"""
$rootCollection = Get-SCCMCollection -filter "Name='$($rootCollectionName)'" -sccmserver $sccm
if (!$rootCollection) {
	throw "Cant find ""$($rootCollectionName)"""
}
Write-Host "Found collection: $($rootCollection.CollectionID)"


Function checkAndCreate ($CollectionName, $ParentCollectionID, $wql, $limit = $null) {
	Write-Host "Checking ""$($CollectionName)""" -ForegroundColor Cyan
	$newCollection = Get-SCCMCollection -filter "Name='$($CollectionName)'" -sccmserver $sccm
	
	if (!$newCollection) {
		if (!$WhatIf.IsPresent) {
			Write-Host "Creating collection: ""$($CollectionName)"""
			$newCollection = New-SCCMCollection -name "$($CollectionName)" -SccmServer $sccm -parentCollectionID $ParentCollectionID -refreshDays 1 -Verbose
		} else {
			Write-Host "What if: Creating collection: ""$($CollectionName)""" -ForegroundColor Red
		}

		if (!$WhatIf.IsPresent) {
			Write-Verbose "Adding rule with WQL: $wql"
			Add-SCCMCollectionRule -queryExpression $wql -Server $sccm -collectionID $newCollection.CollectionId -queryRuleName $CollectionName -limitToCollectionId $limit
		} else {
			Write-Host "What if: Adding collection rule to new collection with wql: $($wql)" -ForegroundColor Red
		}
	} else {
		Write-Host "Found collection ""$($CollectionName)"""
	}
	
	return $newCollection
}

Write-Host "Lookup Manufacturer and Model"
$Manufacturer = Get-wmiobject -query "SELECT DISTINCT Manufacturer FROM SMS_G_System_COMPUTER_SYSTEM" -computername $Sccm.Machine -namespace $Sccm.Namespace | Sort-Object Manufacturer, Model
$Manufacturer | ForEach-Object {
	$wql = "SELECT * FROM SMS_R_System inner join SMS_G_System_COMPUTER_SYSTEM on SMS_G_System_COMPUTER_SYSTEM.ResourceId = SMS_R_System.ResourceId where SMS_G_System_COMPUTER_SYSTEM.Manufacturer = '$($_.Manufacturer)'"
	$ManufacturerCollection = checkAndCreate -collectionName $_.Manufacturer -ParentCollectionID $rootCollection.CollectionId -wql $wql -limit $null

	$Model = Get-wmiobject -query "SELECT DISTINCT Model FROM SMS_G_System_COMPUTER_SYSTEM WHERE Manufacturer = '$($ManufacturerCollection.Name)'" -computername $Sccm.Machine -namespace $Sccm.Namespace | Sort-Object Manufacturer, Model
	$Model | ForEach-Object {
		$wql = "SELECT * FROM SMS_R_System inner join SMS_G_System_COMPUTER_SYSTEM on SMS_G_System_COMPUTER_SYSTEM.ResourceId = SMS_R_System.ResourceId where SMS_G_System_COMPUTER_SYSTEM.Model = '$($_.Model)'"
		$ModelCollection = checkAndCreate -collectionName $_.Model -ParentCollectionID $ManufacturerCollection.CollectionId -wql $wql -limit $ManufacturerCollection.CollectionId
	}
}