diff --git a/CHANGELOG.md b/CHANGELOG.md index 4543a0f5..61b2b3e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. - Send monthly reports on 1st of every month, not randomly during the month - Signup form sets the "auto-login" cookie to avoid an extra click during first login - Autofocus the email field in the signup form, and submit on enter key +- Add support for OpsGenie EU region (#294) ### Bug Fixes - Prevent double-clicking the submit button in signup form diff --git a/hc/api/models.py b/hc/api/models.py index e8046ee4..fb8272d4 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -650,6 +650,24 @@ class Channel(models.Model): doc = json.loads(self.value) return doc["down"] + @property + def opsgenie_key(self): + assert self.kind == "opsgenie" + if not self.value.startswith("{"): + return self.value + + doc = json.loads(self.value) + return doc["key"] + + @property + def opsgenie_region(self): + assert self.kind == "opsgenie" + if not self.value.startswith("{"): + return "us" + + doc = json.loads(self.value) + return doc["region"] + class Notification(models.Model): class Meta: diff --git a/hc/api/tests/test_channel_model.py b/hc/api/tests/test_channel_model.py index d64163cc..5c8e446a 100644 --- a/hc/api/tests/test_channel_model.py +++ b/hc/api/tests/test_channel_model.py @@ -149,3 +149,14 @@ class ChannelModelTestCase(BaseTestCase): "headers": {"X-Status": "OK"}, }, ) + + def test_it_handles_legacy_opsgenie_value(self): + c = Channel(kind="opsgenie", value="foo123") + self.assertEqual(c.opsgenie_key, "foo123") + self.assertEqual(c.opsgenie_region, "us") + + def test_it_handles_json_opsgenie_value(self): + c = Channel(kind="opsgenie") + c.value = json.dumps({"key": "abc", "region": "eu"}) + self.assertEqual(c.opsgenie_key, "abc") + self.assertEqual(c.opsgenie_region, "eu") diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py index ac37e926..d680feb4 100644 --- a/hc/api/tests/test_notify.py +++ b/hc/api/tests/test_notify.py @@ -421,7 +421,7 @@ class NotifyTestCase(BaseTestCase): self.assertEqual(Notification.objects.count(), 0) @patch("hc.api.transports.requests.request") - def test_opsgenie(self, mock_post): + def test_opsgenie_with_legacy_value(self, mock_post): self._setup_data("opsgenie", "123") mock_post.return_value.status_code = 202 @@ -431,6 +431,7 @@ class NotifyTestCase(BaseTestCase): 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"]) @@ -448,6 +449,19 @@ class NotifyTestCase(BaseTestCase): 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_pushover(self, mock_post): self._setup_data("po", "123|0") diff --git a/hc/api/transports.py b/hc/api/transports.py index b08d52c4..74ffcaa2 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -223,7 +223,7 @@ class OpsGenie(HttpTransport): def notify(self, check): headers = { "Conent-Type": "application/json", - "Authorization": "GenieKey %s" % self.channel.value, + "Authorization": "GenieKey %s" % self.channel.opsgenie_key, } payload = {"alias": str(check.code), "source": settings.SITE_NAME} @@ -235,6 +235,9 @@ class OpsGenie(HttpTransport): payload["description"] = tmpl("opsgenie_description.html", check=check) url = "https://api.opsgenie.com/v2/alerts" + if self.channel.opsgenie_region == "eu": + url = "https://api.eu.opsgenie.com/v2/alerts" + if check.status == "up": url += "/%s/close?identifierType=alias" % check.code @@ -468,6 +471,7 @@ class Trello(HttpTransport): return self.post(self.URL, params=params) + class Apprise(HttpTransport): def notify(self, check): @@ -481,8 +485,14 @@ class Apprise(HttpTransport): a.add(self.channel.value) - notify_type = apprise.NotifyType.SUCCESS \ - if check.status == "up" else apprise.NotifyType.FAILURE + notify_type = ( + apprise.NotifyType.SUCCESS + if check.status == "up" + else apprise.NotifyType.FAILURE + ) - return "Failed" if not \ - a.notify(body=body, title=title, notify_type=notify_type) else None + return ( + "Failed" + if not a.notify(body=body, title=title, notify_type=notify_type) + else None + ) diff --git a/hc/front/forms.py b/hc/front/forms.py index 4e01e49a..ff90497a 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -85,7 +85,8 @@ class CronForm(forms.Form): class AddOpsGenieForm(forms.Form): error_css_class = "has-error" - value = forms.CharField(max_length=40) + region = forms.ChoiceField(initial="us", choices=(("us", "US"), ("eu", "EU"))) + key = forms.CharField(max_length=40) class AddEmailForm(forms.Form): diff --git a/hc/front/tests/test_add_opsgenie.py b/hc/front/tests/test_add_opsgenie.py index a13d494d..706e329f 100644 --- a/hc/front/tests/test_add_opsgenie.py +++ b/hc/front/tests/test_add_opsgenie.py @@ -1,3 +1,5 @@ +import json + from hc.api.models import Channel from hc.test import BaseTestCase @@ -11,7 +13,7 @@ class AddOpsGenieTestCase(BaseTestCase): self.assertContains(r, "escalation policies and incident tracking") def test_it_works(self): - form = {"value": "123456"} + form = {"key": "123456", "region": "us"} self.client.login(username="alice@example.org", password="password") r = self.client.post(self.url, form) @@ -19,14 +21,28 @@ class AddOpsGenieTestCase(BaseTestCase): c = Channel.objects.get() self.assertEqual(c.kind, "opsgenie") - self.assertEqual(c.value, "123456") + + payload = json.loads(c.value) + self.assertEqual(payload["key"], "123456") + self.assertEqual(payload["region"], "us") self.assertEqual(c.project, self.project) def test_it_trims_whitespace(self): - form = {"value": " 123456 "} + form = {"key": " 123456 ", "region": "us"} self.client.login(username="alice@example.org", password="password") self.client.post(self.url, form) c = Channel.objects.get() - self.assertEqual(c.value, "123456") + payload = json.loads(c.value) + self.assertEqual(payload["key"], "123456") + + def test_it_saves_eu_region(self): + form = {"key": "123456", "region": "eu"} + + self.client.login(username="alice@example.org", password="password") + r = self.client.post(self.url, form) + + c = Channel.objects.get() + payload = json.loads(c.value) + self.assertEqual(payload["region"], "eu") diff --git a/hc/front/views.py b/hc/front/views.py index e3b62b22..07bcf56e 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -1138,13 +1138,14 @@ def add_opsgenie(request): form = AddOpsGenieForm(request.POST) if form.is_valid(): channel = Channel(project=request.project, kind="opsgenie") - channel.value = form.cleaned_data["value"] + v = {"region": form.cleaned_data["region"], "key": form.cleaned_data["key"]} + channel.value = json.dumps(v) channel.save() channel.assign_all_checks() return redirect("hc-channels") else: - form = AddUrlForm() + form = AddOpsGenieForm() ctx = {"page": "channels", "project": request.project, "form": form} return render(request, "integrations/add_opsgenie.html", ctx) diff --git a/templates/integrations/add_opsgenie.html b/templates/integrations/add_opsgenie.html index f0115b96..29ee708f 100644 --- a/templates/integrations/add_opsgenie.html +++ b/templates/integrations/add_opsgenie.html @@ -71,13 +71,42 @@ id="api-key" type="text" class="form-control" - name="value" + name="key" placeholder="" - value="{{ form.value.value|default:"" }}"> + value="{{ form.key.value|default:"" }}"> - {% if form.value.errors %} + {% if form.key.errors %}