SCCM Module for PowerShell

In a post a few days ago I mentioned “some slightly modified functions from Michael Niehaus“.

Well… Why not share them.

Save this as a module, load it and play around.

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#                                                                                                                            Rikard Ronnkvist / snowland.se
#  Usage:
#   Save the file as SCCM-Commands.psm1
#   PS:>Import-Module SCCM-Commands
#   PS:>Get-SCCMCommands
#
#  2009-04-07   Michael Niehaus     Original code posted at http://blogs.technet.com/mniehaus/
#  2010-03-10   Rikard Ronnkvist    Major makeover and first snowland.se release
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Function Get-SCCMCommands {
	# List all SCCM-commands
	[CmdletBinding()]
	PARAM ()
	PROCESS {
		return Get-Command -Name *-SCCM* -CommandType Function  | Sort-Object Name | Format-Table Name, Module
	}
}

Function Connect-SCCMServer {
	# Connect to one SCCM server
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$false,HelpMessage="SCCM Server Name or FQDN",ValueFromPipeline=$true)][Alias("ServerName","FQDN","ComputerName")][String[]] $HostName = (Get-Content env:computername),
		[Parameter(Mandatory=$false,HelpMessage="Optional SCCM Site Code",ValueFromPipelineByPropertyName=$true )][String[]] $siteCode = $null,
		[Parameter(Mandatory=$false,HelpMessage="Credentials to use" )][System.Management.Automation.PSCredential] $credential = $null
	)

	PROCESS {
		# Get the pointer to the provider for the site code
		if ($siteCode -eq $null -or $siteCode -eq "") {
			Write-Verbose "Getting provider location for default site on server $HostName"
			if ($credential -eq $null) {
				$sccmProviderLocation = Get-WmiObject -query "select * from SMS_ProviderLocation where ProviderForLocalSite = true" -Namespace "root\sms" -computername $HostName -errorAction Stop
			} else {
				$sccmProviderLocation = Get-WmiObject -query "select * from SMS_ProviderLocation where ProviderForLocalSite = true" -Namespace "root\sms" -computername $HostName -credential $credential -errorAction Stop
			}
		} else {
			Write-Verbose "Getting provider location for site $siteCode on server $HostName"
			if ($credential -eq $null) {
				$sccmProviderLocation = Get-WmiObject -query "SELECT * FROM SMS_ProviderLocation where SiteCode = '$siteCode'" -Namespace "root\sms" -computername $HostName -errorAction Stop
			} else {
				$sccmProviderLocation = Get-WmiObject -query "SELECT * FROM SMS_ProviderLocation where SiteCode = '$siteCode'" -Namespace "root\sms" -computername $HostName -credential $credential -errorAction Stop
			}
		}

		# Split up the namespace path
		$parts = $sccmProviderLocation.NamespacePath -split "\\", 4
		Write-Verbose "Provider is located on $($sccmProviderLocation.Machine) in namespace $($parts[3])"

		# Create a new object with information
		$retObj = New-Object -TypeName System.Object
		$retObj | add-Member -memberType NoteProperty -name Machine -Value $HostName
		$retObj | add-Member -memberType NoteProperty -name Namespace -Value $parts[3]
		$retObj | add-Member -memberType NoteProperty -name SccmProvider -Value $sccmProviderLocation

		return $retObj
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Function Get-SCCMObject {
	#  Generic query tool
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipelineByPropertyName=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$true, HelpMessage="SCCM Class to query",ValueFromPipeline=$true)][Alias("Table","View")][String[]] $class,
		[Parameter(Mandatory=$false,HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		if ($Filter -eq $null -or $Filter -eq "")
		{
			Write-Verbose "WMI Query: SELECT * FROM $class"
			$retObj = get-wmiobject -class $class -computername $SccmServer.Machine -namespace $SccmServer.Namespace
		}
		else
		{
			Write-Verbose "WMI Query: SELECT * FROM $class WHERE $Filter"
			$retObj = get-wmiobject -query "SELECT * FROM $class WHERE $Filter" -computername $SccmServer.Machine -namespace $SccmServer.Namespace
		}

		return $retObj
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Function Get-SCCMPackage {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_Package" -Filter $Filter
	}
}

Function Get-SCCMCollection {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_Collection" -Filter $Filter
	}
}

Function Get-SCCMAdvertisement {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_Advertisement" -Filter $Filter
	}
}

