A number of years ago I started having my primary OSD Task Sequences "check in" as one of their last steps. Specifically check in with key information so I can follow up as necessary ("trust but verify") with downstream actions to ensure our asset management systems are properly updated.
I chose a Slack webhook to accomplish this as I could set up a quiet channel for notifications and queue them for later review (once a day or a few times per week).
As I've presented sessions at MMS, usergroups, and other conferences referencing this Slack notification process, I often get follow-up questions from attendees about how it all works. So I decided to write about it and share!
Why Slack?
Short version: It's what we use in our environment and creating a private, quiet channel with a webhook was easy. But the principles within can be applied to many other services like Teams or even to fire off an email notification.
Quick Disclosure
What I've implemented and share here is actually a middleware solution. I'm queuing submissions from Task Sequences and processing them in batch via cron
, which in turn makes the webhook call. I'm not directly having a workstation send the webhook request since I'm also using this data for other purposes; however, the concepts in this post could absolutely be condensed into one direct call from the Task Sequence. It would work.
The Basic Workflow
Summed up in a linear illustration, key parts of the process looks like this:
(Powershell) Invoke-RestMethod --> Data written to queue --> Processing scripts --> Slack webhook/notification sent
The Details
Task Sequence Step
In my primary production Task Sequence, I have a step called "TS Completion Check-In
" which is a Run PowerShell Script
step type. It is the second to last step for me, and invokes the following script:
$authCode = "randomstringgoeshere"
$deviceName = hostname
$checkinUrl = 'https://fqdn/path/to/receiver/'
$timestamp = [math]::Truncate((New-TimeSpan -Start (Get-Date "01/01/1970") -End ((Get-Date).ToUniversalTime())).TotalSeconds) # Adjust output to UTC
# Task Sequence Variables
$tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment
# These variables are globally available
$MACAddress = (Get-NetAdapter | Where-Object Status -eq "Up").MacAddress
$OSInfo = (Get-ComputerInfo | Select-Object OsName, OsVersion)
$OSData = ($OSInfo.OsName -replace "Microsoft ", "") + " (" + $OSInfo.OsVersion + ")"
if ($MACAddress.Count -gt 1) {
$MAC = $MACAddress[0]
} else {
$MAC = $MACAddress
}
# These variables are custom and will be empty unless your TS declares/sets them
$InstallType = $tsenv.Value("InstallType")
$OSDOUCN = $tsenv.Value("OSDOUCN")
$FirmwareRev = $tsenv.Value("OSDDeviceFirmwareVersion")
# Create POST Body (hashtable)
$body = @{
name = $deviceName
auth = $authCode
time = $timestamp
install = $InstallType
ou = $OSDOUCN
macaddress = $MAC
firmware = $FirmwareRev
osdata = $OSData
}
# Create Headers
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/x-www-form-urlencoded")
# Parameter Hashtable
$Params = @{
Uri = $checkinUrl
Method = "Post"
Headers = $headers
Body = $body
}
# Submit!
$response = Invoke-RestMethod @Params
$response | ConvertTo-Json
A couple of notes about this relatively simple script:
- I use an
$authCode
string as an additional layer of security obfuscation to prevent completely random hits to the web server from being processed (more about that in the next step); $checkinUrl
is the FQDN to the receiver host, in my case an internal server hosting the PHP shared below which only responds to internal (network line of sight) requests;- A handful of TS variable values (such as my custom
$InstallType
and $FirmwareRev
) are collected and used to craft the POST body of the Invoke-RestMethod
request — these variables are set and manipulated elsewhere in the TS; and - A
$response
from the request is output as JSON just so it'll be captured in the SMSTS.log
if necessary to evaluate a problem/failure.
The "Middleware" Processing Step(s)
Receiving the Data
On the server side (the $checkinUrl
above), there's a super basic PHP script to handle the POST data and write it to file:
<?php
$authCode = "samerandomstringgoeshere";
if (isset($_POST['auth'])) {
if ($authCode == $_POST['auth']) {
// Write to file named for the host at the data path
$filename = __DIR__ . '/../data/'.$_POST['name'].'.php';
// Write out device data after removing the auth code
unset($_POST['auth']);
file_put_contents($filename, '<?php return ' . var_export($_POST, true) . '; ?>');
chmod($filename, 0660);
print "Request Successful.";
} else {
print "Request Failed.";
}
} else {
print "Request Failed.";
}
?>
This receiver writes a file named for the submitting device at the data
path (writable by the web server process and outside the web server docroot). This data
path is our "queue" for notifications to process and for each file written will include a PHP array representing the data originally submitted (minus the auth code since we only use that to help filter out bad submissions). An important note: this PHP intake script as shared does not sanitize inputs so be super judicious in how you use this as a starting point! Also: remember to properly sanitize your inputs in production systems!
Processing Queued Responses
I have a crontab entry to process the queued up responses on a schedule throughout the week:
# Regular runs for task sequences completed:
1 10,12,14,16 * * 1-5 /path/to/php /path/to/TSCompleteNotifier.php >> /dev/null 2>&1
This runs at 1 minute after the 10, 12 (noon), 14, and 16 hours Monday through Friday. The schedule works well for our normal volume and cadence.
The TSCompleteNotifier.php
script looks like this:
<?php
$slackWebhookURL = 'https://hooks.slack.com/you/generated/when/setting/up';
$dataPath = __DIR__ . '/data/';
// Create multidimensional array of all active files
$notifyDevices = array();
foreach (glob($dataPath . '*.php') as $newFile) {
$deviceName = explode('.php', basename($newFile))[0];
$notifyDevices[$deviceName] = include $newFile;
}
// Push the new device notifications to Slack
foreach ($notifyDevices as $notification) {
$channel = '#quiet-private-channel-name-goes-here';
$bot_name = 'Device Rebuild Completed';
$icon = ':information_source:';
$message = '';
$blockMessage = ":computer: $notification[name] completed task sequence on " . date("F j, g:i a", $notification['time']);
$attachments = array([
'fallback' => "Device rebuild detected: $notification[name] completed task sequence on $notification[time]",
'pretext' => ':information_source: Device Rebuild Completed',
'color' => '#8c1919',
'fields' => array(
[
'title' => 'Rebuild Details:',
'value' => $blockMessage,
'short' => false
],
[
'title' => 'Installation Details:',
'value' => ":triangular_flag_on_post: `$notification[install]`\n`$notification[osdata]`",
'short' => true
],
[
'title' => 'Device Details:',
'value' => ":desktop_computer: `$notification[macaddress]`\nfirmware revision `$notification[firmware]`",
'short' => true
],
[
'title' => 'AD Organizational Unit:',
'value' => "`$notification[ou]`",
'short' => false
]
)
]);
$data = array(
'channel' => $channel,
'username' => $bot_name,
'text' => $message,
'icon_emoji' => $icon,
'attachments' => $attachments
);
$data_string = json_encode($data);
$ch = curl_init($slackWebhookURL);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array(
'Content-Type: application/json',
'Content-Length: ' . strlen($data_string)
)
);
// Execute cURL
$result = curl_exec($ch);
curl_close($ch);
}
// Remove the notified files
array_map('unlink', glob($dataPath . '*.php'));
?>
By setting the $slackWebhookURL
to the specific URL you generated when creating a webhook in Slack, along with setting the appropriate $channel
name a few lines below, the rest of the script will output a structured notification for each of the data files queued up and remove them afterward. The notification will look like this (sanitized from a recent notification):
Aside: How To Create the Notification Body
Over half of the TSCompleteNotifier.php
script relates to the structure of the Slack Blocks used to structure the "pretty" message and the resulting array of data that's sent to the Slack webhook URL. Slack has a wonderful Block Kit Builder (workspace login required) to build a layout and test for valid structure. Blocks are the way this notification has the bold subheaders and side-by-side layout and their definitions are in the 'fields'
part of the script above. Slack provides great detail about the Block Kit for reference.
The Repo
I've created a starter repo you can use for your own purpose with the scripts above. It requires the middleware host (Linux/Apache/PHP) as of the time of this post's publication, though I might also add a direct Powershell script in the near future if I have some time (see postscript below). Feel free to use it as a launch point for your own notification process!
UPDATE: I have added a "direct from the Task Sequence" Powershell script to be a one-step notification mechanism. See the related blog post for more information.
Postscript
As I originally noted, there's not a reason you couldn't combine the Block Kit portions of the TSCompleteNotifier.php
script with that of the "TS Completion Check-In
" step to do this all in one stop. I chose not to do that in production since there's a second downstream process I am running in this script sequence for an unrelated purpose that isn't in the example. It's as straightforward as changing the $checkinUrl
to the Slack webhook URL, the $body
array to "mirror" the PHP script's $data_string
with all the block and message structure, and modifying the $headers
accordingly.
Hopefully this was helpful or insightful. Good luck on your own notification adventure/idea!