diff --git a/hc/api/models.py b/hc/api/models.py index 1bb7b726..5c4084cb 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -410,6 +410,22 @@ class Channel(models.Model): tmpl = "https://api.hipchat.com/v2/room/%s/notification?auth_token=%s" return tmpl % (doc["roomId"], doc.get("access_token")) + @property + def pd_service_key(self): + assert self.kind == "pd" + if not self.value.startswith("{"): + return self.value + + doc = json.loads(self.value) + return doc["service_key"] + + @property + def pd_account(self): + assert self.kind == "pd" + if self.value.startswith("{"): + doc = json.loads(self.value) + return doc["account"] + def latest_notification(self): return Notification.objects.filter(channel=self).latest() diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py index 5e0acd14..597cccdf 100644 --- a/hc/api/tests/test_notify.py +++ b/hc/api/tests/test_notify.py @@ -178,6 +178,20 @@ class NotifyTestCase(BaseTestCase): args, kwargs = mock_post.call_args payload = kwargs["json"] self.assertEqual(payload["event_type"], "trigger") + self.assertEqual(payload["service_key"], "123") + + @patch("hc.api.transports.requests.request") + def test_pd_complex(self, mock_post): + self._setup_data("pd", json.dumps({"service_key": "456"})) + mock_post.return_value.status_code = 200 + + self.channel.notify(self.check) + assert Notification.objects.count() == 1 + + args, kwargs = mock_post.call_args + payload = kwargs["json"] + self.assertEqual(payload["event_type"], "trigger") + self.assertEqual(payload["service_key"], "456") @patch("hc.api.transports.requests.request") def test_slack(self, mock_post): diff --git a/hc/api/transports.py b/hc/api/transports.py index 98be7d53..2c7dae8f 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -203,11 +203,12 @@ class PagerDuty(HttpTransport): def notify(self, check): description = tmpl("pd_description.html", check=check) payload = { - "service_key": self.channel.value, + "vendor": settings.PD_VENDOR_KEY, + "service_key": self.channel.pd_service_key, "incident_key": str(check.code), "event_type": "trigger" if check.status == "down" else "resolve", "description": description, - "client": "healthchecks.io", + "client": settings.SITE_NAME, "client_url": settings.SITE_ROOT } diff --git a/hc/front/forms.py b/hc/front/forms.py index 79c5e3a4..1e2d58ed 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -31,11 +31,6 @@ class CronForm(forms.Form): grace = forms.IntegerField(min_value=1, max_value=43200) -class AddPdForm(forms.Form): - error_css_class = "has-error" - value = forms.CharField(max_length=32) - - class AddOpsGenieForm(forms.Form): error_css_class = "has-error" value = forms.CharField(max_length=40) diff --git a/hc/front/tests/test_add_pd.py b/hc/front/tests/test_add_pd.py index d4117908..a42b7918 100644 --- a/hc/front/tests/test_add_pd.py +++ b/hc/front/tests/test_add_pd.py @@ -1,32 +1,42 @@ +from django.test.utils import override_settings from hc.api.models import Channel from hc.test import BaseTestCase +@override_settings(PD_VENDOR_KEY="foo") class AddPdTestCase(BaseTestCase): url = "/integrations/add_pd/" 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") + self.assertContains(r, "If your team uses") def test_it_works(self): - # Integration key is 32 characters long - form = {"value": "12345678901234567890123456789012"} + session = self.client.session + session["pd"] = "1234567890AB" # 12 characters + session.save() self.client.login(username="alice@example.org", password="password") - r = self.client.post(self.url, form) - self.assertRedirects(r, "/integrations/") + url = "/integrations/add_pd/1234567890AB/?service_key=123" + r = self.client.get(url) + self.assertEqual(r.status_code, 302) c = Channel.objects.get() self.assertEqual(c.kind, "pd") - self.assertEqual(c.value, "12345678901234567890123456789012") + self.assertEqual(c.pd_service_key, "123") - def test_it_trims_whitespace(self): - form = {"value": " 123456 "} + def test_it_validates_code(self): + session = self.client.session + session["pd"] = "1234567890AB" # 12 characters + session.save() self.client.login(username="alice@example.org", password="password") - self.client.post(self.url, form) - - c = Channel.objects.get() - self.assertEqual(c.value, "123456") + url = "/integrations/add_pd/XXXXXXXXXXXX/?service_key=123" + r = self.client.get(url) + self.assertEqual(r.status_code, 400) + + @override_settings(PD_VENDOR_KEY=None) + def test_it_requires_vendor_key(self): + r = self.client.get("/integrations/add_pd/") + self.assertEqual(r.status_code, 404) diff --git a/hc/front/urls.py b/hc/front/urls.py index 21819aa1..c9e16a9b 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -16,6 +16,7 @@ channel_urls = [ url(r'^add_email/$', views.add_email, name="hc-add-email"), url(r'^add_webhook/$', views.add_webhook, name="hc-add-webhook"), url(r'^add_pd/$', views.add_pd, name="hc-add-pd"), + url(r'^add_pd/([\w]{12})/$', views.add_pd, name="hc-add-pd-state"), url(r'^add_slack/$', views.add_slack, name="hc-add-slack"), url(r'^add_slack_btn/$', views.add_slack_btn, name="hc-add-slack-btn"), url(r'^add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"), diff --git a/hc/front/views.py b/hc/front/views.py index dee951c0..5b7755c7 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -24,7 +24,7 @@ from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, Ping, Notification) from hc.api.transports import Telegram from hc.front.forms import (AddWebhookForm, NameTagsForm, - TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm, + TimeoutForm, AddUrlForm, AddEmailForm, AddOpsGenieForm, CronForm, AddSmsForm) from hc.front.schemas import telegram_callback from hc.lib import jsonschema @@ -99,6 +99,7 @@ def index(request): "enable_discord": settings.DISCORD_CLIENT_ID is not None, "enable_telegram": settings.TELEGRAM_TOKEN is not None, "enable_sms": settings.TWILIO_AUTH is not None, + "enable_pd": settings.PD_VENDOR_KEY is not None, "registration_open": settings.REGISTRATION_OPEN } @@ -351,6 +352,7 @@ def channels(request): "enable_discord": settings.DISCORD_CLIENT_ID is not None, "enable_telegram": settings.TELEGRAM_TOKEN is not None, "enable_sms": settings.TWILIO_AUTH is not None, + "enable_pd": settings.PD_VENDOR_KEY is not None, "added": request.GET.get("added") } @@ -455,31 +457,13 @@ def add_webhook(request): return render(request, "integrations/add_webhook.html", ctx) -@login_required -def add_pd(request): - if request.method == "POST": - form = AddPdForm(request.POST) - if form.is_valid(): - channel = Channel(user=request.team.user, kind="pd") - channel.value = form.cleaned_data["value"] - channel.save() - - channel.assign_all_checks() - return redirect("hc-channels") - else: - form = AddPdForm() - - ctx = {"page": "channels", "form": form} - return render(request, "integrations/add_pd.html", ctx) - - def _prepare_state(request, session_key): state = get_random_string() request.session[session_key] = state return state -def _get_validated_code(request, session_key): +def _get_validated_code(request, session_key, key="code"): if session_key not in request.session: return None @@ -488,7 +472,46 @@ def _get_validated_code(request, session_key): if session_state is None or session_state != request_state: return None - return request.GET.get("code") + return request.GET.get(key) + + +def add_pd(request, state=None): + if settings.PD_VENDOR_KEY is None: + raise Http404("pagerduty integration is not available") + + if state and request.user.is_authenticated(): + if "pd" not in request.session: + return HttpResponseBadRequest() + + session_state = request.session.pop("pd") + if session_state != state: + return HttpResponseBadRequest() + + if request.GET.get("error") == "cancelled": + messages.warning(request, "PagerDuty setup was cancelled") + return redirect("hc-channels") + + channel = Channel() + channel.user = request.team.user + channel.kind = "pd" + channel.value = json.dumps({ + "service_key": request.GET.get("service_key"), + "account": request.GET.get("account") + }) + channel.save() + channel.assign_all_checks() + messages.success(request, "The PagerDuty integration has been added!") + return redirect("hc-channels") + + state = _prepare_state(request, "pd") + callback = settings.SITE_ROOT + reverse("hc-add-pd-state", args=[state]) + connect_url = "https://connect.pagerduty.com/connect?" + urlencode({ + "vendor": settings.PD_VENDOR_KEY, + "callback": callback + }) + + ctx = {"page": "channels", "connect_url": connect_url} + return render(request, "integrations/add_pd.html", ctx) def add_slack(request): diff --git a/hc/settings.py b/hc/settings.py index 1cd71b5a..2f2be05a 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -162,6 +162,9 @@ TWILIO_ACCOUNT = None TWILIO_AUTH = None TWILIO_FROM = None +# PagerDuty +PD_VENDOR_KEY = None + if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")): from .local_settings import * else: diff --git a/static/img/integrations/pd_connect_button.png b/static/img/integrations/pd_connect_button.png new file mode 100644 index 00000000..28367d03 Binary files /dev/null and b/static/img/integrations/pd_connect_button.png differ diff --git a/static/img/integrations/setup_pd_1.png b/static/img/integrations/setup_pd_1.png index 0255a9af..ff189f6f 100644 Binary files a/static/img/integrations/setup_pd_1.png and b/static/img/integrations/setup_pd_1.png differ diff --git a/static/img/integrations/setup_pd_2.png b/static/img/integrations/setup_pd_2.png index f0d5b1dd..cfd9bd80 100644 Binary files a/static/img/integrations/setup_pd_2.png and b/static/img/integrations/setup_pd_2.png differ diff --git a/static/img/integrations/setup_pd_3.png b/static/img/integrations/setup_pd_3.png new file mode 100644 index 00000000..aa095668 Binary files /dev/null and b/static/img/integrations/setup_pd_3.png differ diff --git a/templates/front/channels.html b/templates/front/channels.html index ec3daab8..598e1709 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -40,8 +40,12 @@ (unconfirmed) {% endif %} {% elif ch.kind == "pd" %} - API key - {{ ch.value }} + {% if ch.pd_account %} + account + {{ ch.pd_account}}, + {% endif %} + service key + {{ ch.pd_service_key }} {% elif ch.kind == "opsgenie" %} API key {{ ch.value }} @@ -215,6 +219,7 @@ Add Integration {% endif %} + {% if enable_pd %}
PagerDuty is - a well-known incident management system. It provides - alerting, on-call scheduling, escalation policies and incident tracking. - If you use or plan on using PagerDuty, you can can integrate it - with your {% site_name %} account in few simple steps.
+If your team uses PagerDuty, + you can set up {% site_name %} to create a PagerDuty incident when + a check goes down, and resolve it when a check goes back up.
+ ++ {% site_name %} is a free and + open source + service for monitoring your cron jobs, background processes and + scheduled tasks. Before adding PagerDuty integration, please log into + {% site_name %}:
+ +- Log into your PagerDuty account, - go to Configuration > Services, - and click on Add New Service. -
-- Give it a descriptive name, and - for Integration Type select - Use our API directly. + After {% if request.user.is_authenticated %}{% else %}logging in and{% endif %} + clicking on "Alert with PagerDuty", you will be + asked to log into your PagerDuty account.
-+ Next, PagerDuty will let set the name and escalation policy + for this integration. +
Paste the Integration Key down below. Save the integration, and it's done!
-