Function Get-SCCMDriver {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_Driver" -Filter $Filter
	}
}

Function Get-SCCMDriverPackage {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_DriverPackage" -Filter $Filter
	}
}

Function Get-SCCMTaskSequence {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_TaskSequence" -Filter $Filter
	}
}

Function Get-SCCMSite {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_Site" -Filter $Filter
	}
}

Function Get-SCCMImagePackage {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_ImagePackage" -Filter $Filter
	}
}

Function Get-SCCMOperatingSystemInstallPackage {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_OperatingSystemInstallPackage" -Filter $Filter
	}
}

Function Get-SCCMBootImagePackage {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Optional Filter on query")][String[]] $Filter = $null
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_BootImagePackage" -Filter $Filter
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Function Get-SCCMComputer {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server")][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Filter on SCCM Resource ID",ValueFromPipelineByPropertyName=$true)][int32] $ResourceID = $false,
		[Parameter(Mandatory=$false, HelpMessage="Filter on Netbiosname on computer",ValueFromPipeline=$true)][String[]] $NetbiosName = "%",
		[Parameter(Mandatory=$false, HelpMessage="Filter on Domain name",ValueFromPipelineByPropertyName=$true)][Alias("Domain", "Workgroup")][String[]] $ResourceDomainOrWorkgroup = "%",
		[Parameter(Mandatory=$false, HelpMessage="Filter on SmbiosGuid (UUID)")][String[]] $SmBiosGuid = "%"
	)

	PROCESS {
		if ($ResourceID -eq $false -and $NetbiosName -eq "%" -and $ResourceDomainOrWorkgroup -eq "%" -and $SmBiosGuid -eq "%") {
			throw "Need at least one filter..."
		}

		if ($ResourceID -eq $false) {
			return Get-SCCMObject -sccmServer $SccmServer -class "SMS_R_System" -Filter "NetbiosName LIKE '$NetbiosName' AND ResourceDomainOrWorkgroup LIKE '$ResourceDomainOrWorkgroup' AND SmBiosGuid LIKE '$SmBiosGuid'"
		} else {
			return Get-SCCMObject -sccmServer $SccmServer -class "SMS_R_System" -Filter "ResourceID = $ResourceID"
		}
	}
}

Function Get-SCCMUser {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server")][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Filter on SCCM Resource ID",ValueFromPipelineByPropertyName=$true)][int32] $ResourceID = $false,
		[Parameter(Mandatory=$false, HelpMessage="Filter on unique username in form DOMAIN\UserName",ValueFromPipelineByPropertyName=$true)][String[]] $UniqueUserName = "%",
		[Parameter(Mandatory=$false, HelpMessage="Filter on Domain name",ValueFromPipelineByPropertyName=$true)][Alias("Domain")][String[]] $WindowsNTDomain = "%",
		[Parameter(Mandatory=$false, HelpMessage="Filter on UserName",ValueFromPipeline=$true)][String[]] $UserName = "%"
	)

	PROCESS {
		if ($ResourceID -eq $false -and $UniqueUserName -eq "%" -and $WindowsNTDomain -eq "%" -and $UserName -eq "%") {
			throw "Need at least one filter..."
		}

		if ($ResourceID -eq $false) {
			return Get-SCCMObject -sccmServer $SccmServer -class "SMS_R_User" -Filter "UniqueUserName LIKE '$UniqueUserName' AND WindowsNTDomain LIKE '$WindowsNTDomain' AND UserName LIKE '$UserName'"
		} else {
			return Get-SCCMObject -sccmServer $SccmServer -class "SMS_R_User" -Filter "ResourceID = $ResourceID"
		}
	}
}

Function Get-SCCMCollectionMembers {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server")][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="CollectionID", ValueFromPipeline=$true)][String[]] $CollectionID
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_CollectionMember_a" -Filter "CollectionID = '" + $CollectionID + "'"
	}
}

Function Get-SCCMSubCollections {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server")][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$true, HelpMessage="CollectionID",ValueFromPipeline=$true)][Alias("parentCollectionID")][String[]] $CollectionID
	)

	PROCESS {
		return Get-SCCMObject -sccmServer $SccmServer -class "SMS_CollectToSubCollect" -Filter "parentCollectionID = '$CollectionID'"
	}
}

