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.


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


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/'))

PSOL: Count files in SCCM-inboxes – Version 3

A bit more simple version of the last script… this time as a PowerShell oneliner.

Get-ChildItem \\MYSERVER\SMS_C01\inboxes -Recurse | Group-Object Directory | Where { $_.Count -gt 1 }  | Sort-Object Count -Descending | Format-Table Count, Name -AutoSize

Re-notify users of a alerts

This little powershell-script will update all alerts with resolutionstate “New” and an age of at least 24 hours… and with that done the users subscribing to that alert will get a new notification.

$UpdateAlerts = Get-Alert | where { $_.ResolutionState -eq 0 -and $_.LastModified -lt (get-date).AddHours(-24) }

foreach ($alert in $UpdateAlerts) { $alert.Update("Renotify operators of an untouched alert open for 24 hours") }

PSOL: Resolve informational alerts without repeatcount

To get rid of a large number of alerts in SCOM you can of course use PowerShell…

This script will take informational alerts without any repeats that are 24 hours old and simply resolve them with a small comment.

get-alert | where { $_.Severity -eq 0 -and $_.RepeatCount -eq 0 -and $_.ResolutionState -eq '0' -and $_.LastModified -lt (get-date).AddHours(-24)} | Resolve-Alert -comment "Alert resovled via script (24 hours old informational alert without repeatcount)" | Format-Table MonitoringObjectPath, Name, TimeRaised

(Change “$_.RepeatCount -eq 0″ to “$_.RepeatCount -lt 3″ to get alerts with less then 3 repeats)


Next Page »