Note: Download the latest solution files and view complete documentation and video guides here:

SMS / Voice Notifications with PowerShell, System Center Operations Manager and Twilio

 

What It Does

This script allows System Center Operations Manager 2012 notifications to be sent via SMS messages or voice calls by integrating with the internet-based Twilio messaging service (no modem or mobile service required). It can send the dynamic content of an alert either as text in an SMS message, or machine-read as speech in a voice call. The script integrates as a Notification Channel in Operations Manager, making it easy to use alongside email and instant messaging notifications.

SMS / Voice Notifications with PowerShell, System Center Operations Manager and Twilio
Even the best admins can't put out fires in their sleep

Usage Scenario

Most Operations Manager deployments use email for notifications of alerts. Sometimes additional methods of contact may be helpful, e.g. having critical alerts trigger a phone call to operations staff after-hours to maximize the chance of notification being delivered (who doesn’t like being woken up at 3 AM?). This script can provide a supplement to email notifications by providing automated SMS text messages and voice phone calls as alternative communication channels.

This method provides arguably the least expensive means to implement SMS and voice integration. It requires only an Internet connection and a Twilio account, which can be operated in trial mode for free, or for very low cost with a full-featured account (less than $0.01 per message). If you don’t require the robust features of third party notification add-ons like those from Derdack, this is a simple and cheap option.

The script respects the schedules configured for subscribers, so you can limit messages from being sent outside allowed notification windows.

How it Works

The script is invoked as a notification command in Operations Manager, as triggered by a subscription. It is added as a Channel alongside the built-in channels like Email and Instant Message. You can then add it to any Notification Subscription as a channel for delivery. When the subscription is triggered by an alert, the script will be executed and the specified message will be sent to each of the subscribers who has an SMS address (phone number) configured and whose notification schedules allow delivery at the time of the alert.

Twilio, PowerShell, Operations Manager notification architecture
How the notification magic happens

Twilio Integration

Twilio provides the message delivery. The script uses the Twilio API to connect to the service via HTTP and request delivery of the message to a given recipient. Therefore, a Twilio account must be available and configured for the script to use. It’s worth noting that this means an Internet connection is required for message delivery. Therefore, rest assured that this script will never notify you about your Internet connection being down.

Delivery Methods

Messages can be delivered by SMS text message or voice phone call. The method is determined by the MessageType parameter used when invoking the script (“SMS” or “Voice”). Messages are truncated to fit within the SMS 160 character limit. For voice calls, the message text is read aloud by a robot voice.

Configuration

There are two aspects of configuring the script. First, an XML configuration file is used to provide Twilio account details. Because the account credentials can be used to manipulate the account (whether accidentally or maliciously), ensure the configuration file is protected by reasonable means to be accessible only to the Operations Manager service account and trusted administrators.

Second, the script command-line parameters are used to specify the runtime options. These are documented in the script comments.

Implementing In Your Environment

See the documentation and download page for implementation details. A step-by-step installation video is also included.

The Script

 