Function Get-SCCMParentCollection {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server")][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$true, HelpMessage="CollectionID",ValueFromPipeline=$true)][Alias("subCollectionID")][String[]] $CollectionID
	)

	PROCESS {
		$parentCollection = Get-SCCMObject -sccmServer $SccmServer -class "SMS_CollectToSubCollect" -Filter "subCollectionID = '$CollectionID'"

		return Get-SCCMCollection -sccmServer $SccmServer -Filter "CollectionID = '$($parentCollection.parentCollectionID)'"
	}
}

Function Get-SCCMSiteDefinition {
	# Get all definitions for one SCCM site
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer
	)

	PROCESS {
		Write-Verbose "Refresh the site $($SccmServer.SccmProvider.SiteCode) control file"
		Invoke-WmiMethod -path SMS_SiteControlFile -name RefreshSCF -argumentList $($SccmServer.SccmProvider.SiteCode) -computername $SccmServer.Machine -namespace $SccmServer.Namespace

		Write-Verbose "Get the site definition object for this site"
		return get-wmiobject -query "SELECT * FROM SMS_SCI_SiteDefinition WHERE SiteCode = '$($SccmServer.SccmProvider.SiteCode)' AND FileType = 2" -computername $SccmServer.Machine -namespace $SccmServer.Namespace
	}
}

Function Get-SCCMSiteDefinitionProps {
	# Get definitionproperties for one SCCM site
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer
	)

	PROCESS {
		return Get-SCCMSiteDefinition -sccmServer $SccmServer | ForEach-Object { $_.Props }
	}
}

Function Get-SCCMIsR2 {
	# Return $true if the SCCM server is R2 capable
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer
	)

	PROCESS {
		$result = Get-SCCMSiteDefinitionProps -sccmServer $SccmServer | ? {$_.PropertyName -eq "IsR2CapableRTM"}
		if (-not $result) {
			return $false
		} elseif ($result.Value = 31) {
			return $true
		} else {
			return $false
		}
	}
}

Function Get-SCCMCollectionRules {
	# Get a set of all collectionrules
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server")][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="CollectionID", ValueFromPipeline=$true)][String[]] $CollectionID
	)

	PROCESS {
		Write-Verbose "Collecting rules for $CollectionID"
		$col = [wmi]"$($SccmServer.SccmProvider.NamespacePath):SMS_Collection.CollectionID='$($CollectionID)'"

		return $col.CollectionRules
	}
}

Function Get-SCCMInboxes {
	# Give a count of files in the SCCM-inboxes
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server",ValueFromPipeline=$true)][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$false, HelpMessage="Minimum number of files in directory")][int32] $minCount = 1
	)

	PROCESS {
		Write-Verbose "Reading \\$($SccmServer.Machine)\SMS_$($SccmServer.SccmProvider.SiteCode)\inboxes"
		return Get-ChildItem \\$($SccmServer.Machine)\SMS_$($SccmServer.SccmProvider.SiteCode)\inboxes -Recurse | Group-Object Directory | Where { $_.Count -gt $minCount } | Format-Table Count, Name -AutoSize
	}
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Function New-SCCMCollection {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server")][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$true, HelpMessage="Collection Name", ValueFromPipeline=$true)][String[]] $name,
		[Parameter(Mandatory=$false, HelpMessage="Collection comment")][String[]] $comment = "",
		[Parameter(Mandatory=$false, HelpMessage="Refresh Rate in Minutes")] [ValidateRange(0, 59)] [int] $refreshMinutes = 0,
		[Parameter(Mandatory=$false, HelpMessage="Refresh Rate in Hours")] [ValidateRange(0, 23)] [int] $refreshHours = 0,
		[Parameter(Mandatory=$false, HelpMessage="Refresh Rate in Days")] [ValidateRange(0, 31)] [int] $refreshDays = 0,
		[Parameter(Mandatory=$false, HelpMessage="Parent CollectionID")][String[]] $parentCollectionID = "COLLROOT"
	)

	PROCESS {
		# Build the parameters for creating the collection
		$arguments = @{Name = $name; Comment = $comment; OwnedByThisSite = $true}
		$newColl = set-wmiinstance -class SMS_Collection -arguments $arguments -computername $SccmServer.Machine -namespace $SccmServer.Namespace

		# Hack - for some reason without this we don't get the CollectionID value
		$hack = $newColl.PSBase | select * | out-null

		# It's really hard to set the refresh schedule via set-wmiinstance, so we'll set it later if necessary
		if ($refreshMinutes -gt 0 -or $refreshHours -gt 0 -or $refreshDays -gt 0)
		{
			Write-Verbose "Create the recur interval object"
			$intervalClass = [wmiclass]"\\$($SccmServer.Machine)\$($SccmServer.Namespace):SMS_ST_RecurInterval"
			$interval = $intervalClass.CreateInstance()
			if ($refreshMinutes -gt 0) {
				$interval.MinuteSpan = $refreshMinutes
			}
			if ($refreshHours -gt 0) {
				$interval.HourSpan = $refreshHours
			}
			if ($refreshDays -gt 0) {
				$interval.DaySpan = $refreshDays
			}

			Write-Verbose "Set the refresh schedule"
			$newColl.RefreshSchedule = $interval
			$newColl.RefreshType=2
			$path = $newColl.Put()
		}   

		Write-Verbose "Setting parent to $parentCollectionID"
		$subArguments  = @{SubCollectionID = $newColl.CollectionID}
		$subArguments += @{ParentCollectionID = $parentCollectionID}

		# Add the link
		$newRelation = Set-WmiInstance -Class SMS_CollectToSubCollect -arguments $subArguments -computername $SccmServer.Machine -namespace $SccmServer.Namespace

		Write-Verbose "Return the new collection with ID $($newColl.CollectionID)"
		return $newColl
	}
}

