diff --git a/hc/api/admin.py b/hc/api/admin.py index 62883a34..cd0ad9f6 100644 --- a/hc/api/admin.py +++ b/hc/api/admin.py @@ -168,6 +168,8 @@ class ChannelsAdmin(admin.ModelAdmin): return "Slack" elif obj.kind == "hipchat": return "HipChat" + elif obj.kind == "opsgenie": + return "OpsGenie" elif obj.kind == "email" and obj.email_verified: return "Email" elif obj.kind == "email" and not obj.email_verified: diff --git a/hc/api/models.py b/hc/api/models.py index e645119a..7a4c729b 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -28,6 +28,7 @@ CHANNEL_KINDS = (("email", "Email"), ("pd", "PagerDuty"), ("po", "Pushover"), ("pushbullet", "Pushbullet"), + ("opsgenie", "OpsGenie"), ("victorops", "VictorOps")) PO_PRIORITIES = { @@ -187,6 +188,8 @@ class Channel(models.Model): return transports.Pushbullet(self) elif self.kind == "po": return transports.Pushover(self) + elif self.kind == "opsgenie": + return transports.OpsGenie(self) else: raise NotImplementedError("Unknown channel kind: %s" % self.kind) diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py index 50a38af9..b65374a6 100644 --- a/hc/api/tests/test_notify.py +++ b/hc/api/tests/test_notify.py @@ -216,6 +216,19 @@ class NotifyTestCase(BaseTestCase): json = kwargs["json"] self.assertIn("DOWN", json["message"]) + @patch("hc.api.transports.requests.request") + def test_opsgenie(self, mock_post): + self._setup_data("opsgenie", "123") + mock_post.return_value.status_code = 200 + + self.channel.notify(self.check) + n = Notification.objects.first() + self.assertEqual(n.error, "") + + args, kwargs = mock_post.call_args + json = kwargs["json"] + self.assertIn("DOWN", json["message"]) + @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 0dd5a72b..fbcb4be3 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -141,6 +141,26 @@ class HipChat(HttpTransport): return self.post(self.channel.value, payload) +class OpsGenie(HttpTransport): + + def notify(self, check): + payload = { + "apiKey": self.channel.value, + "alias": str(check.code), + "source": "healthchecks.io" + } + + if check.status == "down": + payload["tags"] = ",".join(check.tags_list()) + payload["message"] = tmpl("opsgenie_message.html", check=check) + payload["note"] = tmpl("opsgenie_note.html", check=check) + + url = "https://api.opsgenie.com/v1/json/alert" + if check.status == "up": + url += "/close" + + return self.post(url, payload) + class PagerDuty(HttpTransport): URL = "https://events.pagerduty.com/generic/2010-04-15/create_event.json" diff --git a/hc/front/forms.py b/hc/front/forms.py index 4923dc1a..8460fd66 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -38,6 +38,9 @@ class AddPdForm(forms.Form): error_css_class = "has-error" value = forms.CharField(max_length=20) +class AddOpsGenieForm(forms.Form): + error_css_class = "has-error" + value = forms.CharField(max_length=40) class AddEmailForm(forms.Form): error_css_class = "has-error" diff --git a/hc/front/tests/test_add_opsgenie.py b/hc/front/tests/test_add_opsgenie.py new file mode 100644 index 00000000..e8d1d345 --- /dev/null +++ b/hc/front/tests/test_add_opsgenie.py @@ -0,0 +1,31 @@ +from hc.api.models import Channel +from hc.test import BaseTestCase + + +class AddOpsGenieTestCase(BaseTestCase): + url = "/integrations/add_opsgenie/" + + def test_instructions_work(self): + self.client.login(username="alice@example.org", password="password") + r = self.client.get(self.url) + self.assertContains(r, "incident management system") + + def test_it_works(self): + form = {"value": "123456"} + + self.client.login(username="alice@example.org", password="password") + r = self.client.post(self.url, form) + self.assertRedirects(r, "/integrations/") + + c = Channel.objects.get() + self.assertEqual(c.kind, "opsgenie") + self.assertEqual(c.value, "123456") + + def test_it_trims_whitespace(self): + form = {"value": " 123456 "} + + self.client.login(username="alice@example.org", password="password") + self.client.post(self.url, form) + + c = Channel.objects.get() + self.assertEqual(c.value, "123456") diff --git a/hc/front/urls.py b/hc/front/urls.py index 07c77ac5..ca9a6f04 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -20,6 +20,7 @@ channel_urls = [ url(r'^add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"), url(r'^add_pushbullet/$', views.add_pushbullet, name="hc-add-pushbullet"), url(r'^add_pushover/$', views.add_pushover, name="hc-add-pushover"), + url(r'^add_opsgenie/$', views.add_opsgenie, name="hc-add-opsgenie"), url(r'^add_victorops/$', views.add_victorops, name="hc-add-victorops"), url(r'^([\w-]+)/checks/$', views.channel_checks, name="hc-channel-checks"), url(r'^([\w-]+)/remove/$', views.remove_channel, name="hc-remove-channel"), diff --git a/hc/front/views.py b/hc/front/views.py index 4f8da7e5..e3e14ba8 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -16,7 +16,8 @@ from django.utils.six.moves.urllib.parse import urlencode from hc.api.decorators import uuid_or_400 from hc.api.models import DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, Ping from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm, - TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm) + TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm, + AddOpsGenieForm) # from itertools recipes: @@ -569,6 +570,24 @@ def add_pushover(request): return render(request, "integrations/add_pushover.html", ctx) +@login_required +def add_opsgenie(request): + if request.method == "POST": + form = AddOpsGenieForm(request.POST) + if form.is_valid(): + channel = Channel(user=request.team.user, kind="opsgenie") + channel.value = form.cleaned_data["value"] + channel.save() + + channel.assign_all_checks() + return redirect("hc-channels") + else: + form = AddUrlForm() + + ctx = {"page": "channels", "form": form} + return render(request, "integrations/add_opsgenie.html", ctx) + + @login_required def add_victorops(request): if request.method == "POST": diff --git a/static/img/integrations/opsgenie.png b/static/img/integrations/opsgenie.png new file mode 100644 index 00000000..df3e9dae Binary files /dev/null and b/static/img/integrations/opsgenie.png differ diff --git a/static/img/integrations/setup_opsgenie_1.png b/static/img/integrations/setup_opsgenie_1.png new file mode 100644 index 00000000..d20b90b7 Binary files /dev/null and b/static/img/integrations/setup_opsgenie_1.png differ diff --git a/static/img/integrations/setup_opsgenie_2.png b/static/img/integrations/setup_opsgenie_2.png new file mode 100644 index 00000000..019e5518 Binary files /dev/null and b/static/img/integrations/setup_opsgenie_2.png differ diff --git a/templates/front/channels.html b/templates/front/channels.html index 9f5bb85c..a06cad1f 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -35,6 +35,7 @@ {% if ch.kind == "po" %} Pushover {% endif %} {% if ch.kind == "victorops" %} VictorOps {% endif %} {% if ch.kind == "pushbullet" %} Pushbullet {% endif %} + {% if ch.kind == "opsgenie" %} OpsGenie {% endif %} {% if ch.kind == "email" %} @@ -46,6 +47,9 @@ {% elif ch.kind == "pd" %} API key {{ ch.value }} + {% elif ch.kind == "opsgenie" %} + API key + {{ ch.value }} {% elif ch.kind == "victorops" %} Post URL {{ ch.value }} @@ -179,6 +183,15 @@ Add Integration +
  • + OpsGenie icon + +

    OpsGenie

    +

    Alerting & Incident Management Solution for Dev & Ops.

    + + Add Integration +
  • {% if enable_pushbullet %}
  • Notifications in HipChat channel. + + + OpsGenie icon + + Open and resolve incidents in OpsGenie. + + PagerDuty icon @@ -269,6 +276,15 @@ Open and resolve incidents in VictorOps. + + + + + Pushbullet icon + + Instant push notifications with Pushbullet. + + {% if enable_pushover %} diff --git a/templates/integrations/add_opsgenie.html b/templates/integrations/add_opsgenie.html new file mode 100644 index 00000000..8beef295 --- /dev/null +++ b/templates/integrations/add_opsgenie.html @@ -0,0 +1,102 @@ +{% extends "base.html" %} +{% load compress humanize staticfiles hc_extras %} + +{% block title %}Add OpsGenie - {% site_name %}{% endblock %} + + +{% block content %} +
    +
    +

    OpsGenie

    + +

    OpsGenie provides + alerting, on-call scheduling, escalation policies and incident tracking. + You can can integrate it with your {% site_name %} account in few + simple steps.

    + +

    Setup Guide

    +
    +
    + 1 +

    + Log into your OpsGenie account, + go to Integrations > Add New Integrations, + and add the "API" integration. +

    +

    + Give it a descriptive name, then + save the integration. +

    + +
    +
    + Screenshot +
    +
    +
    +
    + 2 + After adding the new integration, take note of its + API key, a long string + of letters and digits. + +
    +
    + Screenshot +
    +
    + +
    +
    + 3 +

    Paste the API key down below. Save the integration, and + you are done!

    +
    +
    + +

    Integration Settings

    + +
    + {% csrf_token %} +
    + +
    + + + {% if form.value.errors %} +
    + {{ form.value.errors|join:"" }} +
    + {% endif %} +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + +{% endblock %} + +{% block scripts %} +{% compress js %} + + +{% endcompress %} +{% endblock %} diff --git a/templates/integrations/add_pd.html b/templates/integrations/add_pd.html index 47bc3d84..3ad1b11d 100644 --- a/templates/integrations/add_pd.html +++ b/templates/integrations/add_pd.html @@ -21,7 +21,7 @@ 1

    Log into your PagerDuty account, - go to Configuration > Services, + go to Configuration > Services, and click on Add New Service.

    diff --git a/templates/integrations/opsgenie_message.html b/templates/integrations/opsgenie_message.html new file mode 100644 index 00000000..a3114643 --- /dev/null +++ b/templates/integrations/opsgenie_message.html @@ -0,0 +1 @@ +The check "{{ check.name_then_code }}" is DOWN. \ No newline at end of file diff --git a/templates/integrations/opsgenie_note.html b/templates/integrations/opsgenie_note.html new file mode 100644 index 00000000..1f559849 --- /dev/null +++ b/templates/integrations/opsgenie_note.html @@ -0,0 +1,4 @@ +{% load hc_extras humanize %} + +Expecting to receive a ping every {{ check.timeout|hc_duration }}. +Last ping was {{ check.last_ping|naturaltime }}.