PowerShell
Edit|Remove
<# 
    .SYNOPSIS 
        Sends a message via SMS or voice call using the Twilio messaging service to  
        recipients defined in the specified System Center Operations Manager subscription. 
 
    .DESCRIPTION 
        This script is intended to be invoked as a notification command in System Center  
        Operations Manager 2012/R2. It accepts a message in the form of a string and sends 
        to a list of phone numbers retrieved from the recipients defined in the specified  
        subscription. The message is truncated to fit with the 160 character SMS limit and 
        delivered via the Twilio messaging service (see www.twilio.com).  
         
        A valid Twilio account must be provided to the script via an associated configuration file. 
        Additionally, the .Net library for the Twilio API must be available in a configured path 
        on the system. 
         
        The recipients must be Subscribers defined in Operations Manager with a "Text Message (SMS)" 
        address defined, which is a phone number. The number can be in one of several formats, though 
        Twilio prefers E.164, e.g.(for US) +15551234567.  
 
    .PARAMETER  MessageText 
        The text of the message to send as a string. Will be truncated to fit within the 160 
        character SMS limit. For voice calls, Twilio will perform text-to-speech conversion. 
     
    .PARAMETER  MessageType 
        Choice of SMS or Voice. SMS will be delivered as a normal text message. Voice will 
        be delivered as a voice phone call with machine reading of the MessageText. 
 
    .PARAMETER  SubscriptionID 
        The ID property of the Operations Manager subscription that defines recipients for this 
        message. Preferred format is "{GUID}" and is provided in notifcation channel command parameters using the 
        "$MPElement$" substituion variable. Alternatively, script will attempt to use as DisplayName 
        to find subscription by name rather  than ID. 
 
    .PARAMETER  ConfigurationFilePath 
        The directory containing the XML configuration file which defines the Twilio account SID,  
        auth token, sending phone number, and API files path.  
 
    .PARAMETER  EnableTraceLogging 
        Switch to enable trace logging to a file in the same directory as the script. Unless included,  
        no trace data will be logged. 
 
    .EXAMPLE 
        To manually test with trace logging from a PowerShell prompt using a subscription ID obtained from Operations Manager: 
         
        .\Send-TwilioMessage.ps1 -MessageText "London Bridge is falling down!" -MessageType SMS -SubscriptionID "{5B2E1566-39E8-DB71-4A19-2C55FEF4829A}" -EnableTraceLogging 
 
    .EXAMPLE 
        To manually test a voice call from a PowerShell prompt using a subscription ID obtained from Operations Manager: 
         
        .\Send-TwilioMessage.ps1 -MessageText "London Bridge is falling down!" -MessageType Voice -SubscriptionID "{5B2E1566-39E8-DB71-4A19-2C55FEF4829A}" 
 
    .EXAMPLE 
        Configured as command parameters to powershell.exe in Operations Manager notification channel command, 
        sending the alert name as an SMS message: 
         
        -File Send-TwilioMessage.ps1 -MessageType SMS -MessageText "OpsMgr Alert: $Data[Default='Not Present']/Context/DataItem/AlertName$" -SubscriptionID "$MPElement$" 
 
    .INPUTS 
        None. 
 
    .OUTPUTS 
        No objects returned. 
 
    .NOTES 
        For more details and implementation guidance, see the associated documentation at https://automys.com 
#> 
 
[CmdletBinding()] 
Param( 
    [Parameter(Mandatory=$true)] 
    [ValidateNotNullOrEmpty()] 
    [string]$MessageText, 
 
    [ValidateSet("SMS","Voice")] 
    [string]$MessageType = "SMS", 
 
    [Parameter(Mandatory=$true)] 
    [ValidateNotNullOrEmpty()] 
    [string]$SubscriptionID, 
 
    [string]$ConfigurationFilePath, 
 
    [switch]$EnableTraceLogging 
) 
 
 
# Define function to add entry to trace log located in same folder as script 
function AppendLog ([string]$Message) 
{ 
    if($EnableTraceLogging -eq $true) 
    { 
        Add-Content -Path $logPath -Value ((Get-Date).ToString() + "`t" + $Message) 
    } 
} 
 