Function Add-SCCMCollectionRule {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true,  HelpMessage="SCCM Server")][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(Mandatory=$true,  HelpMessage="CollectionID", ValueFromPipelineByPropertyName=$true)] $collectionID,
		[Parameter(Mandatory=$false, HelpMessage="Computer name to add (direct)", ValueFromPipeline=$true)] [String[]] $name,
		[Parameter(Mandatory=$false, HelpMessage="WQL Query Expression", ValueFromPipeline=$true)] [String[]] $queryExpression,
		[Parameter(Mandatory=$true,  HelpMessage="Rule Name", ValueFromPipeline=$true)] [String[]] $queryRuleName
	)

	PROCESS {
		# Get the specified collection (to make sure we have the lazy properties)
		$coll = [wmi]"$($SccmServer.SccmProvider.NamespacePath):SMS_Collection.CollectionID='$collectionID'"

		# Build the new rule
		if ($queryExpression -ne $null) {
			# Create a query rule
			$ruleClass = [wmiclass]"$($SccmServer.SccmProvider.NamespacePath):SMS_CollectionRuleQuery"
			$newRule = $ruleClass.CreateInstance()
			$newRule.RuleName = $queryRuleName
			$newRule.QueryExpression = $queryExpression

			$null = $coll.AddMembershipRule($newRule)
		} else {
			$ruleClass = [wmiclass]"$($SccmServer.SccmProvider.NamespacePath):SMS_CollectionRuleDirect"

			# Find each computer
			foreach ($n in $name) {
				foreach ($computer in Get-SCCMComputer -sccmServer $SccmServer -Filter "Name = '$n'") {
					# See if the computer is already a member
					$found = $false
					if ($coll.CollectionRules -ne $null) {
						foreach ($member in $coll.CollectionRules) {
							if ($member.ResourceID -eq $computer.ResourceID) {
								$found = $true
							}
						}
					}
					if (-not $found) {
						Write-Verbose "Adding new rule for computer $n"
						$newRule = $ruleClass.CreateInstance()
						$newRule.RuleName = $n
						$newRule.ResourceClassName = "SMS_R_System"
						$newRule.ResourceID = $computer.ResourceID

						$null = $coll.AddMembershipRule($newRule)
					} else {
						Write-Verbose "Computer $n is already in the collection"
					}
				}
			}
		}
	}
}

Function Add-SCCMDirUserCollectionRule {
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true, HelpMessage="SCCM Server")][Alias("Server","SmsServer")][System.Object] $SccmServer,
		[Parameter(ValueFromPipelineByPropertyName=$true)][String[]] $CollectionID,
		[Parameter(ValueFromPipeline=$true)][String[]] $UserName
	)

	PROCESS {
		$coll = [wmi]"\\$($SccmServer.Machine)\$($SccmServer.Namespace):SMS_Collection.CollectionID='$CollectionID'"
		$ruleClass = [wmiclass]"\\$($SccmServer.Machine)\$($SccmServer.Namespace):SMS_CollectionRuleDirect"

		$RuleClass
		$UserRule=Get-User "userName='$UserName'"
		$NewRuleName=$UserRule.name
		$NewRuleResourceID = $UserRule.ResourceID
		$newRule = $ruleClass.CreateInstance()

		$newRule.RuleName = $NewRuleName
		$newRule.ResourceClassName = "SMS_R_User"
		$newRule.ResourceID = $NewRuleResourceID

		$null = $coll.AddMembershipRule($newRule)
		$coll.requestrefresh()
		Clear-Variable -name oldrule -errorAction SilentlyContinue
		Clear-Variable -name Coll -errorAction SilentlyContinue
	}
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# EOF

