diff --git a/zerver/fixtures/pagerduty/pagerduty_acknowledge.json b/zerver/fixtures/pagerduty/pagerduty_acknowledge.json new file mode 100644 index 0000000000..26ff71a7ee --- /dev/null +++ b/zerver/fixtures/pagerduty/pagerduty_acknowledge.json @@ -0,0 +1,162 @@ +{ + "messages": [ + { + "created_on": "2015-02-07T21:09:49Z", + "data": { + "incident": { + "acknowledgers": [ + { + "at": "2015-02-07T21:09:49Z", + "object": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski", + "type": "user" + } + } + ], + "assigned_to": [ + { + "at": "2015-02-07T21:08:36Z", + "object": { + "email": "armooo+2@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/PJ9ODL1", + "id": "PJ9ODL1", + "name": "Armooo2", + "type": "user" + } + }, + { + "at": "2015-02-07T21:09:49Z", + "object": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski", + "type": "user" + } + } + ], + "assigned_to_user": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski" + }, + "created_on": "2015-02-07T21:05:47Z", + "escalation_policy": { + "deleted_at": null, + "id": "PUIGL4T", + "name": "Default" + }, + "html_url": "https://zulip-test.pagerduty.com/incidents/PO1XIJ5", + "id": "PO1XIJ5", + "incident_key": "It is on fire", + "incident_number": 1, + "last_status_change_by": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski" + }, + "last_status_change_on": "2015-02-07T21:09:49Z", + "number_of_escalations": 0, + "service": { + "deleted_at": null, + "html_url": "https://zulip-test.pagerduty.com/services/PIL5CUQ", + "id": "PIL5CUQ", + "name": "Test service" + }, + "status": "acknowledged", + "trigger_details_html_url": "https://zulip-test.pagerduty.com/incidents/PO1XIJ5/log_entries/Q01ZH27LNRIAUB", + "trigger_summary_data": { + "subject": "It is on fire" + }, + "trigger_type": "email_trigger" + } + }, + "id": "a710cfd0-af0d-11e4-8aca-f23c91739642", + "type": "incident.assign" + }, + { + "created_on": "2015-02-07T21:09:49Z", + "data": { + "incident": { + "acknowledgers": [ + { + "at": "2015-02-07T21:09:49Z", + "object": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski", + "type": "user" + } + } + ], + "assigned_to": [ + { + "at": "2015-02-07T21:08:36Z", + "object": { + "email": "armooo+2@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/PJ9ODL1", + "id": "PJ9ODL1", + "name": "Armooo2", + "type": "user" + } + }, + { + "at": "2015-02-07T21:09:49Z", + "object": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski", + "type": "user" + } + } + ], + "assigned_to_user": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski" + }, + "created_on": "2015-02-07T21:05:47Z", + "escalation_policy": { + "deleted_at": null, + "id": "PUIGL4T", + "name": "Default" + }, + "html_url": "https://zulip-test.pagerduty.com/incidents/PO1XIJ5", + "id": "PO1XIJ5", + "incident_key": "It is on fire", + "incident_number": 1, + "last_status_change_by": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski" + }, + "last_status_change_on": "2015-02-07T21:09:49Z", + "number_of_escalations": 0, + "service": { + "deleted_at": null, + "html_url": "https://zulip-test.pagerduty.com/services/PIL5CUQ", + "id": "PIL5CUQ", + "name": "Test service" + }, + "status": "acknowledged", + "trigger_details_html_url": "https://zulip-test.pagerduty.com/incidents/PO1XIJ5/log_entries/Q01ZH27LNRIAUB", + "trigger_summary_data": { + "subject": "It is on fire" + }, + "trigger_type": "email_trigger" + } + }, + "id": "a7169c30-af0d-11e4-8aca-f23c91739642", + "type": "incident.acknowledge" + } + ] +} diff --git a/zerver/fixtures/pagerduty/pagerduty_auto_resolved.json b/zerver/fixtures/pagerduty/pagerduty_auto_resolved.json new file mode 100644 index 0000000000..6b3ad564ef --- /dev/null +++ b/zerver/fixtures/pagerduty/pagerduty_auto_resolved.json @@ -0,0 +1,41 @@ +{ + "messages": [ + { + "created_on": "2015-02-07T21:29:42Z", + "data": { + "incident": { + "assigned_to": [], + "assigned_to_user": null, + "created_on": "2015-02-07T21:19:42Z", + "escalation_policy": { + "deleted_at": null, + "id": "PUIGL4T", + "name": "Default" + }, + "html_url": "https://zulip-test.pagerduty.com/incidents/PX7K9J2", + "id": "PX7K9J2", + "incident_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "incident_number": 2, + "last_status_change_by": null, + "last_status_change_on": "2015-02-07T21:29:42Z", + "number_of_escalations": 0, + "resolved_by_user": null, + "service": { + "deleted_at": null, + "html_url": "https://zulip-test.pagerduty.com/services/PIL5CUQ", + "id": "PIL5CUQ", + "name": "Test service" + }, + "status": "resolved", + "trigger_details_html_url": "https://zulip-test.pagerduty.com/incidents/PX7K9J2/log_entries/Q2LOVDWN1FGDPC", + "trigger_summary_data": { + "subject": "new" + }, + "trigger_type": "web_trigger" + } + }, + "id": "6e0d4b20-af10-11e4-8aca-f23c91739642", + "type": "incident.resolve" + } + ] +} diff --git a/zerver/fixtures/pagerduty/pagerduty_resolved.json b/zerver/fixtures/pagerduty/pagerduty_resolved.json new file mode 100644 index 0000000000..46e36aa8f1 --- /dev/null +++ b/zerver/fixtures/pagerduty/pagerduty_resolved.json @@ -0,0 +1,51 @@ +{ + "messages": [ + { + "created_on": "2015-02-07T21:31:53Z", + "data": { + "incident": { + "assigned_to": [], + "assigned_to_user": null, + "created_on": "2015-02-07T21:05:47Z", + "escalation_policy": { + "deleted_at": null, + "id": "PUIGL4T", + "name": "Default" + }, + "html_url": "https://zulip-test.pagerduty.com/incidents/PO1XIJ5", + "id": "PO1XIJ5", + "incident_key": "It is on fire", + "incident_number": 1, + "last_status_change_by": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski" + }, + "last_status_change_on": "2015-02-07T21:31:53Z", + "number_of_escalations": 0, + "resolved_by_user": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski" + }, + "service": { + "deleted_at": null, + "html_url": "https://zulip-test.pagerduty.com/services/PIL5CUQ", + "id": "PIL5CUQ", + "name": "Test service" + }, + "status": "resolved", + "trigger_details_html_url": "https://zulip-test.pagerduty.com/incidents/PO1XIJ5/log_entries/Q01ZH27LNRIAUB", + "trigger_summary_data": { + "subject": "It is on fire" + }, + "trigger_type": "email_trigger" + } + }, + "id": "bc3d9ed0-af10-11e4-947f-22000ad9bf74", + "type": "incident.resolve" + } + ] +} diff --git a/zerver/fixtures/pagerduty/pagerduty_trigger.json b/zerver/fixtures/pagerduty/pagerduty_trigger.json new file mode 100644 index 0000000000..7322036cbe --- /dev/null +++ b/zerver/fixtures/pagerduty/pagerduty_trigger.json @@ -0,0 +1,56 @@ +{ + "messages": [ + { + "created_on": "2015-02-07T21:42:52Z", + "data": { + "incident": { + "assigned_to": [ + { + "at": "2015-02-07T21:42:52Z", + "object": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason Michalski", + "type": "user" + } + } + ], + "assigned_to_user": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason Michalski" + }, + "created_on": "2015-02-07T21:42:52Z", + "escalation_policy": { + "deleted_at": null, + "id": "PUIGL4T", + "name": "Default" + }, + "html_url": "https://zulip-test.pagerduty.com/incidents/P140S4Y", + "id": "P140S4Y", + "incident_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "incident_number": 3, + "last_status_change_by": null, + "last_status_change_on": "2015-02-07T21:42:52Z", + "number_of_escalations": 0, + "service": { + "deleted_at": null, + "html_url": "https://zulip-test.pagerduty.com/services/PIL5CUQ", + "id": "PIL5CUQ", + "name": "Test service" + }, + "status": "triggered", + "trigger_details_html_url": "https://zulip-test.pagerduty.com/incidents/P140S4Y/log_entries/Q3P8OPKZDJWLKB", + "trigger_summary_data": { + "subject": "foo" + }, + "trigger_type": "web_trigger" + } + }, + "id": "44df6240-af12-11e4-8e1e-22000ae31361", + "type": "incident.trigger" + } + ] +} diff --git a/zerver/fixtures/pagerduty/pagerduty_unacknowledge.json b/zerver/fixtures/pagerduty/pagerduty_unacknowledge.json new file mode 100644 index 0000000000..6394441326 --- /dev/null +++ b/zerver/fixtures/pagerduty/pagerduty_unacknowledge.json @@ -0,0 +1,56 @@ +{ + "messages": [ + { + "created_on": "2015-02-07T21:53:09Z", + "data": { + "incident": { + "assigned_to": [ + { + "at": "2015-02-07T21:42:52Z", + "object": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski", + "type": "user" + } + } + ], + "assigned_to_user": { + "email": "armooo@zulip.com", + "html_url": "https://zulip-test.pagerduty.com/users/POBCFRJ", + "id": "POBCFRJ", + "name": "Jason MIchalski" + }, + "created_on": "2015-02-07T21:42:52Z", + "escalation_policy": { + "deleted_at": null, + "id": "PUIGL4T", + "name": "Default" + }, + "html_url": "https://zulip-test.pagerduty.com/incidents/P140S4Y", + "id": "P140S4Y", + "incident_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "incident_number": 3, + "last_status_change_by": null, + "last_status_change_on": "2015-02-07T21:53:09Z", + "number_of_escalations": 0, + "service": { + "deleted_at": null, + "html_url": "https://zulip-test.pagerduty.com/services/PIL5CUQ", + "id": "PIL5CUQ", + "name": "Test service" + }, + "status": "triggered", + "trigger_details_html_url": "https://zulip-test.pagerduty.com/incidents/P140S4Y/log_entries/Q3P8OPKZDJWLKB", + "trigger_summary_data": { + "subject": "foo" + }, + "trigger_type": "web_trigger" + } + }, + "id": "b4695980-af13-11e4-8a3a-12313f0a2181", + "type": "incident.unacknowledge" + } + ] +} diff --git a/zerver/test_hooks.py b/zerver/test_hooks.py index 261e0c1b33..e9529dcd75 100644 --- a/zerver/test_hooks.py +++ b/zerver/test_hooks.py @@ -734,3 +734,82 @@ class ZenDeskHookTests(AuthedTestCase): msg = self.generate_webhook_response(message='New comment:\n> It is better\n* here') self.assertEqual(msg.content, 'New comment:\n> It is better\n* here') + +class PagerDutyHookTests(AuthedTestCase): + + def send_webhook(self, data, stream_name): + email = 'hamlet@zulip.com' + self.subscribe_to_stream(email, stream_name) + api_key = self.get_api_key(email) + url = '/api/v1/external/pagerduty?api_key=%s&stream=%s' % (api_key, stream_name) + result = self.client.post(url, ujson.dumps(data), content_type="application/json") + self.assert_json_success(result) + + # Check the correct message was sent + msg = Message.objects.filter().order_by('-id')[0] + self.assertEqual(msg.sender.email, email) + + return msg + + def test_trigger(self): + data = ujson.loads(self.fixture_data('pagerduty', 'trigger')) + msg = self.send_webhook(data, 'pagerduty') + self.assertEqual(msg.subject, 'incident 3') + self.assertEqual( + msg.content, + ':unhealthy_heart: Incident [3](https://zulip-test.pagerduty.com/incidents/P140S4Y) triggered by [Test service](https://zulip-test.pagerduty.com/services/PIL5CUQ) and assigned to [armooo@](https://zulip-test.pagerduty.com/users/POBCFRJ)\n\n>foo' + ) + + def test_unacknowledge(self): + data = ujson.loads(self.fixture_data('pagerduty', 'unacknowledge')) + msg = self.send_webhook(data, 'pagerduty') + self.assertEqual(msg.subject, 'incident 3') + self.assertEqual( + msg.content, + ':unhealthy_heart: Incident [3](https://zulip-test.pagerduty.com/incidents/P140S4Y) unacknowledged by [Test service](https://zulip-test.pagerduty.com/services/PIL5CUQ) and assigned to [armooo@](https://zulip-test.pagerduty.com/users/POBCFRJ)\n\n>foo' + ) + + def test_resolved(self): + data = ujson.loads(self.fixture_data('pagerduty', 'resolved')) + msg = self.send_webhook(data, 'pagerduty') + self.assertEqual(msg.subject, 'incident 1') + self.assertEqual( + msg.content, + ':healthy_heart: Incident [1](https://zulip-test.pagerduty.com/incidents/PO1XIJ5) resolved by [armooo@](https://zulip-test.pagerduty.com/users/POBCFRJ)\n\n>It is on fire' + ) + + def test_auto_resolved(self): + data = ujson.loads(self.fixture_data('pagerduty', 'auto_resolved')) + msg = self.send_webhook(data, 'pagerduty') + self.assertEqual(msg.subject, 'incident 2') + self.assertEqual( + msg.content, + ':healthy_heart: Incident [2](https://zulip-test.pagerduty.com/incidents/PX7K9J2) resolved\n\n>new' + ) + + def test_acknowledge(self): + data = ujson.loads(self.fixture_data('pagerduty', 'acknowledge')) + msg = self.send_webhook(data, 'pagerduty') + self.assertEqual(msg.subject, 'incident 1') + self.assertEqual( + msg.content, + ':average_heart: Incident [1](https://zulip-test.pagerduty.com/incidents/PO1XIJ5) acknowledged by [armooo@](https://zulip-test.pagerduty.com/users/POBCFRJ)\n\n>It is on fire' + ) + + def test_bad_message(self): + data = {'messages': [{'type': 'incident.triggered'}]} + msg = self.send_webhook(data, 'pagerduty') + self.assertEqual(msg.subject, 'pagerduty') + self.assertEqual( + msg.content, + 'Unknown pagerdudy message\n``` py\n{u\'type\': u\'incident.triggered\'}\n```' + ) + + def test_unknown_message_type(self): + data = {'messages': [{'type': 'foo'}]} + msg = self.send_webhook(data, 'pagerduty') + self.assertEqual(msg.subject, 'pagerduty') + self.assertEqual( + msg.content, + 'Unknown pagerdudy message\n``` py\n{u\'type\': u\'foo\'}\n```' + ) diff --git a/zerver/views/webhooks.py b/zerver/views/webhooks.py index e9e56c34c4..0fbf431cd5 100644 --- a/zerver/views/webhooks.py +++ b/zerver/views/webhooks.py @@ -16,6 +16,7 @@ from django.db.models import Q from defusedxml.ElementTree import fromstring as xml_fromstring +import pprint import base64 import logging import re @@ -914,3 +915,108 @@ def api_zendesk_webhook(request, user_profile): check_send_message(user_profile, get_client('ZulipZenDeskWebhook'), 'stream', [stream], subject, message) return json_success() + + +PAGER_DUTY_EVENT_NAMES = { + 'incident.trigger': 'triggered', + 'incident.acknowledge': 'acknowledged', + 'incident.unacknowledge': 'unacknowledged', + 'incident.resolve': 'resolved', + 'incident.assign': 'assigned', + 'incident.escalate': 'escalated', + 'incident.delegate': 'delineated', +} + +def build_pagerdudy_formatdict(message): + # Normalize the message dict, after this all keys will exist. I would + # rather some strange looking messages than dropping pages. + + format_dict = {} + format_dict['action'] = PAGER_DUTY_EVENT_NAMES[message['type']] + + format_dict['incident_id'] = message['data']['incident']['id'] + format_dict['incident_num'] = message['data']['incident']['incident_number'] + format_dict['incident_url'] = message['data']['incident']['html_url'] + + format_dict['service_name'] = message['data']['incident']['service']['name'] + format_dict['service_url'] = message['data']['incident']['service']['html_url'] + + # This key can be missing on null + if message['data']['incident'].get('assigned_to_user', None): + format_dict['assigned_to_email'] = message['data']['incident']['assigned_to_user']['email'] + format_dict['assigned_to_username'] = message['data']['incident']['assigned_to_user']['email'].split('@')[0] + format_dict['assigned_to_url'] = message['data']['incident']['assigned_to_user']['html_url'] + else: + format_dict['assigned_to_email'] = 'nobody' + format_dict['assigned_to_username'] = 'nobody' + format_dict['assigned_to_url'] = '' + + # This key can be missing on null + if message['data']['incident'].get('resolved_by_user', None): + format_dict['resolved_by_email'] = message['data']['incident']['resolved_by_user']['email'] + format_dict['resolved_by_username'] = message['data']['incident']['resolved_by_user']['email'].split('@')[0] + format_dict['resolved_by_url'] = message['data']['incident']['resolved_by_user']['html_url'] + else: + format_dict['resolved_by_email'] = 'nobody' + format_dict['resolved_by_username'] = 'nobody' + format_dict['resolved_by_url'] = '' + + format_dict['trigger_subject'] = message['data']['incident']['trigger_summary_data']['subject'] + return format_dict + + +def send_raw_pagerduty_json(user_profile, stream, message): + subject = 'pagerduty' + body = ( + 'Unknown pagerdudy message\n' + '``` py\n' + '%s\n' + '```') % (pprint.pformat(message),) + check_send_message(user_profile, get_client('ZulipPagerDutyWebhook'), 'stream', + [stream], subject, body) + + +def send_formated_pagerduty(user_profile, stream, message_type, format_dict): + if message_type in ('incident.trigger', 'incident.unacknowledge'): + template = (':unhealthy_heart: Incident ' + '[{incident_num}]({incident_url}) {action} by ' + '[{service_name}]({service_url}) and assigned to ' + '[{assigned_to_username}@]({assigned_to_url})\n\n>{trigger_subject}') + + elif message_type == 'incident.resolve' and format_dict['resolved_by_url']: + template = (':healthy_heart: Incident ' + '[{incident_num}]({incident_url}) resolved by ' + '[{resolved_by_username}@]({resolved_by_url})\n\n>{trigger_subject}') + elif message_type == 'incident.resolve' and not format_dict['resolved_by_url']: + template = (':healthy_heart: Incident ' + '[{incident_num}]({incident_url}) resolved\n\n>{trigger_subject}') + else: + template = (':average_heart: Incident [{incident_num}]({incident_url}) ' + '{action} by [{assigned_to_username}@]({assigned_to_url})\n\n>{trigger_subject}') + + subject = 'incident {incident_num}'.format(**format_dict) + body = template.format(**format_dict) + + check_send_message(user_profile, get_client('ZulipPagerDutyWebhook'), 'stream', + [stream], subject, body) + + +@api_key_only_webhook_view +@has_request_variables +def api_pagerduty_webhook(request, user_profile, stream=REQ(default='pagerduty')): + payload = ujson.loads(request.body) + + for message in payload['messages']: + message_type = message['type'] + + if message_type not in PAGER_DUTY_EVENT_NAMES: + send_raw_pagerduty_json(user_profile, stream, message) + + try: + format_dict = build_pagerdudy_formatdict(message) + except: + send_raw_pagerduty_json(user_profile, stream, message) + else: + send_formated_pagerduty(user_profile, stream, message_type, format_dict) + + return json_success() diff --git a/zproject/urls.py b/zproject/urls.py index fbeca190d7..784ac45cdd 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -164,6 +164,7 @@ urlpatterns += patterns('zerver.views', url(r'^api/v1/external/stash$', 'webhooks.api_stash_webhook'), url(r'^api/v1/external/freshdesk$', 'webhooks.api_freshdesk_webhook'), url(r'^api/v1/external/zendesk$', 'webhooks.api_zendesk_webhook'), + url(r'^api/v1/external/pagerduty$', 'webhooks.api_pagerduty_webhook'), url(r'^user_uploads/(?P(\d*|unk))/(?P.*)', 'get_uploaded_file'), )