Add Pushover integration

This commit is contained in:
Thomas Jost 2015-11-25 15:53:12 +01:00
parent f08221a1db
commit 85c1f65887
14 changed files with 224 additions and 10 deletions

View File

@ -63,3 +63,14 @@ in development environment.
$ ./manage.py runserver $ ./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`

View File

@ -146,6 +146,8 @@ class ChannelsAdmin(admin.ModelAdmin):
def formatted_kind(self, obj): def formatted_kind(self, obj):
if obj.kind == "pd": if obj.kind == "pd":
return "PagerDuty" return "PagerDuty"
elif obj.kind == "po":
return "Pushover"
elif obj.kind == "webhook": elif obj.kind == "webhook":
return "Webhook" return "Webhook"
elif obj.kind == "slack": elif obj.kind == "slack":

View File

@ -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),
),
]

View File

@ -24,7 +24,7 @@ DEFAULT_TIMEOUT = td(days=1)
DEFAULT_GRACE = td(hours=1) DEFAULT_GRACE = td(hours=1)
CHANNEL_KINDS = (("email", "Email"), ("webhook", "Webhook"), CHANNEL_KINDS = (("email", "Email"), ("webhook", "Webhook"),
("hipchat", "HipChat"), ("hipchat", "HipChat"),
("slack", "Slack"), ("pd", "PagerDuty")) ("slack", "Slack"), ("pd", "PagerDuty"), ("po", "Pushover"))
class Check(models.Model): class Check(models.Model):
@ -183,6 +183,41 @@ class Channel(models.Model):
n.status = r.status_code n.status = r.status_code
n.save() 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): class Notification(models.Model):
owner = models.ForeignKey(Check) owner = models.ForeignKey(Check)

View File

@ -31,7 +31,7 @@ class AddChannelTestCase(TestCase):
def test_instructions_work(self): def test_instructions_work(self):
self.client.login(username="alice", password="password") 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 url = "/integrations/add_%s/" % frag
r = self.client.get(url) r = self.client.get(url)
self.assertContains(r, "Integration Settings", status_code=200) self.assertContains(r, "Integration Settings", status_code=200)

View File

@ -21,6 +21,7 @@ urlpatterns = [
url(r'^integrations/add_pd/$', views.add_pd, name="hc-add-pd"), 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_slack/$', views.add_slack, name="hc-add-slack"),
url(r'^integrations/add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"), 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-]+)/checks/$', views.channel_checks, name="hc-channel-checks"),
url(r'^integrations/([\w-]+)/remove/$', views.remove_channel, name="hc-remove-channel"), url(r'^integrations/([\w-]+)/remove/$', views.remove_channel, name="hc-remove-channel"),
url(r'^integrations/([\w-]+)/verify/([\w-]+)/$', url(r'^integrations/([\w-]+)/verify/([\w-]+)/$',

View File

@ -1,10 +1,13 @@
from datetime import timedelta as td from datetime import timedelta as td
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone 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.decorators import uuid_or_400
from hc.api.models import Channel, Check, Ping from hc.api.models import Channel, Check, Ping
from hc.front.forms import AddChannelForm, TimeoutForm from hc.front.forms import AddChannelForm, TimeoutForm
@ -224,16 +227,14 @@ def channels(request):
ctx = { ctx = {
"page": "channels", "page": "channels",
"channels": 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) return render(request, "front/channels.html", ctx)
@login_required def do_add_channel(request, data):
def add_channel(request): form = AddChannelForm(data)
assert request.method == "POST"
form = AddChannelForm(request.POST)
if form.is_valid(): if form.is_valid():
channel = form.save(commit=False) channel = form.save(commit=False)
channel.user = request.user channel.user = request.user
@ -250,6 +251,11 @@ def add_channel(request):
return HttpResponseBadRequest() return HttpResponseBadRequest()
@login_required
def add_channel(request):
assert request.method == "POST"
return do_add_channel(request, request.POST)
@login_required @login_required
@uuid_or_400 @uuid_or_400
def channel_checks(request, code): def channel_checks(request, code):
@ -322,3 +328,54 @@ def add_slack(request):
def add_hipchat(request): def add_hipchat(request):
ctx = {"page": "channels"} ctx = {"page": "channels"}
return render(request, "integrations/add_hipchat.html", ctx) 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)

View File

@ -125,6 +125,10 @@ COMPRESS_OFFLINE = True
EMAIL_BACKEND = "djmail.backends.default.EmailBackend" EMAIL_BACKEND = "djmail.backends.default.EmailBackend"
# Pushover integration -- override these in local_settings
PUSHOVER_API_TOKEN = None
PUSHOVER_SUBSCRIPTION_URL = None
try: try:
from .local_settings import * from .local_settings import *
except ImportError as e: except ImportError as e:

View File

@ -126,6 +126,10 @@ table.channels-table > tbody > tr > th {
height: 48px; height: 48px;
} }
.btn img.ai-icon {
height: 1.4em;
}
.add-integration h2 { .add-integration h2 {
margin-top: 0; margin-top: 0;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -5,7 +5,7 @@
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</span></button> <button type="button" class="close" data-dismiss="modal">&times;</span></button>
<h4 class="update-timeout-title">Assign Checks to Channel {{ channel.value }}</h4> <h4 class="update-timeout-title">Assign Checks to Channel {% if channel.kind == "po" %}{{ channel.po_value|join:" / " }}{% else %}{{ channel.value }}{% endif %}</h4>
</div> </div>
<input type="hidden" name="channel" value="{{ channel.code }}" /> <input type="hidden" name="channel" value="{{ channel.code }}" />

View File

@ -23,14 +23,20 @@
{% if ch.kind == "slack" %} Slack {% endif %} {% if ch.kind == "slack" %} Slack {% endif %}
{% if ch.kind == "hipchat" %} HipChat {% endif %} {% if ch.kind == "hipchat" %} HipChat {% endif %}
{% if ch.kind == "pd" %} PagerDuty {% endif %} {% if ch.kind == "pd" %} PagerDuty {% endif %}
{% if ch.kind == "po" %} Pushover {% endif %}
</td> </td>
<td> <td>
<span class="preposition"> <span class="preposition">
{% if ch.kind == "email" %} to {% endif %} {% if ch.kind == "email" %} to {% endif %}
{% if ch.kind == "pd" %} API key {% endif %} {% if ch.kind == "pd" %} API key {% endif %}
{% if ch.kind == "po" %} User key / priority {% endif %}
</span> </span>
{% if ch.kind == "po" %}
{{ ch.po_value|join:" / " }}
{% else %}
{{ ch.value }} {{ ch.value }}
{% endif %}
{% if ch.kind == "email" and not ch.email_verified %} {% if ch.kind == "email" and not ch.email_verified %}
<span class="channel-unconfirmed">(unconfirmed) <span class="channel-unconfirmed">(unconfirmed)
@ -108,6 +114,17 @@
<a href="{% url 'hc-add-hipchat' %}" class="btn btn-primary">Add Integration</a> <a href="{% url 'hc-add-hipchat' %}" class="btn btn-primary">Add Integration</a>
</li> </li>
{% if enable_pushover %}
<li>
<img src="{% static 'img/integrations/pushover.png' %}"
alt="Pushover icon" />
<h2>Pushover</h2>
<p>Receive instant push notifications on your phone or tablet.</p>
<a href="{% url 'hc-add-pushover' %}" class="btn btn-primary">Add Integration</a>
</li>
{% endif %}
</ul> </ul>

View File

@ -0,0 +1,50 @@
{% extends "base.html" %}
{% load compress humanize staticfiles hc_extras %}
{% block title %}Add Pushover - healthchecks.io{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<h1>Pushover</h1>
<p><a href="https://www.pushover.net/">Pushover</a> 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.</p>
<h2>Integration Settings</h2>
<form method="post" class="form-horizontal" action="{% url 'hc-add-pushover' %}">
{% csrf_token %}
<div class="form-group">
<label for="po_priority" class="col-sm-2 control-label">Notification priority</label>
<div class="col-sm-3">
<select class="form-control" id="po_priority" name="po_priority">
<option value="-2">Lowest</option>
<option value="-1">Low</option>
<option value="0" selected>Normal</option>
<option value="1">High</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">
<img class="ai-icon" src="{% static 'img/integrations/pushover.png' %}" alt="Pushover" />
Subscribe with Pushover
</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
{% endcompress %}
{% endblock %}

View File

@ -0,0 +1,14 @@
{% load humanize %}
{% if check.status == "down" %}
The check "{{ check.name_then_code }}" is <b>DOWN</b>.
Last ping was {{ check.last_ping|naturaltime }}.
{% else %}
The check "{{ check.name_then_code }}" received a ping and is now <b>UP</b>.
{% 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 %}