Some examples on what you can do:

# List all available SCCM commands
Get-SCCMCommands

# Create an SCCM-Connection to the local server
$sccm = Connect-SCCMServer -Verbose

# Create a new collection with a collection rule
$newCollection = New-SCCMCollection -SccmServer $sccm -name "Some Collection Name" -Verbose
Add-SCCMCollectionRule -Server $sccm -collectionID $newRoot.CollectionId -queryExpression "SELECT * FROM SMS_R_System" -queryRuleName "All Systems" -Verbose

# Count files in the inboxes
$sccm | Get-SCCMInboxes

# Get a package
$MyPackage = Get-SCCMPackage -server $sccm -filter "Name = 'Some Package Name'"

If you have some comments, ideas and things to add… Comment this post or shoot me an email.


Scandinavian chars in cmd-files

Got a scriptingquestion from a colleague, wasn’t that easy to find on google.

But with Windows Search I did find an old cmd-script that had exactly that problem sorted out.

So, the question was about scandinavian chars in a cmd-script. Some paths are named with non English letters and when you use them in a script it translates to a strange char instead of the letter.

This works fine:

DEL /F /Q "%USERPROFILE%\Local Settings\Some Directory\*.*"

This doesn’t work since there is a scandinavian letter in the path:

DEL /F /Q "%USERPROFILE%\Lokala inställningar\Some Directory\*.*"

To fix it you need to change codepage, like this:

CHCP 850
DEL /F /Q "%USERPROFILE%\Lokala inställningar\Some Directory\*.*"

You might need to use different codepage depending on the language you are using.

Some more info on MSDN


Add text to images with PowerShell

Been working on some server deployment lately and had some problems with the WinPE… so to not get to confused when using different PE’s to boot up the servers we added some image-information on the background.

Did that in MS Paint… but how fun is that? :-P

Here is a cool function that add some text to a image (Original code from http://www.ravichaganti.com/blog/?p=1012)

Function AddTextToImage {
	# Orignal code from http://www.ravichaganti.com/blog/?p=1012
	[CmdletBinding()]
	PARAM (
		[Parameter(Mandatory=$true)][String] $sourcePath,
		[Parameter(Mandatory=$true)][String] $destPath,
		[Parameter(Mandatory=$true)][String] $Title,
		[Parameter()][String] $Description = $null
	)

	Write-Verbose "Load System.Drawing"
	[Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null

	Write-Verbose "Get the image from $sourcePath"
	$srcImg = [System.Drawing.Image]::FromFile($sourcePath)

	Write-Verbose "Create a bitmap as $destPath"
	$bmpFile = new-object System.Drawing.Bitmap([int]($srcImg.width)),([int]($srcImg.height))

	Write-Verbose "Intialize Graphics"
	$Image = [System.Drawing.Graphics]::FromImage($bmpFile)
	$Image.SmoothingMode = "AntiAlias"

	$Rectangle = New-Object Drawing.Rectangle 0, 0, $srcImg.Width, $srcImg.Height
	$Image.DrawImage($srcImg, $Rectangle, 0, 0, $srcImg.Width, $srcImg.Height, ([Drawing.GraphicsUnit]::Pixel))

	Write-Verbose "Draw title: $Title"
	$Font = new-object System.Drawing.Font("Verdana", 24)
	$Brush = New-Object Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(255, 0, 0,0))
	$Image.DrawString($Title, $Font, $Brush, 10, 10)

	if ($Description -ne $null) {
		Write-Verbose "Draw description: $Description"
		$Font = New-object System.Drawing.Font("Verdana", 12)
		$Brush = New-Object Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(120, 0, 0, 0))
		$Image.DrawString($Description, $Font, $Brush, 10, 50)
	}

	Write-Verbose "Save and close the files"
	$bmpFile.save($destPath, [System.Drawing.Imaging.ImageFormat]::Bmp)
	$bmpFile.Dispose()
	$srcImg.Dispose()
}

