From 85c1f65887e74f69a0d36e1d8640f991244b9263 Mon Sep 17 00:00:00 2001 From: Thomas Jost Date: Wed, 25 Nov 2015 15:53:12 +0100 Subject: [PATCH 1/3] Add Pushover integration --- README.md | 11 +++ hc/api/admin.py | 2 + hc/api/migrations/0017_auto_20151117_1032.py | 19 +++++ hc/api/models.py | 37 +++++++++- hc/front/tests/test_add_channel.py | 2 +- hc/front/urls.py | 1 + hc/front/views.py | 69 +++++++++++++++++-- hc/settings.py | 4 ++ static/css/channels.css | 4 ++ static/img/integrations/pushover.png | Bin 0 -> 11767 bytes templates/front/channel_checks.html | 2 +- templates/front/channels.html | 19 ++++- templates/integrations/add_pushover.html | 50 ++++++++++++++ templates/integrations/pushover_message.html | 14 ++++ 14 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 hc/api/migrations/0017_auto_20151117_1032.py create mode 100644 static/img/integrations/pushover.png create mode 100644 templates/integrations/add_pushover.html create mode 100644 templates/integrations/pushover_message.html diff --git a/README.md b/README.md index c930d52f..dfeeb1e2 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,14 @@ in development environment. $ ./manage.py runserver +## Integrations + +### Pushover + +To enable Pushover integration, you will need to: + +* register a new application on https://pushover.net/apps/build +* enable subscriptions in your application and make sure to enable the URL + subscription type +* add the application token and subscription URL to `hc/local_settings.py`, as + `PUSHOVER_API_TOKEN` and `PUSHOVER_SUBSCRIPTION_URL` diff --git a/hc/api/admin.py b/hc/api/admin.py index 745f3cea..8e27cd91 100644 --- a/hc/api/admin.py +++ b/hc/api/admin.py @@ -146,6 +146,8 @@ class ChannelsAdmin(admin.ModelAdmin): def formatted_kind(self, obj): if obj.kind == "pd": return "PagerDuty" + elif obj.kind == "po": + return "Pushover" elif obj.kind == "webhook": return "Webhook" elif obj.kind == "slack": diff --git a/hc/api/migrations/0017_auto_20151117_1032.py b/hc/api/migrations/0017_auto_20151117_1032.py new file mode 100644 index 00000000..6ed2b6dc --- /dev/null +++ b/hc/api/migrations/0017_auto_20151117_1032.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0016_auto_20151030_1107'), + ] + + operations = [ + migrations.AlterField( + model_name='channel', + name='kind', + field=models.CharField(choices=[('email', 'Email'), ('webhook', 'Webhook'), ('hipchat', 'HipChat'), ('slack', 'Slack'), ('pd', 'PagerDuty'), ('po', 'Pushover')], max_length=20), + ), + ] diff --git a/hc/api/models.py b/hc/api/models.py index f43fd886..40b9b76a 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -24,7 +24,7 @@ DEFAULT_TIMEOUT = td(days=1) DEFAULT_GRACE = td(hours=1) CHANNEL_KINDS = (("email", "Email"), ("webhook", "Webhook"), ("hipchat", "HipChat"), - ("slack", "Slack"), ("pd", "PagerDuty")) + ("slack", "Slack"), ("pd", "PagerDuty"), ("po", "Pushover")) class Check(models.Model): @@ -183,6 +183,41 @@ class Channel(models.Model): n.status = r.status_code n.save() + elif self.kind == "po": + tmpl = "integrations/pushover_message.html" + ctx = { + "check": check, + "down_checks": self.user.check_set.filter(status="down").exclude(code=check.code).order_by("created"), + } + text = render_to_string(tmpl, ctx).strip() + if check.status == "down": + title = "%s is DOWN" % check.name_then_code() + else: + title = "%s is now UP" % check.name_then_code() + + user_key, priority = self.po_value + payload = { + "token": settings.PUSHOVER_API_TOKEN, + "user": user_key, + "message": text, + "title": title, + "html": 1, + "priority": priority, + } + + url = "https://api.pushover.net/1/messages.json" + r = requests.post(url, data=payload, timeout=5) + + n.status = r.status_code + n.save() + + @property + def po_value(self): + assert self.kind == "po" + user_key, prio = self.value.split("|") + prio = int(prio) + return user_key, prio + class Notification(models.Model): owner = models.ForeignKey(Check) diff --git a/hc/front/tests/test_add_channel.py b/hc/front/tests/test_add_channel.py index d77b1013..db069412 100644 --- a/hc/front/tests/test_add_channel.py +++ b/hc/front/tests/test_add_channel.py @@ -31,7 +31,7 @@ class AddChannelTestCase(TestCase): def test_instructions_work(self): self.client.login(username="alice", password="password") - for frag in ("email", "webhook", "pd", "slack", "hipchat"): + for frag in ("email", "webhook", "pd", "pushover", "slack", "hipchat"): url = "/integrations/add_%s/" % frag r = self.client.get(url) self.assertContains(r, "Integration Settings", status_code=200) diff --git a/hc/front/urls.py b/hc/front/urls.py index 2c5289b3..201f9a35 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -21,6 +21,7 @@ urlpatterns = [ url(r'^integrations/add_pd/$', views.add_pd, name="hc-add-pd"), url(r'^integrations/add_slack/$', views.add_slack, name="hc-add-slack"), url(r'^integrations/add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"), + url(r'^integrations/add_pushover/$', views.add_pushover, name="hc-add-pushover"), url(r'^integrations/([\w-]+)/checks/$', views.channel_checks, name="hc-channel-checks"), url(r'^integrations/([\w-]+)/remove/$', views.remove_channel, name="hc-remove-channel"), url(r'^integrations/([\w-]+)/verify/([\w-]+)/$', diff --git a/hc/front/views.py b/hc/front/views.py index afe89586..a1485733 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -1,10 +1,13 @@ from datetime import timedelta as td from django.conf import settings +from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone +from django.utils.six.moves.urllib.parse import urlencode +from django.utils.crypto import get_random_string from hc.api.decorators import uuid_or_400 from hc.api.models import Channel, Check, Ping from hc.front.forms import AddChannelForm, TimeoutForm @@ -224,16 +227,14 @@ def channels(request): ctx = { "page": "channels", "channels": channels, - "num_checks": num_checks - + "num_checks": num_checks, + "enable_pushover": settings.PUSHOVER_API_TOKEN is not None, } return render(request, "front/channels.html", ctx) -@login_required -def add_channel(request): - assert request.method == "POST" - form = AddChannelForm(request.POST) +def do_add_channel(request, data): + form = AddChannelForm(data) if form.is_valid(): channel = form.save(commit=False) channel.user = request.user @@ -250,6 +251,11 @@ def add_channel(request): return HttpResponseBadRequest() +@login_required +def add_channel(request): + assert request.method == "POST" + return do_add_channel(request, request.POST) + @login_required @uuid_or_400 def channel_checks(request, code): @@ -322,3 +328,54 @@ def add_slack(request): def add_hipchat(request): ctx = {"page": "channels"} return render(request, "integrations/add_hipchat.html", ctx) + + +@login_required +def add_pushover(request): + if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None: + return HttpResponseForbidden() + + if request.method == "POST": + # Initiate the subscription + nonce = get_random_string() + request.session["po_nonce"] = nonce + + failure_url = request.build_absolute_uri(reverse("hc-channels")) + success_url = request.build_absolute_uri(reverse("hc-add-pushover")) + "?" + urlencode({ + "nonce": nonce, + "prio": request.POST.get("po_priority", "0"), + }) + subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode({ + "success": success_url, + "failure": failure_url, + }) + + return redirect(subscription_url) + + # Handle successful subscriptions + if "pushover_user_key" in request.GET and "nonce" in request.GET and "prio" in request.GET: + # Validate nonce + if request.GET["nonce"] != request.session.get("po_nonce", None): + return HttpResponseForbidden() + del request.session["po_nonce"] + + if request.GET.get("pushover_unsubscribed", "0") == "1": + # Unsubscription: delete all Pushover channels for this user + for channel in Channel.objects.filter(user=request.user, kind="po"): + channel.delete() + + return redirect("hc-channels") + + else: + # Subscription + user_key = request.GET["pushover_user_key"] + priority = int(request.GET["prio"]) + + return do_add_channel(request, { + "kind": "po", + "value": "%s|%d" % (user_key, priority), + }) + + else: + ctx = {"page": "channels"} + return render(request, "integrations/add_pushover.html", ctx) diff --git a/hc/settings.py b/hc/settings.py index 30d359b6..24301ed6 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -125,6 +125,10 @@ COMPRESS_OFFLINE = True EMAIL_BACKEND = "djmail.backends.default.EmailBackend" +# Pushover integration -- override these in local_settings +PUSHOVER_API_TOKEN = None +PUSHOVER_SUBSCRIPTION_URL = None + try: from .local_settings import * except ImportError as e: diff --git a/static/css/channels.css b/static/css/channels.css index ec5806e4..5ff844dd 100644 --- a/static/css/channels.css +++ b/static/css/channels.css @@ -126,6 +126,10 @@ table.channels-table > tbody > tr > th { height: 48px; } +.btn img.ai-icon { + height: 1.4em; +} + .add-integration h2 { margin-top: 0; } diff --git a/static/img/integrations/pushover.png b/static/img/integrations/pushover.png new file mode 100644 index 0000000000000000000000000000000000000000..73f3798880725971bcdfa7ad8ce253b28175bb5c GIT binary patch literal 11767 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 yyn6+b6${493qD~Uzr@d -

