Sunday, September 1, 2019

Send Active vSphere Alarms to Microsoft Teams

One of the ways that we have been driving adoption of Microsoft Teams at work is by automatically pushing useful content into channels via its webhook support.

I've pushed alarms from our general monitoring system into Teams and it removes the limitation of short text messages while providing more details on an issue before resorting to using the corporate VPN.  I simply arm myself with the Microsoft Teams app on my phone.

I thought about what else might benefit from the ability to see details while on the go and came up with active vSphere alarms.  I wanted to be able to get details on active alarms, the time they started, their status (severity, if you ask me) and the objects and clusters involved.

I started playing around with PowerShell and soon had rudimentary alarm data going into a Microsoft Teams channel.  But what if I am or can be connected to work? 

I added a button to open the vSphere Client and then an alarm-specific button that opens the vSphere Client summary page for the object: 

Example vSphere Alarms Alert
Setting it all up is pretty straightforward: create an incoming webhook connector in Microsoft Teams, save the URL, and schedule the PowerShell to run at a frequency of your choosing.

Adding a Webhook

Open Microsoft Teams, choose or create a new Team, choose or create a new channel within that Team, and then select the three dots to the right of the channel name to create a connector.  The screenshots below will help lead you along, but it's pretty simple.  Be sure to save the link that is generated for your connector.  You can always go back and manage the configured connectors on your channel if you need to get the URL again.






Scheduling

Save the PowerShell at the bottom of this article and save it on a server somewhere so that you can set it up as a Scheduled Task.  Invocation is easy, and you will need to provide your vCenter FQDN and Microsoft Teams webhook URL as part of the program/scripts arguments:

-ExecutionPolicy Bypass -file Send-vSphereAlarms.ps1 -vcenter vcenter-fqdn -TeamsUri uri

I like to configure the task to run every 10 minutes, but pick what works best for you and your environment.  

I'm certain that a lot can be changed in the script. Play around and enjoy!

Send-vSphereAlarms.ps1