With that piece of code and some slightly modified functions from Michael Niehaus you can do some cool stuff…

$taskSequence = "Server Deployment Task Sequence"

Import-Module Misc\Image-Functions.psm1
Import-Module SCCM\SCCM-Functions.psm1

$sccm = Connect-SCCMServer
$taskSequence = Get-TaskSequencePackage -SccmServer $sccm -filter "Name = '$taskSequence'"
$BootImage = Get-BootImagePackage -SccmServer $sccm -filter "PackageID = '$($taskSequence.BootImageID)'"
$DescriptionText = "Version:`t$($BootImage.Version)`nPackageID:`t$($BootImage.PackageID)`n`n$($BootImage.Description)"

AddTextToImage -sourcePath "X:\Path\WinPE-Background-Source.bmp" -destPath "X:\Path\WinPE-Background-To-Inject.bmp" -Title $BootImage.Name -Description $DescriptionText

Shouldn’t be that hard to auto-inject the BMP to the WIM file…


Nice PowerShell prompt

Been playing around with PowerShell the last days, and with nothing else better to do I did a nice looking prompt. :-)

This prompt will give you a marker if you run it as Administrator and another mark if you are outside the filesystem.

How to:
First you need to edit your profile so it autoload the new prompt.

notepad $profile

Then paste this code, save and start a new PowerShell command line.

function prompt
{
	# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
	#                                                                              Rikard Ronnkvist / snowland.se
	# Multicolored prompt with marker for windows started as Admin and marker for providers outside filesystem
	# Examples
	#    C:\Windows\System32>
	#    [Admin] C:\Windows\System32>
	#    [Registry] HKLM:\SOFTWARE\Microsoft\Windows>
	#    [Admin] [Registry] HKLM:\SOFTWARE\Microsoft\Windows>
	# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

	# New nice WindowTitle
	$Host.UI.RawUI.WindowTitle = "PowerShell v" + (get-host).Version.Major + "." + (get-host).Version.Minor + " (" + $pwd.Provider.Name + ") " + $pwd.Path

	# Admin ?
	if( (
		New-Object Security.Principal.WindowsPrincipal (
			[Security.Principal.WindowsIdentity]::GetCurrent())
		).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
	{
		# Admin-mark in WindowTitle
		$Host.UI.RawUI.WindowTitle = "[Admin] " + $Host.UI.RawUI.WindowTitle

		# Admin-mark on prompt
		Write-Host "[" -nonewline -foregroundcolor DarkGray
		Write-Host "Admin" -nonewline -foregroundcolor Red
		Write-Host "] " -nonewline -foregroundcolor DarkGray
	}

	# Show providername if you are outside FileSystem
	if ($pwd.Provider.Name -ne "FileSystem") {
		Write-Host "[" -nonewline -foregroundcolor DarkGray
		Write-Host $pwd.Provider.Name -nonewline -foregroundcolor Gray
		Write-Host "] " -nonewline -foregroundcolor DarkGray
	}

	# Split path and write \ in a gray
	$pwd.Path.Split("\") | foreach {
	    Write-Host $_ -nonewline -foregroundcolor Yellow
	    Write-Host "\" -nonewline -foregroundcolor Gray
	}

	# Backspace last \ and write >
	Write-Host "`b>" -nonewline -foregroundcolor Gray

    return " "
}

Read MSI information with PowerShell

I tried to search for some way to read MSI-information with PowerShell, that wasn’t to easy to find.

Anyway, here is a function that helps you read the “Property” view from one MSI-file, the result is stored in a hash table.

# Load some TypeData
$SavedEA = $Global:ErrorActionPreference
$Global:ErrorActionPreference = "SilentlyContinue"
Update-TypeData -AppendPath ((Split-Path -Parent $MyInvocation.MyCommand.Path) + "\comObject.types.ps1xml")
$Global:ErrorActionPreference = $SavedEA

function global:get-msiproperties {
	PARAM (
		[Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,HelpMessage="MSI Database Filename",ValueFromPipeline=$true)]
		[Alias("Filename","Path","Database","Msi")]
		$msiDbName
	)

	# A quick check to see if the file exist
	if(!(Test-Path $msiDbName)){
		throw "Could not find " + $msiDbName
	}

	# Create an empty hashtable to store properties in
	$msiProps = @{}

	# Creating WI object and load MSI database
	$wiObject = New-Object -com WindowsInstaller.Installer
	$wiDatabase = $wiObject.InvokeMethod("OpenDatabase", (Resolve-Path $msiDbName).Path, 0);

	# Open the Property-view
	$view = $wiDatabase.InvokeMethod("OpenView", "SELECT * FROM Property")
	$view.InvokeMethod("Execute");

	# Loop thru the table
	$r = $view.InvokeMethod("Fetch");
	while($r -ne $null) {
		# Add property and value to hash table
		$msiProps[$r.InvokeParamProperty("StringData",1)] = $r.InvokeParamProperty("StringData",2)

		# Fetch the next row
		$r = $view.InvokeMethod("Fetch");
	}

	# Return the hash table
	return $msiProps
}