Assign Checks to Channel {{ channel.value }}

+

Assign Checks to Channel {% if channel.kind == "po" %}{{ channel.po_value|join:" / " }}{% else %}{{ channel.value }}{% endif %}

diff --git a/templates/front/channels.html b/templates/front/channels.html index e22811cc..12de0b30 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -23,14 +23,20 @@ {% if ch.kind == "slack" %} Slack {% endif %} {% if ch.kind == "hipchat" %} HipChat {% endif %} {% if ch.kind == "pd" %} PagerDuty {% endif %} + {% if ch.kind == "po" %} Pushover {% endif %} {% if ch.kind == "email" %} to {% endif %} {% if ch.kind == "pd" %} API key {% endif %} + {% if ch.kind == "po" %} User key / priority {% endif %} - {{ ch.value }} + {% if ch.kind == "po" %} + {{ ch.po_value|join:" / " }} + {% else %} + {{ ch.value }} + {% endif %} {% if ch.kind == "email" and not ch.email_verified %} (unconfirmed) @@ -108,6 +114,17 @@ Add Integration + {% if enable_pushover %} +
  • + Pushover icon + +

    Pushover

    +

    Receive instant push notifications on your phone or tablet.

    + + Add Integration +
  • + {% endif %} diff --git a/templates/integrations/add_pushover.html b/templates/integrations/add_pushover.html new file mode 100644 index 00000000..80bfd463 --- /dev/null +++ b/templates/integrations/add_pushover.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} +{% load compress humanize staticfiles hc_extras %} + +{% block title %}Add Pushover - healthchecks.io{% endblock %} + + +{% block content %} +
    +
    +

    Pushover

    + +

    Pushover is a service to receive + instant push notifications on your phone or tablet from a variety of + sources. If you bought the app on your mobile device, you can integrate it + with your healthchecks.io account in a few simple steps.

    + +

    Integration Settings

    + +
    + {% csrf_token %} +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +{% endblock %} + +{% block scripts %} +{% compress js %} + + +{% endcompress %} +{% endblock %} diff --git a/templates/integrations/pushover_message.html b/templates/integrations/pushover_message.html new file mode 100644 index 00000000..d3f5595c --- /dev/null +++ b/templates/integrations/pushover_message.html @@ -0,0 +1,14 @@ +{% load humanize %} + +{% if check.status == "down" %} +The check "{{ check.name_then_code }}" is DOWN. +Last ping was {{ check.last_ping|naturaltime }}. +{% else %} +The check "{{ check.name_then_code }}" received a ping and is now UP. +{% endif %}{% if down_checks %} +The following checks are {% if check.status == "down" %}also{% else %}still{% endif %} down: +{% for down_check in down_checks %}- "{{ down_check.name_then_code }}" (last ping: {{ down_check.last_ping|naturaltime }}) +{% endfor %} +{% else %} +All the other checks are up. +{% endif %} From 280bd6a2a244ac5d273cba8a006e73c68fc25093 Mon Sep 17 00:00:00 2001 From: Thomas Jost Date: Wed, 25 Nov 2015 16:56:34 +0100 Subject: [PATCH 2/3] Handle Pushover emergency notifications --- hc/api/models.py | 3 +++ hc/front/views.py | 6 +++++- hc/settings.py | 2 ++ templates/integrations/add_pushover.html | 11 +++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/hc/api/models.py b/hc/api/models.py index 40b9b76a..f4d9b37d 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -204,6 +204,9 @@ class Channel(models.Model): "html": 1, "priority": priority, } + if priority == 2: # Emergency notification + payload["retry"] = settings.PUSHOVER_EMERGENCY_RETRY_DELAY + payload["expire"] = settings.PUSHOVER_EMERGENCY_EXPIRATION url = "https://api.pushover.net/1/messages.json" r = requests.post(url, data=payload, timeout=5) diff --git a/hc/front/views.py b/hc/front/views.py index a1485733..a56bfdfe 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -377,5 +377,9 @@ def add_pushover(request): }) else: - ctx = {"page": "channels"} + ctx = { + "page": "channels", + "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY), + "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION), + } return render(request, "integrations/add_pushover.html", ctx) diff --git a/hc/settings.py b/hc/settings.py index 24301ed6..2e0b805a 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -128,6 +128,8 @@ EMAIL_BACKEND = "djmail.backends.default.EmailBackend" # Pushover integration -- override these in local_settings PUSHOVER_API_TOKEN = None PUSHOVER_SUBSCRIPTION_URL = None +PUSHOVER_EMERGENCY_RETRY_DELAY = 300 +PUSHOVER_EMERGENCY_EXPIRATION = 86400 try: from .local_settings import * diff --git a/templates/integrations/add_pushover.html b/templates/integrations/add_pushover.html index 80bfd463..a2bb367b 100644 --- a/templates/integrations/add_pushover.html +++ b/templates/integrations/add_pushover.html @@ -26,6 +26,14 @@ + @@ -47,4 +55,7 @@ {% endcompress %} + {% endblock %} From 99cb654ec5730f6be33bb091aa3ac9e70963470c Mon Sep 17 00:00:00 2001 From: Thomas Jost Date: Wed, 25 Nov 2015 21:01:10 +0100 Subject: [PATCH 3/3] Fix tests when Pushover is not configured --- hc/front/tests/test_add_channel.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hc/front/tests/test_add_channel.py b/hc/front/tests/test_add_channel.py index db069412..90781732 100644 --- a/hc/front/tests/test_add_channel.py +++ b/hc/front/tests/test_add_channel.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib.auth.models import User from django.test import TestCase from hc.api.models import Channel @@ -10,6 +11,9 @@ class AddChannelTestCase(TestCase): self.alice.set_password("password") self.alice.save() + settings.PUSHOVER_API_TOKEN = "bogus_token" + settings.PUSHOVER_SUBSCRIPTION_URL = "bogus_url" + def test_it_works(self): url = "/integrations/add/" form = {"kind": "email", "value": "alice@example.org"}