# Define function to check the master and address schedules for an individual subscriber 
# Returns true if current time conforms to schedules, false if not 
function CheckSubscriberSchedules ($Subscriber$Address) 
{ 
    $scheduleValidated = $true 
 
    # Check against subscriber master schedule(s). If any violated, overall check not satisifed. 
    foreach($scheduleEntry in $Subscriber.ScheduleEntries) 
    { 
        if((CheckSchedule -Schedule $scheduleEntry-eq $false) 
        { 
            $scheduleValidated = $false 
        } 
    } 
             
    # Check against subscriber address (phone number) schedule(s). If any violated, overall check not satisifed. 
    foreach($scheduleEntry in $Address.ScheduleEntries) 
    { 
        if((CheckSchedule -Schedule $scheduleEntry-eq $false) 
        { 
            $scheduleValidated = $false 
        } 
    } 
 
    return $scheduleValidated 
} 
 
# Define function to convert a NotificationRecipientScheduleEntry time range object to a standard DateTime format in the specified time zone 
function ConvertTimeRange ($EntryTime$TimeZone) 
{ 
    $convertedTime = Get-Date -Hour $EntryTime.Hour -Minute $EntryTime.Minute -Second 0 
    return [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId($convertedTime$scheduleTimeZone) 
} 
 
# Define function to check a single schedule 
# Returns true if current time conforms to schedule, false if not 
function CheckSchedule ($Schedule) 
{ 
    if($Schedule -eq $null) 
    { 
        return $true 
    } 
 
    # Begin with no violations, setting if found 
    $scheduleViolated = $false 
             
    # Get current time in target time zone 
    $scheduleTimeZone = $Schedule.TimeZone.Substring($Schedule.TimeZone.IndexOf("|"+ 1) 
    $now = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([DateTime]::UtcNow, $scheduleTimeZone) 
 
    # Date range check 
    # If date range defined and current time is outside the range, record violation. Otherwise, check passes. 
    if($Schedule.ScheduledStartDate -ne $null -and $Schedule.ScheduledEndDate -ne $null) 
    { 
        if($now -lt $Schedule.ScheduledStartDate -or $now -gt $Schedule.ScheduledEndDate) 
        { 
            $scheduleViolated = $true 
        } 
    } 
 
    # Daily time range check 
    # If current time is outside the daily time range, record violation. Otherwise, check passes. 
    if($now -lt (ConvertTimeRange -EntryTime $Schedule.DailyStartTime -TimeZone $scheduleTimeZone-or $now -gt (ConvertTimeRange -EntryTime $Schedule.DailyEndTime -TimeZone $scheduleTimeZone)) 
    { 
        $scheduleViolated = $true 
    } 
 
    # Day of week test 
    $allowedDays = @() 
    $scheduleDaysString = $Schedule.ScheduledDays.ToString() 
    $scheduleDaysString = $scheduleDaysString -replace "Weekdays","Monday,Tuesday,Wednesday,Thursday,Friday" 
    $scheduleDaysString = $scheduleDaysString -replace "WeekendDays","Saturday,Sunday" 
    switch ($scheduleDaysString) 
    { 
        "None" { 
            # No days allowed 
        } 
        "All" { 
            $allowedDays +"Monday""Tuesday""Wednesday""Thursday""Friday""Saturday""Sunday" 
        } 
        default { 
            # One or more days by name, comma separated 
            $allowedDays +$scheduleDaysString -replace " ","" -split ","  
        } 
    } 
             
    # If today is not in the list of allowed days, record violation 
    if(($allowedDays -contains $now.DayOfWeek) -eq $false) 
    { 
        $scheduleViolated = $true 
    } 
 
    # Determine overall result 
    # If now is outside the schedule and we wanted to be inside, return false to indicate schedule not satisifed 
    if ($Schedule.ScheduleEntryType -eq "Inclusion" -and $scheduleViolated -eq $true) 
    { 
        return $false 
    } 
    # If now is within the schedule but we wanted to exclude these times, return false to indicate schedule not satisifed 
    elseif ($Schedule.ScheduleEntryType -eq "Exclusion" -and $scheduleViolated -eq $false) 
    { 
        return $false 
    } 
    # Otherwise, the schedule was satisifed 
    else 
    { 
        return $true 
    } 
} 
 
# Test access to log file, create new name if denied (likely created by another user or process) 
$logPath = $PSScriptRoot + "\trace.log" 
try  
{  
    [IO.File]::OpenWrite($logPath).Close()  
} 
catch  
{ 
    $logSuffix = Get-Date -Format "yyyyMMddhhMMss" 
    $logPath = "$PSScriptRoot\trace-$logSuffix.log" 
} 
 
if($ConfigurationFilePath.Length -eq 0) 
{ 
    $ConfigurationFilePath = $PSScriptRoot + "\Send-TwilioMessage_config.xml" 
} 
 
AppendLog -Message "Script started" 
AppendLog -Message "Running as user [$([Environment]::UserDomainName)\$([Environment]::UserName)]" 
AppendLog -Message "MessageText=[$MessageText]; MessageType=[$MessageType]; SubscriptionID=[$SubscriptionID]; ConfigurationFilePath=[$ConfigurationFilePath]" 
 
try  
{ 
    AppendLog -Message "Reading configuration file" 
    # Check for the expected configuration file 
    if((Test-Path $ConfigurationFilePath-eq $false) 
    { 
        throw "Configuration file not found at expected path: $ConfigurationFilePath" 
    } 
 
    # Read and validate configuration parameters from file 
    [xml]$configFile = Get-Content $ConfigurationFilePath 
 
    if($configFile -eq $null -or $configFile.Settings -eq $null -or $configFile.Settings.Twilio -eq $null) 
    { 
        throw "Error reading the configuration file $ConfigurationFilePath. Verify the format is correct." 
    } 
 
    if($configFile.Settings.Twilio.AccountSID.Length -eq 0) 
    { 
        throw "No value defined for AccountSID in configuration file" 
    } 
 
    if($configFile.Settings.Twilio.AuthToken.Length -eq 0) 
    { 
        throw "No value defined for AuthToken in configuration file" 
    } 
 
    if($configFile.Settings.Twilio.SenderPhoneNumber.Length -eq 0) 
    { 
        throw "No value defined for SenderPhoneNumber in configuration file" 
    } 
 
    if($configFile.Settings.Twilio.APIFilesPath.Length -eq 0) 
    { 
        $configFile.Settings.Twilio.APIFilesPath = "." 
    } 
 
    # Check for Twilio API library files 
    AppendLog -Message "Loading Twilio API library" 
    $libraryFilesPath = $configFile.Settings.Twilio.APIFilesPath.TrimEnd('\') 
    if($libraryFilesPath -eq ".") 
    { 
        # If "." path configured, expect files in the same folder as script 
        $libraryFilesPath = $PSScriptRoot 
    } 
 
    $libraryFileList = "Twilio.Api.dll","RestSharp.dll" 
    foreach($fileName in $libraryFileList) 
    { 
        $filePath = $libraryFilesPath + "\" + $fileName 
        if((Test-Path $filePath-eq $false) 
        { 
            throw "Required API file $fileName not found at expected path $libraryFilesPath" 
        } 
    } 
 
    # Load Twilio .NET API library 
    Add-Type -Path ($libraryFilesPath + "\" + "Twilio.Api.dll") 
 
    # Create Twilio client object 
    $twilioClient = New-Object Twilio.TwilioRestClient($configFile.Settings.Twilio.AccountSID, $configFile.Settings.Twilio.AuthToken) 
 
    # Verify access to account 
    AppendLog -Message "Validating Twilio account" 
    $accountTest = $twilioClient.GetAccount() 
    if($accountTest -eq  $null -or $accountTest.Sid.Length -eq 0) 
    { 
        $errorMessage = "Failed to access Twilio account. Validate the account SID and auth token in the configuration file match the API Credentials shown at https://www.twilio.com/user/account." 
        if($accountTest.RestException -ne $null -and $accountTest.RestException.Message.Length -gt 0) 
        { 
            $errorMessage +" Details: " + $accountTest.RestException.Message 
        } 
        throw $errorMessage 
    } 
 
    # Load Operations Manager PowerShell module 
    AppendLog -Message "Loading Operations Manager PowerShell module" 
    Import-Module OperationsManager 
    if((Get-Module OperationsManager) -eq $null) 
    { 
        # If not loaded by name, try the path to module from Registry 
        $modulePath = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\System Center Operations Manager\12\Setup\Powershell\V2" | select -ExpandProperty InstallDirectory 
        $modulePath +"\OperationsManager\OperationsManager.psd1" 
        Import-Module $modulePath 
    } 
    if((Get-Module OperationsManager) -eq $null) 
    { 
        # Not found by either method. Throw error and exit. 
        throw "Failed to load PowerShell module for System Center Operations Manager"  
    } 
 
    # Get the notifcation subscription specified as parameter. Try both name and GUID. 
    AppendLog -Message "Retrieving subscription and recipient details" 
    $subscription = Get-SCOMNotificationSubscription | where {$_.Id -eq $SubscriptionID -or $_.DisplayName -eq $SubscriptionID} 
 
    # Validate subscription 
    if($subscription -eq $null) 
    { 
        throw "No notification subscription found with ID [$SubscriptionID]" 
    } 
    AppendLog -Message "Found subscription [$($subscription.DisplayName)]" 
 
    # Get list of recipients to subscription 
    $recipientList = $subscription.ToRecipients + $subscription.CcRecipients + $subscription.BccRecipients 
 
    # Validate recipient list 
    if($recipientList -eq $null -or $recipientList.Count -eq 0) 
    { 
        throw "No recipients found for subscription with ID [$SubscriptionID]" 
    } 
    AppendLog -Message "Found recipients: [$(($recipientList | select -ExpandProperty Name) -join ",")]" 
 
    # Get phone number list for recipients 
    $phoneList = @() 
    foreach($recipient in $recipientList) 
    { 
        $smsAddress = $recipient.Devices | where Protocol -eq SMS 
        if($smsAddress -ne $null -and (CheckSubscriberSchedules -Subscriber $recipient -Address $smsAddress-eq $false) 
        { 
            AppendLog -Message "Schedules for $($recipient.Name) do not allow SMS/Voice notifications at current time" 
        } 
 
        $phoneNumber = $smsAddress.Address 
        if($phoneNumber.Length -gt 0) 
        { 
            $phoneList +$phoneNumber 
        } 
        else 
        { 
            AppendLog -Message "No phone number configured for recipient: $($recipient.Name)" 
        } 
    } 
 
    # Validate phone list 
    if($phoneList.Count -eq 0) 
    { 
        throw "No phone numbers found for recipients of subscription with ID [$SubscriptionID]" 
    } 
    AppendLog -Message "Found phone numbers: [$($phoneList -join ",")]" 
 
    # Truncate message to fit within SMS limit 
    $MESSAGE_CHARACTER_LIMIT = 160 
    if($MessageText.Length -gt ($MESSAGE_CHARACTER_LIMIT - 3)) 
    { 
        $MessageText = $MessageText.Substring(0, $MESSAGE_CHARACTER_LIMIT - 3) + "..." 
    } 
 
    # Send message(s) via the specified method 
    AppendLog -Message "Sending messages" 
    $errorList = @() 
    foreach($recipient in $phoneList) 
    { 
        switch($MessageType) 
        { 
            "SMS" { 
                # Send SMS message using supplied message text 
                $sendResult = $twilioClient.SendSmsMessage($configFile.Settings.Twilio.SenderPhoneNumber, $recipient$MessageText) 
            } 
     
            "Voice" { 
                # Build voice message URL 
                [Reflection.Assembly]::LoadWithPartialName("System.Web"| Out-Null 
                $encodedText = [System.Web.HttpUtility]::UrlEncode($MessageText) 
                $messageURL = "http://twimlets.com/message?Message%5B0%5D=" + $encodedText 
 
                # Send voice message using supplied message text 
                $sendResult = $twilioClient.InitiateOutboundCall($configFile.Settings.Twilio.SenderPhoneNumber, $recipient$messageURL) 
            } 
        } 
 
        # Check results 
        if($sendResult -eq $null -or $sendResult.Status.Length -eq 0) 
        { 
            $errorMessage = "Failed to send message." 
            if($sendResult.RestException -ne $null -and $sendResult.RestException.Message.Length -gt 0) 
            { 
                $errorMessage +" Exception details: Status=[" + $sendResult.RestException.Status + "], Message=[" + $sendResult.RestException.Message + "]" 
            } 
            $errorList +$errorMessage 
        } 
        else 
        { 
            AppendLog -Message "Successfully sent message to [$recipient]" 
        } 
    } 
 
    # Validate results 
    if($errorList.Count -gt 0) 
    { 
       $errorString = $errorList -join ";" 
       throw "Encountered $($errorList.Count) failures while sending messages. Details: [$errorString]" 
    } 
} 
catch  
{ 
    AppendLog -Message "ERROR: $($error[0].Exception.Message)" 
} 
finally 
{ 
    AppendLog -Message "Script finished" 
}