You need to expand your type configuration with this file, saved as comObject.types.ps1xml in the same directory as the script above.

<!--
Copy from: Abhishek's PowerShell Blog - http://abhishek225.spaces.live.com/blog/

Original post: http://abhishek225.spaces.live.com/blog/cns!13469C7B7CE6E911!165.entry
-->
<Types>
	<Type>
		<Name>System.__ComObject</Name>
		<Members>
			<ScriptMethod>
				<Name>GetProperty</Name>
				<Script>
					$type = $this.gettype();
					$type.invokeMember($args[0],[System.Reflection.BindingFlags]::GetProperty,$null,$this,$null)
				</Script>
			</ScriptMethod>
			<ScriptMethod>
				<Name>SetProperty</Name>
				<Script>
					$type = $this.gettype();
					$type.invokeMember($args[0],[System.Reflection.BindingFlags]::GetProperty,$null,$this,@($args[1]))
				</Script>
			</ScriptMethod>
			<ScriptMethod>
				<Name>InvokeParamProperty</Name>
				<Script>
					$type = $this.gettype();
					$index = $args.count -1 ;
					$methodargs=$args[1..$index]
					$type.invokeMember($args[0],[System.Reflection.BindingFlags]::GetProperty,$null,$this,$methodargs)
				</Script>
			</ScriptMethod>
			<ScriptMethod>
				<Name>InvokeMethod</Name>
				<Script>
					$type = $this.gettype();
					$index = $args.count -1 ;
					$methodargs=$args[1..$index]
					$type.invokeMember($args[0],[System.Reflection.BindingFlags]::InvokeMethod,$null,$this,$methodargs)
				</Script>
			</ScriptMethod>
		</Members>
	</Type>
</Types>

Off topic: Powershell to rename pictures

I’m trying hard to convert myself from VBScript to Powershell… so in the need of a powerfull filerenamer I wrote one in Powershell.

You need to download the Powershell Pack from http://code.msdn.microsoft.com/PowerShellPack

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#                                                                                           Rikard Ronnkvist / snowland.se
#  Will rename JPG and AVI files, uses EXIF-tag on JPG-images and filedate on AVI-files.
#
#  Files will be named:
#    \some path\YYYYMM\YYYYMMDD_HHMMSS_00.JPG
#               ^^^^^^ - Optional, if you have createSubdir set to True
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
$basePath = "F:\Pictures\Import dir"
$createSubdir = $True
$testMode = $False
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Download from:  http://code.msdn.microsoft.com/PowerShellPack
Import-Module PowerShellPack

# Add \* to use when searching
$searchPath = $basePath + "\*"

# Search for files
Write-Host "           Searching: " -ForegroundColor DarkGray -NoNewline
Write-Host $basePath -ForegroundColor Yellow

$allFiles  = Get-ChildItem -Path $searchPath -Include *.AVI,*.JPG -Exclude folder.jpg

Write-Host "               Found: " -ForegroundColor DarkGray -NoNewline
Write-Host $allFiles.Count -ForegroundColor Yellow -NoNewline
Write-Host " files" -ForegroundColor DarkGray

