Msteams webhook ()

* msteams: MS_TEAMS_INBOUNDWEBHOOK_URL MS_TEAMS_APIKEY for msteams webhook

* msteams(webhook)

* msteams: escape backslashes (SCOM) in example-payload.json.j2

* msteams webhook: use list not a tuple.

* msteams webhook: comment tests that need mocking

* msteams webhook: tests: only send Content-Type header.
This commit is contained in:
Jarno Huuskonen 2020-02-25 23:59:30 +02:00 committed by GitHub
parent 58023cc05d
commit 728e39e5cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 275 additions and 14 deletions

View file

@ -71,6 +71,7 @@ Webhooks
* [Grafana](https://github.com/alerta/alerta/blob/master/alerta/webhooks/grafana.py)
* [Graylog](https://github.com/alerta/alerta/blob/master/alerta/webhooks/graylog.py)
* [Mailgun](webhooks/mailgun)
* [MS Teams](webhooks/msteams)
* [New Relic](https://github.com/alerta/alerta/blob/master/alerta/webhooks/newrelic.py)
* [PagerDuty](https://github.com/alerta/alerta/blob/master/alerta/webhooks/pagerduty.py)
* [Pingdom](https://github.com/alerta/alerta/blob/master/alerta/webhooks/pingdom.py)

View file

@ -56,6 +56,8 @@ class SendConnectorCardMessage(PluginBase):
MS_TEAMS_SUMMARY_FMT = self.get_config('MS_TEAMS_SUMMARY_FMT', default=None, type=str, **kwargs) # Message summary(title) format
MS_TEAMS_TEXT_FMT = self.get_config('MS_TEAMS_TEXT_FMT', default=None, type=str, **kwargs) # Message text format
MS_TEAMS_PAYLOAD = self.get_config('MS_TEAMS_PAYLOAD', default=None, type=str, **kwargs) # json/Jinja2 MS teams messagecard payload
MS_TEAMS_INBOUNDWEBHOOK_URL = self.get_config('MS_TEAMS_INBOUNDWEBHOOK_URL', default=None, type=str, **kwargs) # webhook url for connectorcard actions
MS_TEAMS_APIKEY = self.get_config('MS_TEAMS_APIKEY', default=None, type=str, **kwargs) # X-API-Key (needs webhook.write permission)
DASHBOARD_URL = self.get_config('DASHBOARD_URL', default='', type=str, **kwargs)
if alert.repeat:
@ -71,6 +73,11 @@ class SendConnectorCardMessage(PluginBase):
'url': url
}
if MS_TEAMS_INBOUNDWEBHOOK_URL and MS_TEAMS_APIKEY:
# Add X-API-Key header for teams(webhook) HttpPOST actions
template_vars['headers'] = '[ {{ "name": "X-API-Key", "value": "{}" }} ]'.format(MS_TEAMS_APIKEY)
template_vars['webhook_url'] = MS_TEAMS_INBOUNDWEBHOOK_URL
if MS_TEAMS_PAYLOAD:
# Use "raw" json ms teams message card format
payload_template = self._load_template(MS_TEAMS_PAYLOAD)

View file

@ -1,43 +1,50 @@
{
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": "{{ alert.status|capitalize }} {{ alert.resource }} / {{ alert.service|join(',') }}",
"summary": "{{ alert.status|capitalize }} {{ alert.resource|replace('\\', '\\\\') }} / {{ alert.service|join(',')|replace('\\', '\\\\') }}",
"themeColor": "{{ color }}",
"title": "[{{ alert.status|capitalize }}] {{ alert.environment }} {{ alert.service|join(',') }} {{ alert.severity|upper }} - {{ alert.event }} on {{ alert.resource }}",
"title": "[{{ alert.status|capitalize }}] {{ alert.environment }} {{ alert.service|join(',')|replace('\\', '\\\\') }} {{ alert.severity|upper }} - {{ alert.event }} on {{ alert.resource|replace('\\', '\\\\') }}",
"sections": [
{
"facts": [
{
"name": "Resource:",
"value": "**{{ alert.resource }}**{% if alert.attributes['hostaddress'] or alert.attributes['hostaddress6'] %} ({{ alert.attributes['hostaddress'] }}{% if alert.attributes['hostaddress6'] %} / {{ alert.attributes['hostaddress6'] }}{% endif %}){% endif %}"
"value": "**{{ alert.resource|replace('\\', '\\\\') }}**{% if alert.attributes['hostaddress'] or alert.attributes['hostaddress6'] %} ({{ alert.attributes['hostaddress'] }}{% if alert.attributes['hostaddress6'] %} / {{ alert.attributes['hostaddress6'] }}{% endif %}){% endif %}"
},
{
"name": "Service(s):",
"value": "**{{ alert.service|join(',') }}**"
"value": "**{{ alert.service|join(',')|replace('\\', '\\\\') }}**"
}
{%- if alert.origin %}
,{
"name": "Origin:",
"value": "{{ alert.origin }}"
"value": "{{ alert.origin|replace('\\', '\\\\') }}"
}{% endif %}
]
}
{%- if (alert.status and alert.status == 'open') and alert.text %}
,{
"text": "```{{ alert.text }}```"
"text": "```{{ alert.text|replace('\\', '\\\\') }}```"
}{% endif %}
]
{%- if alert.status and alert.status in [ 'open', 'ack', 'assign' ] %}
,"potentialAction": [
{%- if headers and actions %}
{%- for act in actions %}
{%- if headers and webhook_url %}
{%- for act in [ 'ack', 'close' ] %}
{
"@type": "{{ act['type'] }}",
"name": "{{ act['name'] }}",
"target": "{{ act['target'] }}",
"@type": "HttpPOST",
"name": "{{ act|capitalize }}",
"target": "{{ webhook_url }}",
"headers": {{ headers }},
"body": {{ act['body'] }}
},{% endfor %}{% endif %}
"body": "{ \"action\": \"{{ act }}\", \"alert_id\": \"{{ alert.id }}\" }"
},{% endfor %}
{
"@type": "HttpPOST",
"name": "Blackout",
"target": "{{ webhook_url }}",
"headers": {{ headers }},
"body": "{ \"action\": \"blackout\", \"environment\": \"{{ alert.environment }}\", \"resource\": \"{{ alert.resource|replace('\\', '\\\\') }}\", \"event\": \"{{ alert.event }}\" }"
},{% endif %}
{
"@type": "OpenUri",
"name": "View in Alerta",

View file

@ -1,7 +1,7 @@
from setuptools import setup, find_packages
version = '5.2.0'
version = '5.2.1'
setup(
name="alerta-msteams",

View file

@ -0,0 +1,58 @@
MS Teams Webhook
==============
Receive HttpPOST ack,close and blackout actions via webhook callbacks.
For help, join [![Gitter chat](https://badges.gitter.im/alerta/chat.png)](https://gitter.im/alerta/chat)
Installation
------------
Clone the GitHub repo and run:
$ python setup.py install
Or, to install remotely from GitHub run:
$ pip install git+https://github.com/alerta/alerta-contrib.git#subdirectory=webhooks/msteams
Note: If Alerta is installed in a python virtual environment then plugins
need to be installed into the same environment for Alerta to dynamically
discover them.
Configuration
-------------
The custom webhook will be auto-detected and added to the list of available API endpoints.
- Create apikey with _write:webhooks_ scope.
- Put apikey to `MS_TEAMS_APIKEY` config.
- Configure `MS_TEAMS_INBOUNDWEBHOOK_URL` to point to your alerta `/webhooks/msteams` url.
- Configure msteams [plugin](https://github.com/alerta/alerta-contrib/tree/master/plugins/msteams) `MS_TEAMS_PAYLOAD` to add potentialaction buttons to msteams
alerts. (Example in [example-payload.json.j2](../../plugins/msteams/example-payload.json.j2)
Example Request
--------------
```plain
curl -sSL -X POST -H 'Content-Type: application/json' \
-H 'X-API-Key: <API_KEY>' \
-d \
'
{
"action": "ack",
"alert_id": "32adb117-0045-4626-ad89-a258db98133f"
}
' \
'http://localhost:8080/api/webhooks/msteams'
```
References
----------
* MS Teams message card reference: https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference#httppost-action
License
-------
Copyright (c) 2019 Jarno Huuskonen. Available under the MIT License.

View file

@ -0,0 +1,71 @@
from flask import current_app, g, jsonify, request, make_response
from alerta.models.alert import Alert
from alerta.models.blackout import Blackout
from alerta.utils.audit import write_audit_trail
from alerta.webhooks import WebhookBase
from uuid import UUID
class MsteamsWebhook(WebhookBase):
def incoming(self, query_string, payload):
# Note: This doesn't validate MS Teams Authorization: Bearer JWT
# instead we're relying on alerta to validate X-API-Key header
action = payload.get('action', 'missing')
if action not in [ 'ack', 'close', 'blackout' ]:
resp = make_response(jsonify(status='error', message='Invalid action'), 400)
return resp
if action in [ 'ack', 'close' ]:
alert_id = payload.get('alert_id', None)
err = make_response(jsonify(status='error', message='Missing/invalid alert_id'), 400)
if not alert_id:
return err
try:
# check that alert_id looks like uuid
uuidval = UUID(alert_id, version=4)
if str(uuidval) != alert_id.lower():
return err
except Exception:
return err
alert = Alert.find_by_id(alert_id, customers=g.get('customers', None))
if not alert:
return err
else:
alert.set_status(status=action, text='status changed via MS Teams webhook')
resp = make_response(jsonify(status='ok', message='status changed'), 200)
resp.headers['CARD-ACTION-STATUS'] = 'Alert {}d'.format(action.capitalize())
text = 'alert updated via msteams webhook'
write_audit_trail.send(current_app._get_current_object(), event='webhook-updated', message=text,
user=g.login, customers=g.customers, scopes=g.scopes, resource_id=alert.id,
type='alert', request=request)
elif action == 'blackout':
environment = payload.get('environment', None)
resource = payload.get('resource', None)
event = payload.get('event', None)
if environment and resource and event:
duration = payload.get('duration', None) or current_app.config['BLACKOUT_DURATION']
try:
if not duration or float(duration) < 0.0:
# Should not happen: set default duration
duration = 3600
except ValueError:
# Should not happen: set default duration
duration = 3600
blackout = Blackout(environment, resource=resource, event=event, duration=duration)
blackout.create()
resp = make_response(jsonify(status='ok', message='blackout created'), 201)
resp.headers['CARD-ACTION-STATUS'] = 'Blackout created for {0:.1f} hours'.format(float(duration) / 3600)
else:
# Missging env, resource or event
resp = make_response(jsonify(status='error', message='Missing blackout params'), 412)
return resp

24
webhooks/msteams/setup.py Normal file
View file

@ -0,0 +1,24 @@
from setuptools import setup, find_packages
version = '5.0.0'
setup(
name="alerta-msteamswebhook",
version=version,
description='Alerta webhook for MS Teams',
url='https://github.com/alerta/alerta-contrib',
license='MIT',
author='Jarno Huuskonen',
author_email='jjh74@users.noreply.github.com',
packages=find_packages(),
py_modules=['alerta_msteamswebhook'],
install_requires=[
],
include_package_data=True,
zip_safe=True,
entry_points={
'alerta.webhooks': [
'msteams = alerta_msteamswebhook:MsteamsWebhook'
]
}
)

View file

@ -0,0 +1,93 @@
import json
import unittest
from alerta.app import create_app, custom_webhooks
from uuid import uuid4
import alerta_msteamswebhook
class MsteamsWebhookTestCase(unittest.TestCase):
def setUp(self):
test_config = {
'TESTING': True,
'AUTH_REQUIRED': False
}
self.app = create_app(test_config)
self.client = self.app.test_client()
custom_webhooks.webhooks['msteams'] = alerta_msteamswebhook.MsteamsWebhook()
self.headers = {
'Content-Type': 'application/json'
}
self.alert_id = 'f0c55228-c61d-462a-9aeb-f6048d37fdf6'
def test_msteamswebhook(self):
payload_cmd = """
{
"action": "%s",
"alert_id": "%s"
}
"""
# Missing alert_id
payload_invalidcmd = """
{
"action": "%s"
}
"""
payload_blackout = r"""
{
"action": "blackout",
"environment": "Production",
"resource": "webhooktest1",
"event": "DiskUtilHigh"
}
"""
# ack
# TODO: needs to mock Alert.find_by_id / alert.set_status
#response = self.client.post('/webhooks/msteams', data=payload_cmd % ('ack', self.alert_id), content_type='application/json', headers=self.headers)
#self.assertEqual(response.status_code, 200)
#data = json.loads(response.data.decode('utf-8'))
#self.assertEqual(data['status'], 'ok')
#self.assertEqual(data['message'], 'status changed')
#self.assertTrue(bool(response.headers.get('CARD-ACTION-STATUS', False)))
# ack with missing alert_id
response = self.client.post('/webhooks/msteams', data=payload_invalidcmd % 'ack', content_type='application/json', headers=self.headers)
self.assertEqual(response.status_code, 400)
data = json.loads(response.data.decode('utf-8'))
self.assertEqual(data['status'], 'error')
self.assertEqual(data['message'], 'Missing/invalid alert_id')
# ack with bogus alert_id
response = self.client.post('/webhooks/msteams', data=payload_cmd % ('ack', '7a0e3ee1-fbaa-45th-isis-bogus'), content_type='application/json', headers=self.headers)
self.assertEqual(response.status_code, 400)
data = json.loads(response.data.decode('utf-8'))
self.assertEqual(data['status'], 'error')
self.assertEqual(data['message'], 'Missing/invalid alert_id')
# close alert
# TODO: needs to mock Alert.find_by_id / alert.set_status
#response = self.client.post('/webhooks/msteams', data=payload_cmd % ('close', self.alert_id), content_type='application/json', headers=self.headers)
#self.assertEqual(response.status_code, 200)
#data = json.loads(response.data.decode('utf-8'))
#self.assertEqual(data['status'], 'ok')
#self.assertEqual(data['message'], 'status changed')
#self.assertTrue(bool(response.headers.get('CARD-ACTION-STATUS', False)))
# create blackout
# TODO: needs to mock: blackout.create()
#response = self.client.post('/webhooks/msteams', data=payload_blackout, content_type='application/json', headers=self.headers)
#self.assertEqual(response.status_code, 201)
#data = json.loads(response.data.decode('utf-8'))
#self.assertEqual(data['status'], 'ok')
#self.assertEqual(data['message'], 'blackout created')
#self.assertTrue(bool(response.headers.get('CARD-ACTION-STATUS', False)))