Making a ConfigMgr Task Sequence Available to a User Collection
A recently had a requirement to bundle a number of applications together into a few different task sequences to make available to a user collection. After a bit of research, I discovered that this is a pretty common question, and that there is no built-in method to make this available. Below, I'll walk through how I was able to accomplish the task using a the task sequence, an application, and a bit of PowerShell.
First, a huge shout out to Ryan Ephgrave. I plagiarized a lot of his process and a bit of his PS to get this working. Where my requirements differed from Ryan were:
- I need to be able to re-run a task sequence (as I'm using them for application bundles and not for an OS), so I can't leave the flag file behind.
- I need users to be able to re-run a bundle, so I can't leave the flag file behind forever, but long enough so that ConfigMgr knows it was successful.
- I need my PowerShell script to be reusable for a number of task sequences, so I don't want to embed it into the script itself.
- I need to be able to run multiple bundles, so I needed dynamic flag/log files (else having one would force the others to show installed).
- I need to deploy based on the task sequence ID and not the deployment ID.
Setting up your Task Sequence
First I need to setup my bundle. For this example, I'm just installing four applications. This is setup the exact way any other task sequence is setup. Here is what one of mine look like:
Next, I deploy this task sequence to any possible computer it could run on, but with a run time that's far off into the future. I've setup my deployment with the following important criteria:
- Target collection is All Windows 10 Systems (but since we're "hiding" it, it won't actually show on all those systems!).
- The action is Install and the Purpose is Available (not required!).
- Most important, the deployment does not become available until 12/30/2033 - the last date allowed by Configuration Manager.
- I download content only when needed by the running task sequence.
After this is created, you'll want to get the Package ID for this Task Sequence. This can be found in the console if you enable it, as I show here:
At this stage, we have a task sequence that's made available to all Windows 10 systems, but it won't actually show up for another 15 years.
Setting up the PowerShell Script
Next up is the PowerShell script, which we'll then use for our application. First, the code:
1Param(
2 [Parameter(Mandatory=$true)]
3 [string]$tsPackageId
4)
5
6$error.Clear()
7
8Add-Type @"
9
10 using System;
11 using System.Text;
12 using System.Runtime.InteropServices;
13
14 public class Posh
15 {
16 public enum MoveFileFlags
17 {
18
19 MOVEFILE_REPLACE_EXISTING = 0x00000001,
20 MOVEFILE_COPY_ALLOWED = 0x00000002,
21 MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004,
22 MOVEFILE_WRITE_THROUGH = 0x00000008,
23 MOVEFILE_CREATE_HARDLINK = 0x00000010,
24 MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x00000020
25 }
26
27 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
28 static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, MoveFileFlags dwFlags);
29
30 public static bool MarkFileDelete (string sourcefile)
31 {
32 bool brc = false;
33 brc = MoveFileEx(sourcefile, null, MoveFileFlags.MOVEFILE_DELAY_UNTIL_REBOOT);
34 return brc;
35 }
36 }
37"@
38
39$strQuery = "Select * from CCM_Scheduler_ScheduledMessage where ScheduledMessageID like '%-$tsPackageId-%'"
40
41Get-WmiObject -Query $strQuery -Namespace root\ccm\policy\machine\actualconfig | ForEach-Object {
42 $ActiveTime = $_.ActiveTime
43 $ActiveTime = "2012" + $ActiveTime.Substring(4)
44 $_.ActiveTime = $ActiveTime
45 $_.Put()
46 $ScheduleID = $_.ScheduledMessageID
47 $AdvertisementID = $_.ScheduledMessageID.Substring(0, $_.ScheduledMessageID.IndexOf("-"))
48}
49
50$strQuery = "Select * from CCM_SoftwareDistribution where ADV_AdvertisementID = '$AdvertisementID'"
51
52Get-WmiObject -Namespace "root\CCM\Policy\Machine\ActualConfig" -Query $strQuery | ForEach-Object {
53 $_.ADV_MandatoryAssignments = "True"
54 $_.Adv_ActiveTime = $ActiveTime
55 $_.ADV_RepeatRunBehavior = "RerunAlways"
56 $_.Put()
57}
58
59Remove-Item "$env:WINDIR\TS_$tsPackageId.log" -ErrorAction SilentlyContinue
60
61Write-Host "WMIPath"
62$WMIPath = "\\.\root\ccm:SMS_Client"
63$SMSwmi = [wmiclass]$WMIPath
64
65Try {
66 Write-Host "About to Trigger...."
67 [Void]$SMSwmi.TriggerSchedule($ScheduleID)
68} Catch {
69 Write-Host "Error Caught"
70 $_ | out-file -FilePath "$env:temp\TS_$tsPackageId_Error.log"
71 throw "Cannot Trigger Schedule"
72}
73
74"$error`r`nDone" | Out-File -FilePath "$env:WINDIR\TS_$tsPackageId.log"
75
76[Posh]::MarkFileDelete("$env:WINDIR\TS_$tsPackageId.log")
I'd like to share what the non-obvious lines of code here does, if you don't care, you can feel free to skip this list:
- Lines 8 - 37 setup a new PowerShell type that we can use to later mark a file as needing to be deleted (or more accurately, "moved to null"). Credit to The Scripting Guys for this bit of code.
- Lines 39 through 48 query the CCM_Scheduler_ScheduledMessage class to find the task sequence that we deployed to all Windows 10 systems (based on the Task Sequence ID). It then changes the active time from 2033 back to a date in the past.
- Lines 50 - 57 query the CCM_SoftwareDistribution class to change the advertisements ActiveTime, set it to mandatory and to always rerun.
- Line 59 is used to clear out any old log file from the last time this was run.
- Lines 61 - 72 triggers the advertisement to run. If the advertisement doesn't run, this will fail and will not move on to the following steps.
- Lines 74 - 76 create a flag file in the Windows directory that we'll use later so that ConfigMgr knows if the installation was a success. It then marks the file to be deleted at next boot.
Now, go put this PS1 file in whatever source folder you use for your applications.
Setting up the Application
As our final step, we tie these two things together with an application. Create an application the same way you normally would. Key aspects here are:
Content location is wherever you're saving your PS1 file created earlier.
Your Installation Program will look like this, where TST0001 is replace with the Package ID you captured earlier.
powershell -executionpolicy bypass -file "RunTaskSequence.ps1" -tsPackageId TST0001
The detection method should be a File search in the path %WINDIR% for TS_TST0001.LOG (where again, TST0001 is replaced with the Package ID of the task sequence).
Other than that, you can set whatever type of requirements you'd like, similar to any other application. Once this is setup, you can deploy the application to your user collection (a test one first, of course!) and once policy updates the users you deploy to should be able to run the Application from the Software Center. Once run, the bundle will show "installed" until the user reboots and the log file goes away. At that time they'll be able to run it again if necessary.
Other Thoughts
First, if you have a better way of doing this, please let me know in the comments - I'd love to be able to build bundles of applications for certain groups and deploy them, and I don't want to run through with the hack of stringing applications together with dependencies. Second, remember that your PS1 is reusable! When you setup your next task sequence, just create a new application and change out the Package ID (in both the command line and detection script) and you're ready to rock.
2 comments
Hi. I've followed the method you detailed but am unable to get it to work. The log file always states the following,
The Package ID of the task sequence is correct.
Hi Dean, have you deployed the task sequence to the system and refreshed the machine policy (or given it enough time to update it on its own)?