Tagged: Powershell

Refresh ConfigMgr content where it’s needed

This script will check for content that needs to be refreshed, in this case its content of types like packages, applications, drivers, etc that have the state Retrying or Failed on one or more DPs.
When the script find some content error it will refresh on that DP.

PARAM (
    $sccmServer = "configmgr.snowland.se",
    $sccmSiteCode = "ABC",
    $failStates = "2, 3, 5, 6", # Retrying and Failed (Both Install and Removal)
    $packageTypes = "0, 3, 4, 8, 257, 258" # Not checking 5 (SUP) due to automatic deployments
)

Write-Host "Searching for failed content distributions"
ForEach ($FailedDist in (Get-WmiObject -ComputerName $sccmServer -Namespace "ROOT\SMS\Site_$($sccmSiteCode)" -Query "SELECT * FROM SMS_PackageStatusDistPointsSummarizer WHERE State IN ($($failStates)) AND PackageType IN ($($packageTypes))" | Sort-Object PackageID)) {
    
    # Figure out servername from NalPath
    $failedServer = $FailedDist.ServerNALPath.Substring($FailedDist.ServerNALPath.LastIndexOf("]")+3).Trim("\")

    # Get the distribution points that content wouldn't distribute to
    ForEach ($FailedDPDist in (Get-WmiObject -ComputerName $sccmServer -Namespace "ROOT\SMS\Site_$($sccmSiteCode)" -Query "SELECT * FROM SMS_DistributionPoint WHERE SiteCode='$($FailedDist.SiteCode)' AND PackageID='$($FailedDist.PackageID)' AND ServerNALPath LIKE '%$($failedServer)%'") ) {
        # Refresh content on the selected DP
        Write-Host "Refreshing $($FailedDPDist.PackageID), type $($FailedDist.PackageType) in state $($FailedDist.State) on $($failedServer)"
        $FailedDPDist.RefreshNow = $true
        $FailedDPDist.Put() | Out-Null
    }
}

Write-Host "Done!"

Merry Instagram Christmas

OK, this post might be a bit late… But if you have a Instagramoholic friend that you don’t know what to give for christmas, this might be the thing. πŸ™‚

Running this will output an image of all (Well, a big chunk at least) of the users Instagram images in one single image, then just order a nice print and you are set for christmas.

Will look something like this if you use my account
Example

PARAM (
    $UserName = "rirofal",
    $DownloadPath = "C:\Scripts\psInstaMoz\DL",
    $outputImageName = "C:\Scripts\mozaic.jpg",
    $instaImageSize = 250,
    $maxNofImages = 1600
)

if(!(Test-Path $DownloadPath)) { 
    Throw "Cant access $($DownloadPath)"
}

$JsonData = Invoke-WebRequest "http://instagram.com/$($UserName)/media" | ConvertFrom-Json

$imgNo = 0
while ( $JsonData.more_available -eq $true ) {
    foreach ($item in $JsonData.items) {
        $ImageURL = $item.images.standard_resolution.url
        $ImageDownloadPath = Join-Path -Path $DownloadPath -ChildPath $ImageURL.Split('/')[-1]

        if( !(Test-Path $ImageDownloadPath) ){
            Write-Host "Downloading $($ImageDownloadPath)"
            Invoke-WebRequest $ImageURL -OutFile $ImageDownloadPath
        } else {
            Write-Host "Allready downloaded $($ImageDownloadPath)"
        }
        $imgNo ++
    }

    if ($imgNo -gt $maxNofImages) {
        Write-Host "Reached max of $($maxNofImages)"
        Break
    }

    $LastID = ($JsonData.items | Select -Last 1).id
    $JsonData = Invoke-WebRequest "http://instagram.com/$($UserName)/media?max_id=$LastID" | ConvertFrom-Json
}

# Read local files and calulate output image size
$localFiles = Get-ChildItem -Path $DownloadPath
Write-Host "Downloaded $($localFiles.Count) files"

$sqrtCount = [math]::Sqrt($localFiles.Count)

Write-Host "Square is $($sqrtCount) images"

$absSqrtCount = [math]::floor($sqrtCount)
Write-Host "Rounded down number is $([int] $absSqrtCount)"

[int] $outputImageSize = $absSqrtCount * $instaImageSize
Write-Host "Resulting ImageSize will be $($outputImageSize) px"


# Start to create output image
[Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null

$outFile = New-Object System.Drawing.Bitmap( $outputImageSize,  $outputImageSize )

Write-Host "Selecting $($absSqrtCount * $absSqrtCount) random local files"
$localFiles = $localFiles | Get-Random -Count ($absSqrtCount * $absSqrtCount)

$x = 0
$y = 0

$outImage = [System.Drawing.Graphics]::FromImage($outFile)
$outImage.SmoothingMode = "AntiAlias"

foreach ($localImage in $localFiles) {
    Write-Host "Adding $($localImage.FullName) to output image at X: $($x), Y: $($y)"

    $srcImg = [System.Drawing.Image]::FromFile($localImage.FullName)
    $outImage.DrawImage($srcImg, $x, $y, $instaImageSize, $instaImageSize)

    $x = $x + $instaImageSize
    if ($x -gt ($outputImageSize - $instaImageSize) ) {
        $y = $y + $instaImageSize
        $x = 0
    }
}

Write-Host "Saving JPEG to $($outputImageName)"
$outFile.save($outputImageName, [System.Drawing.Imaging.ImageFormat]::Jpeg)
$outFile.Dispose()

A big thanks to https://github.com/baurmatt/instagram-image-dumper for Instagram downloader code.

Get Powershell ISE to run scripts with -Verbose flag

Missing an easy way to run your scripts from ISE with the -Verbose or -Debug flag?

Easy to add… Open up your Microsoft.PowerShellISE_profile.ps1 file and add the following lines:

$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add('Run with -Verbose', { Invoke-Expression -Command ". '$($psISE.CurrentFile.FullPath)' -Verbose" }, 'Ctrl+F5') | Out-Null
$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add('Run with -Debug',   { Invoke-Expression -Command ". '$($psISE.CurrentFile.FullPath)' -Debug" }, 'Ctrl+F6') | Out-Null

Now when you restart ISE you should see the options under the Add-Ons menu.

Auto load command history

With these few lines in your Powershell Profile you will auto load the command history from the last session.

Run “notepad $profile” and add this:

Function Save-CommandHistory {
	$xmlPath = Join-Path (Split-Path $profile.CurrentUserAllHosts) "commandHistory.xml"
	Write-Verbose "Saving to $($xmlPath)"
	Get-History | Export-Clixml -Path $xmlPath
}

Function Load-CommandHistory {
	$xmlPath = Join-Path (Split-Path $profile.CurrentUserAllHosts) "commandHistory.xml"
	Write-Verbose "Loading from $($xmlPath)"
	Add-History -InputObject (Import-Clixml -Path $xmlPath)
}


Function Prompt {
	# Save the history
	Save-CommandHistory
	
	# Show a prompt
	Write-Host "PS $($PWD.Path.Replace('Microsoft.PowerShell.Core\FileSystem::', ''))>" -NoNewline
}

Write-Host "Loading command history..."
Load-CommandHistory

πŸ™‚

SQL in Powershell without cmdlets

I am currently playing around with ConfigMgr 2012 cmdlets… and they only run in X86 (*doh*).

Anyway, I need to query a database with information about some collections and other stuff I need to create… and running SQL Server cmdlets in X86 doesn’t work.

So to get this to work I found a Powershell function that uses SqlClient from .NET instead… I rewrote the function a bit and here is the result:

Function Run-SqlQuery {
	PARAM(
		[string] $Server = ".",
		[string] $Database = "master",
		[string] $Query,
		[Int32]  $QueryTimeout=60
	)
	$sqlConnection = New-Object System.Data.SqlClient.SQLConnection
	$sqlConnection.ConnectionString = "Data Source=$($Server);Initial Catalog=$($Database);Integrated Security=True;"
	$sqlConnection.Open()
	$sqlCommand = New-Object System.Data.SqlClient.SqlCommand($Query,$sqlConnection)
	$sqlCommand.CommandTimeout = $QueryTimeout
	$sqlDataSet = New-Object System.Data.DataSet
	$sqlDataAdapter = New-Object System.Data.SqlClient.SqlDataAdapter($sqlCommand)
	[void]$sqlDataAdapter.fill($sqlDataSet)
	$sqlConnection.Close()

	return $sqlDataSet.Tables[0]
}

Then you can use it like this:

Run-SqlQuery -Query "EXEC SP_HelpDB"
Run-SqlQuery -Query "SELECT Col1, Col2 FROM MyTable" -Database "MyDatabase" -Server "SomeServer" | Format-Table -AutoSize

This can probably be useful if you need to run a SQL command from a box where your’e not sure if you have SQL cmlets or not.

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 ( ($webReturn).prowl.success.code -eq 200 ) {
        Write-Verbose "Prowl message sent OK"
        Return $true
    } else {        
        Write-Verbose "Error sending Prowl Message: $(($webReturn).prowl.error.code) - $(($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…