forked from GithubBackups/healthchecks
Add Pushover integration
This commit is contained in:
parent
f08221a1db
commit
85c1f65887
11
README.md
11
README.md
@ -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`
|
||||||
|
@ -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":
|
||||||
|
19
hc/api/migrations/0017_auto_20151117_1032.py
Normal file
19
hc/api/migrations/0017_auto_20151117_1032.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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-]+)/$',
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
BIN
static/img/integrations/pushover.png
Normal file
BIN
static/img/integrations/pushover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal">×</span></button>
|
<button type="button" class="close" data-dismiss="modal">×</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 }}" />
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
50
templates/integrations/add_pushover.html
Normal file
50
templates/integrations/add_pushover.html
Normal 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 %}
|
14
templates/integrations/pushover_message.html
Normal file
14
templates/integrations/pushover_message.html
Normal 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 %}
|
Loading…
x
Reference in New Issue
Block a user