$fNum = 0
# Loop thru all files
foreach ($file in $allFiles )
{
	$fNum++
	# If it is an jpg use the exif-data, otherwise use date on file
	if ($file.Extension -eq ".JPG") {
		$imgInfo = $file | Get-Image | Get-ImageProperty
		$fileDate = $imgInfo.dt
	} else {
		$fileDate = $file.LastWriteTime
	}

	if ($createSubdir -eq $True) {
		# Set new filepath
		$fileDir = $basePath + "\" + $fileDate.ToString("yyyyMM")

		# Check directory
		if (!(Test-Path($fileDir))) {
			# Create a new subdirectory
			if ($testMode -ne $True) {
				$newDir = New-Item -Type directory -Path $fileDir
				Write-Host "            Creating: " -ForegroundColor DarkGray -NoNewline
				Write-Host $fileDir -ForegroundColor Red
			}
		}
	} else {
		# Use current directory
		$fileDir = $basePath
	}

	# Set new name to current to get "False" on first while
	$newPath = $file.Fullname

	$i = 0
	while (Test-Path $newPath) {
		# Set new filename
		$newPath = $fileDir + "\" + $fileDate.ToString("yyyyMMdd_HHmmss") + "_" + $i.ToString("00") + $file.Extension
		$i++
	}

	# Write som info
	Write-Host $fNum.ToString().PadLeft(4) -ForegroundColor DarkYellow -NoNewline
	Write-Host " / " -ForegroundColor DarkGray -NoNewline
	Write-Host $allFiles.Count.ToString().PadRight(4) -ForegroundColor Yellow -NoNewline
	Write-Host "   Moving: " -ForegroundColor DarkGray -NoNewline
	Write-Host $file.Name -ForegroundColor Cyan -NoNewline
	Write-Host " -> " -ForegroundColor DarkGray -NoNewline
	Write-Host $newPath -ForegroundColor Green

	# Move and rename the file
	if ($testMode -ne $True) {
		Move-Item $file.Fullname $newPath
	}
}

Ending up with something like this:

Oh, and you can add -Recurse on the Get-ChildItem row…


Problems with new-TestCasConnectivityUser.ps1

When we tried to run new-TestCasConnectivityUser.ps1 to create some mailboxes for the test-cmdlets we ran in to some strange problems.

Got an errormessage stating “CreateTestUser : Mailbox could not be created. Verify that OU ‘Users’ exists and that password meets complexity require”

The OU exist and the password is OK… strange. Googled and didn’t find anything that could relate to that problem. Then I started to disect/debug the powershell script…

Ended up with a command like:

$SecurePassword = Read-Host "Enter password" -AsSecureString
new-Mailbox -Name:ext_dummy -Alias:ext_dummy -UserPrincipalName:ext_dummy@domain.se -SamAccountName:ext_dummy -Password:$SecurePassword -Database:somestrangeguid -OrganizationalUnit:Users

When running that command it says that “Multiple organizational units match identity “Users”. Specify a unique value.”

OK, we have another OU in the hierarchy named “Users”…
Edited the script and changed the value of $OrganizationalUnit to another OU and did a new test with “get-mailboxServer | .\new-TestCasConnectivityUser.ps1″ and a few seconds later we have the users. :-)


Demo Webservice

A while ago I wrote a little PHP-application for demostrating webmonitors in System Center Operations Manager 2007.
It will probably work for any monitoring solution that catches http errorcodes or textstrings on a page.

Free to use, so give it a try. :-)

You can find it at: http://snowland.se/demo/


PSOL: Read web page

If you need to output something from a webpage via Powershell, then .NET is a easy way to do it.

Write-Host ([String] (New-Object Net.WebClient).DownloadString('http://snowland.se/demo/'))

OpsMgrDW Grooming

After a while my demo environment with OpsMgr data warehouse (and loads of other stuff) needed some more space.

I haven’t looked in to grooming of the DW before. So as usual Google is a nice friend. :-)

Stefan Stranger has a nice post with loads of grooming information.

So what I did was to download the dwdatarp tool.

First I ran it to see the current status

dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW

Looks like most of the datasets are stored 400 or 180 days… That is somewhat to much data for a demo environment.
I took the values and divided them by 4. Then ran the following to free up 75% of the database.

dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds Alert -a Raw -m 100
dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds "Client Monitoring" -a Raw -m 8
dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds "Client Monitoring" -a Daily -m 100
dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds Configuration -a Raw -m 100
dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds Event -a Raw -m 25
dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds Performance -a Raw -m 45
dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds Performance -a Hourly -m 100
dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds Performance -a Daily -m 100
dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds State -a Raw -m 45
dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds State -a Hourly -m 100
dwdatarp.exe -s SNWSQL01 -d OperationsManagerDW -ds State -a Daily -m 100

OK… when you have a database full of free space you need to truncate it, but that’s another story…


Next Page »