diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e82fbf9..03a6ed7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file. - Add the SLACK_ENABLED setting (#471) - Add the MATTERMOST_ENABLED setting (#471) - Add the MSTEAMS_ENABLED setting (#471) +- Add the OPSGENIE_ENABLED setting (#471) ## Bug Fixes - Fix unwanted HTML escaping in SMS and WhatsApp notifications diff --git a/docker/.env b/docker/.env index 1aa7514f..997e94b1 100644 --- a/docker/.env +++ b/docker/.env @@ -27,6 +27,7 @@ MATRIX_HOMESERVER= MATRIX_USER_ID= MATTERMOST_ENABLED=True MSTEAMS_ENABLED=True +OPSGENIE_ENABLED=True PD_VENDOR_KEY= PING_BODY_LIMIT=10000 PING_EMAIL_DOMAIN=localhost diff --git a/hc/api/models.py b/hc/api/models.py index 76d66e29..46d3af5d 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -36,7 +36,7 @@ CHANNEL_KINDS = ( ("pagerteam", "Pager Team"), ("po", "Pushover"), ("pushbullet", "Pushbullet"), - ("opsgenie", "OpsGenie"), + ("opsgenie", "Opsgenie"), ("victorops", "VictorOps"), ("discord", "Discord"), ("telegram", "Telegram"), @@ -446,7 +446,7 @@ class Channel(models.Model): elif self.kind == "po": return transports.Pushover(self) elif self.kind == "opsgenie": - return transports.OpsGenie(self) + return transports.Opsgenie(self) elif self.kind == "discord": return transports.Discord(self) elif self.kind == "telegram": diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py index 54d7be2d..e0ac7eea 100644 --- a/hc/api/tests/test_notify.py +++ b/hc/api/tests/test_notify.py @@ -79,58 +79,6 @@ class NotifyTestCase(BaseTestCase): self.assertFalse(mock_post.called) self.assertEqual(Notification.objects.count(), 0) - @patch("hc.api.transports.requests.request") - def test_opsgenie_with_legacy_value(self, mock_post): - self._setup_data("opsgenie", "123") - mock_post.return_value.status_code = 202 - - self.channel.notify(self.check) - n = Notification.objects.first() - self.assertEqual(n.error, "") - - self.assertEqual(mock_post.call_count, 1) - args, kwargs = mock_post.call_args - self.assertIn("api.opsgenie.com", args[1]) - payload = kwargs["json"] - self.assertIn("DOWN", payload["message"]) - - @patch("hc.api.transports.requests.request") - def test_opsgenie_up(self, mock_post): - self._setup_data("opsgenie", "123", status="up") - mock_post.return_value.status_code = 202 - - self.channel.notify(self.check) - n = Notification.objects.first() - self.assertEqual(n.error, "") - - self.assertEqual(mock_post.call_count, 1) - args, kwargs = mock_post.call_args - method, url = args - self.assertTrue(str(self.check.code) in url) - - @patch("hc.api.transports.requests.request") - def test_opsgenie_with_json_value(self, mock_post): - self._setup_data("opsgenie", json.dumps({"key": "456", "region": "eu"})) - mock_post.return_value.status_code = 202 - - self.channel.notify(self.check) - n = Notification.objects.first() - self.assertEqual(n.error, "") - - self.assertEqual(mock_post.call_count, 1) - args, kwargs = mock_post.call_args - self.assertIn("api.eu.opsgenie.com", args[1]) - - @patch("hc.api.transports.requests.request") - def test_opsgenie_returns_error(self, mock_post): - self._setup_data("opsgenie", "123") - mock_post.return_value.status_code = 403 - mock_post.return_value.json.return_value = {"message": "Nice try"} - - self.channel.notify(self.check) - n = Notification.objects.first() - self.assertEqual(n.error, 'Received status code 403 with a message: "Nice try"') - @patch("hc.api.transports.requests.request") def test_victorops(self, mock_post): self._setup_data("victorops", "123") diff --git a/hc/api/tests/test_notify_opsgenie.py b/hc/api/tests/test_notify_opsgenie.py new file mode 100644 index 00000000..c1b89a6f --- /dev/null +++ b/hc/api/tests/test_notify_opsgenie.py @@ -0,0 +1,85 @@ +# coding: utf-8 + +from datetime import timedelta as td +import json +from unittest.mock import patch + +from django.test.utils import override_settings +from django.utils.timezone import now +from hc.api.models import Channel, Check, Notification +from hc.test import BaseTestCase + + +class NotifyTestCase(BaseTestCase): + def _setup_data(self, value, status="down", email_verified=True): + self.check = Check(project=self.project) + self.check.status = status + self.check.last_ping = now() - td(minutes=61) + self.check.save() + + self.channel = Channel(project=self.project) + self.channel.kind = "opsgenie" + self.channel.value = value + self.channel.email_verified = email_verified + self.channel.save() + self.channel.checks.add(self.check) + + @patch("hc.api.transports.requests.request") + def test_opsgenie_with_legacy_value(self, mock_post): + self._setup_data("123") + mock_post.return_value.status_code = 202 + + self.channel.notify(self.check) + n = Notification.objects.first() + self.assertEqual(n.error, "") + + self.assertEqual(mock_post.call_count, 1) + args, kwargs = mock_post.call_args + self.assertIn("api.opsgenie.com", args[1]) + payload = kwargs["json"] + self.assertIn("DOWN", payload["message"]) + + @patch("hc.api.transports.requests.request") + def test_opsgenie_up(self, mock_post): + self._setup_data("123", status="up") + mock_post.return_value.status_code = 202 + + self.channel.notify(self.check) + n = Notification.objects.first() + self.assertEqual(n.error, "") + + self.assertEqual(mock_post.call_count, 1) + args, kwargs = mock_post.call_args + method, url = args + self.assertTrue(str(self.check.code) in url) + + @patch("hc.api.transports.requests.request") + def test_opsgenie_with_json_value(self, mock_post): + self._setup_data(json.dumps({"key": "456", "region": "eu"})) + mock_post.return_value.status_code = 202 + + self.channel.notify(self.check) + n = Notification.objects.first() + self.assertEqual(n.error, "") + + self.assertEqual(mock_post.call_count, 1) + args, kwargs = mock_post.call_args + self.assertIn("api.eu.opsgenie.com", args[1]) + + @patch("hc.api.transports.requests.request") + def test_opsgenie_returns_error(self, mock_post): + self._setup_data("123") + mock_post.return_value.status_code = 403 + mock_post.return_value.json.return_value = {"message": "Nice try"} + + self.channel.notify(self.check) + n = Notification.objects.first() + self.assertEqual(n.error, 'Received status code 403 with a message: "Nice try"') + + @override_settings(OPSGENIE_ENABLED=False) + def test_it_requires_opsgenie_enabled(self): + self._setup_data("123") + self.channel.notify(self.check) + + n = Notification.objects.get() + self.assertEqual(n.error, "Opsgenie notifications are not enabled.") diff --git a/hc/api/transports.py b/hc/api/transports.py index d7d642b3..fa8b35e8 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -279,7 +279,7 @@ class HipChat(HttpTransport): return True -class OpsGenie(HttpTransport): +class Opsgenie(HttpTransport): @classmethod def get_error(cls, response): try: @@ -288,6 +288,9 @@ class OpsGenie(HttpTransport): pass def notify(self, check): + if not settings.OPSGENIE_ENABLED: + return "Opsgenie notifications are not enabled." + headers = { "Conent-Type": "application/json", "Authorization": "GenieKey %s" % self.channel.opsgenie_key, diff --git a/hc/front/forms.py b/hc/front/forms.py index 818a90d8..3b7a93e0 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -99,7 +99,7 @@ class CronForm(forms.Form): grace = forms.IntegerField(min_value=1, max_value=43200) -class AddOpsGenieForm(forms.Form): +class AddOpsgenieForm(forms.Form): error_css_class = "has-error" region = forms.ChoiceField(initial="us", choices=(("us", "US"), ("eu", "EU"))) key = forms.CharField(max_length=40) diff --git a/hc/front/tests/test_add_opsgenie.py b/hc/front/tests/test_add_opsgenie.py index 4c0773e8..aeba5f2d 100644 --- a/hc/front/tests/test_add_opsgenie.py +++ b/hc/front/tests/test_add_opsgenie.py @@ -1,10 +1,11 @@ import json +from django.test.utils import override_settings from hc.api.models import Channel from hc.test import BaseTestCase -class AddOpsGenieTestCase(BaseTestCase): +class AddOpsgenieTestCase(BaseTestCase): def setUp(self): super().setUp() self.url = "/projects/%s/add_opsgenie/" % self.project.code @@ -56,3 +57,9 @@ class AddOpsGenieTestCase(BaseTestCase): self.client.login(username="bob@example.org", password="password") r = self.client.get(self.url) self.assertEqual(r.status_code, 403) + + @override_settings(OPSGENIE_ENABLED=False) + def test_it_handles_disabled_integration(self): + self.client.login(username="alice@example.org", password="password") + r = self.client.get(self.url) + self.assertEqual(r.status_code, 404) diff --git a/hc/front/views.py b/hc/front/views.py index f60baaaa..e6d948dd 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -297,6 +297,7 @@ def index(request): "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None, "enable_mattermost": settings.MATTERMOST_ENABLED is True, "enable_msteams": settings.MSTEAMS_ENABLED is True, + "enable_opsgenie": settings.OPSGENIE_ENABLED is True, "enable_pdc": settings.PD_VENDOR_KEY is not None, "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None, @@ -767,6 +768,7 @@ def channels(request, code): "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None, "enable_mattermost": settings.MATTERMOST_ENABLED is True, "enable_msteams": settings.MSTEAMS_ENABLED is True, + "enable_opsgenie": settings.OPSGENIE_ENABLED is True, "enable_pdc": settings.PD_VENDOR_KEY is not None, "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None, @@ -1432,12 +1434,13 @@ def add_pushover(request, code): return render(request, "integrations/add_pushover.html", ctx) +@require_setting("OPSGENIE_ENABLED") @login_required def add_opsgenie(request, code): project = _get_rw_project_for_user(request, code) if request.method == "POST": - form = forms.AddOpsGenieForm(request.POST) + form = forms.AddOpsgenieForm(request.POST) if form.is_valid(): channel = Channel(project=project, kind="opsgenie") v = {"region": form.cleaned_data["region"], "key": form.cleaned_data["key"]} @@ -1447,7 +1450,7 @@ def add_opsgenie(request, code): channel.assign_all_checks() return redirect("hc-channels", project.code) else: - form = forms.AddOpsGenieForm() + form = forms.AddOpsgenieForm() ctx = {"page": "channels", "project": project, "form": form} return render(request, "integrations/add_opsgenie.html", ctx) diff --git a/hc/settings.py b/hc/settings.py index fd656ce8..20876cdc 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -205,6 +205,9 @@ MATTERMOST_ENABLED = envbool("MATTERMOST_ENABLED", "True") # MS Teams MSTEAMS_ENABLED = envbool("MSTEAMS_ENABLED", "True") +# Opsgenie +OPSGENIE_ENABLED = envbool("OPSGENIE_ENABLED", "True") + # PagerDuty PD_VENDOR_KEY = os.getenv("PD_VENDOR_KEY") diff --git a/templates/docs/configuring_notifications.html b/templates/docs/configuring_notifications.html index fcedaeb4..32281c32 100644 --- a/templates/docs/configuring_notifications.html +++ b/templates/docs/configuring_notifications.html @@ -34,7 +34,7 @@ The "unused" sends from one month do not carry over to the next month.
If you want to receive repeated notifications for as long as a particular check is down, you have a few different options:
MSTEAMS_ENABLED
Default: True
A boolean that turns on/off the MS Teams integration. Enabled by default.
+OPSGENIE_ENABLED
Default: True
A boolean that turns on/off the Opsgenie integration. Enabled by default.
PD_VENDOR_KEY
Default: None
PagerDuty vendor key, diff --git a/templates/docs/self_hosted_configuration.md b/templates/docs/self_hosted_configuration.md index 57460fb5..455cd4fe 100644 --- a/templates/docs/self_hosted_configuration.md +++ b/templates/docs/self_hosted_configuration.md @@ -242,6 +242,12 @@ Default: `True` A boolean that turns on/off the MS Teams integration. Enabled by default. +## `OPSGENIE_ENABLED` {: #OPSGENIE_ENABLED } + +Default: `True` + +A boolean that turns on/off the Opsgenie integration. Enabled by default. + ## `PD_VENDOR_KEY` {: #PD_VENDOR_KEY } Default: `None` diff --git a/templates/front/channels.html b/templates/front/channels.html index 19ed3429..47a25e50 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -287,14 +287,16 @@
Alerting & Incident Management Solution for Dev & Ops.
Add IntegrationOpsGenie provides +
Opsgenie provides alerting, on-call scheduling, escalation policies, and incident tracking. You can integrate it with your {{ site_name }} account in a few simple steps.
@@ -19,7 +19,7 @@- Log into your OpsGenie account, + Log into your Opsgenie account, select a team, and go to the team's Integrations › Add integration page.