Technology Solutions for Everyday Folks
Screen snip (sanitized) of a completed task sequence Slack notification

Triggering a Slack Notification on Completion of a Task Sequence

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

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

Screen snip (sanitized) of a completed task sequence Slack notification. It includes a title/notification header, details of the workstaiton including name, time of completion, nature of installation and OS version, hardware details, and the resulting AD OU location.
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!

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 since there's a second downstream process I am running in this script sequence for an unrelated purpose that isn't in the example. In theory 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!