Msteams webhook (#299)
* 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:
parent
58023cc05d
commit
728e39e5cd
8 changed files with 275 additions and 14 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
version = '5.2.0'
|
||||
version = '5.2.1'
|
||||
|
||||
setup(
|
||||
name="alerta-msteams",
|
||||
|
|
58
webhooks/msteams/README.md
Normal file
58
webhooks/msteams/README.md
Normal file
|
@ -0,0 +1,58 @@
|
|||
MS Teams Webhook
|
||||
==============
|
||||
|
||||
Receive HttpPOST ack,close and blackout actions via webhook callbacks.
|
||||
|
||||
For help, join [](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.
|
71
webhooks/msteams/alerta_msteamswebhook.py
Normal file
71
webhooks/msteams/alerta_msteamswebhook.py
Normal 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
24
webhooks/msteams/setup.py
Normal 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'
|
||||
]
|
||||
}
|
||||
)
|
93
webhooks/msteams/test_msteamswebhook.py
Normal file
93
webhooks/msteams/test_msteamswebhook.py
Normal 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)))
|
||||
|
Loading…
Add table
Reference in a new issue