<#
.SYNOPSIS
Post active vSphere alarms to a Microsoft Teams channel.
.DESCRIPTION
Connect to the specified vSphere vCenter, query for all active alarms, and post those
as a card to Microsoft Teams via a Webhook.
The card will contain one section per alarm with a few facts: the cluster involved,
the date and time of the alarm, the alarm status (yellow/red/etc), and whether
or not the alarm has been acknowledged.
The card will also have a button to visit the vCenter UI while each (alarm) section
has a button that goes directly to that object's summary tab in vCenter.
Hat tip to @ericblee6, who pointed me toward @TheLazyAdmin's article below. That gave
me a new way to form the collections and format the webhook data.
https://www.thelazyadministrator.com/2018/12/11/post-inactive-users-as-a-microsoft-teams-message-with-powershell/
.PARAMETER vCenter
The vCenter server to be queried and linked to.
.PARAMETER TeamsUri
Your Microsoft Teams WebHook URI. To get this, add a Connector to a Teams Channel and
copy the provided URL.
#>
param (
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$vCenter,
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][uri]$TeamsUri
)
# Indicators found in MoRef for types of resources.
$clusterInd = "ClusterComputeResource-"
$hostInd = "HostSystem-"
$vmInd = "VirtualMachine-"
# Return active alarms from vCenter
function Get-Alarms() {
[CmdletBinding()]
param (
[string]$vCenter
)
try {
if ($global:DefaultVIServer.ServiceUri.Host -ne $vCenter) {
Connect-VIServer $vCenter | Out-Null
}
# we need the Uuid for direct-reference vCenter URLs
$Script:vcUuid = $global:DefaultVIServer.InstanceUuid.ToUpper()
}
catch {
Write-Output "Failed to connect to $vCenter"
return $null
}
$Clusters = Get-View -ViewType ComputeResource -Property Name, OverallStatus, TriggeredAlarmstate
$report = @()
$AlarmClusters = $Clusters | Where-Object { $null -ne $_.TriggeredAlarmState }
foreach ($ac in $AlarmClusters) {
foreach ($ta in $ac.TriggeredAlarmstate) {
$object = [PSCustomObject]@{
'AlarmTime' = (($ta.Time).ToLocalTime()).ToString()
'AlarmStatus' = $ta.OverallStatus.ToString()
'AlarmAcked' = $ta.Acknowledged
'Cluster' = $ac.Name
'ImageUrl' = ''
'Entity' = ''
'Type' = ''
'MoRef' = $ta.MoRef
'TriggeredAlarms' = (Get-AlarmDefinition -Id $ta.Alarm.ToString()).Name
}
$entity = $ta.Entity.ToString()
if ($entity -like "$($clusterInd)*") {
$object.Entity = $ac.Name
$object.Type = "Cluster"
$object.MoRef = $ac.MoRef
$object.ImageUrl = "https://i.imgur.com/iq2BkLQ.png"
}
elseif ($entity -like "$($hostInd)*") {
$vmHost = Get-VMHost -Id $entity
$object.Entity = $vmHost.Name
$object.Type = "VMHost"
$object.MoRef = $entity
$object.ImageUrl = "https://i.imgur.com/k22bhCD.png"
}
elseif ($entity -like "$($vmInd)*") {
$vm = Get-VM -Id $entity
$object.Entity = $vm.Name
$object.Type = "VM"
$object.MoRef = $entity
$object.ImageUrl = "https://i.imgur.com/Y0ShH0n.png"
}
$report += $object
}
}
return $report
}
# Grab the alarms
Write-Output "Querying active vSphere alarms from $vcenter"
$Alarms = New-Object System.Collections.Generic.List[System.Object]
Get-Alarms($vcenter) |
Sort-Object Cluster, Time, Entity |
ForEach-Object {
# throw into a collection consumable by Teams
$Alarms.add($_)
}
# Build the card sections, one per alarm. Stuff them into a collection
# consumable by Teams
$Sections = New-Object System.Collections.Generic.List[System.Object]
foreach ($a in $Alarms) {
# Determine query string that lands us on our object in vCenter. It uses the MoRef, but
# with the first "-" replaced and the vCenter UUID
$qs = "#?extensionId=vsphere.core.vm.summary&objectId=urn:vmomi:$($a.MoRef):$($vcUuid)"
$qs = $qs -replace "(.*:vmomi:[a-z]+)-(.*)", '$1:$2' # adjust formatting
$s = @{
activityTitle = $a.Entity
activitySubtitle = $a.Type
activityText = $a.TriggeredAlarms
activityImage = $a.ImageUrl
activityImageType = "article" # avoid rounded corner icons
potentialAction = @(
@{
'@type' = "OpenUri"
name = "Visit Object"
targets = @(
@{
"os" = "default"
"uri" = "https://$($vcenter)/ui/$($qs)"
}
)
}
)
facts = @(
@{
name = 'Cluster'
value = $a.Cluster
}
@{
name = 'Alarm Time'
value = $a.AlarmTime
}
@{
name = 'Alarm Status'
value = $a.AlarmStatus
}
@{
name = 'Alarm Acked'
value = $a.AlarmAcked
}
)
}
$Sections.Add($s)
}
# Post it if you got it
$count = $Sections.Count
if ($count -gt 0) {
$text = "There $(if ($count -gt 1) {'are'} else {'is'}) $count active alarm$(if ($count -gt 1) {'s'} else {''}) at $(Get-Date)"
Write-Output $text
$body = ConvertTo-Json -Depth 8 @{
title = "Active vSphere Alarms via $($vCenter)"
text = $text
sections = $Sections
potentialAction = @(
@{
'@context' = 'http://schema.org'
'@type' = "ViewAction"
name = "vCenter"
target = @("https://$($vcenter)/ui/")
}
)
}
Write-Output "Posting $($count) sections to WebHook $($TeamsUri)"
Invoke-RestMethod -Uri $TeamsUri -Method Post -Body $body -ContentType 'application/json'
}