forked from GithubBackups/healthchecks
commit
ba886e90cb
@ -6,7 +6,7 @@ python:
|
|||||||
- "3.7"
|
- "3.7"
|
||||||
install:
|
install:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install braintree coveralls mock mysqlclient reportlab
|
- pip install braintree coveralls mock mysqlclient reportlab apprise
|
||||||
env:
|
env:
|
||||||
- DB=sqlite
|
- DB=sqlite
|
||||||
- DB=mysql
|
- DB=mysql
|
||||||
|
11
README.md
11
README.md
@ -133,6 +133,7 @@ Configurations settings loaded from environment variables:
|
|||||||
| MATRIX_HOMESERVER | `None`
|
| MATRIX_HOMESERVER | `None`
|
||||||
| MATRIX_USER_ID | `None`
|
| MATRIX_USER_ID | `None`
|
||||||
| MATRIX_ACCESS_TOKEN | `None`
|
| MATRIX_ACCESS_TOKEN | `None`
|
||||||
|
| APPRISE_ENABLED | `"False"`
|
||||||
|
|
||||||
|
|
||||||
Some useful settings keys to override are:
|
Some useful settings keys to override are:
|
||||||
@ -336,3 +337,13 @@ where to forward channel messages by invoking Telegram's
|
|||||||
|
|
||||||
For this to work, your `SITE_ROOT` needs to be correct and use "https://"
|
For this to work, your `SITE_ROOT` needs to be correct and use "https://"
|
||||||
scheme.
|
scheme.
|
||||||
|
|
||||||
|
### Apprise
|
||||||
|
|
||||||
|
To enable Apprise integration, you will need to:
|
||||||
|
|
||||||
|
* ensure you have apprise installed in your local environment:
|
||||||
|
```bash
|
||||||
|
pip install apprise
|
||||||
|
```
|
||||||
|
* enable the apprise functionality by setting the `APPRISE_ENABLED` environment variable.
|
||||||
|
@ -41,6 +41,7 @@ CHANNEL_KINDS = (
|
|||||||
("trello", "Trello"),
|
("trello", "Trello"),
|
||||||
("matrix", "Matrix"),
|
("matrix", "Matrix"),
|
||||||
("whatsapp", "WhatsApp"),
|
("whatsapp", "WhatsApp"),
|
||||||
|
("apprise", "Apprise"),
|
||||||
)
|
)
|
||||||
|
|
||||||
PO_PRIORITIES = {-2: "lowest", -1: "low", 0: "normal", 1: "high", 2: "emergency"}
|
PO_PRIORITIES = {-2: "lowest", -1: "low", 0: "normal", 1: "high", 2: "emergency"}
|
||||||
@ -392,6 +393,8 @@ class Channel(models.Model):
|
|||||||
return transports.Matrix(self)
|
return transports.Matrix(self)
|
||||||
elif self.kind == "whatsapp":
|
elif self.kind == "whatsapp":
|
||||||
return transports.WhatsApp(self)
|
return transports.WhatsApp(self)
|
||||||
|
elif self.kind == "apprise":
|
||||||
|
return transports.Apprise(self)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Unknown channel kind: %s" % self.kind)
|
raise NotImplementedError("Unknown channel kind: %s" % self.kind)
|
||||||
|
|
||||||
|
@ -7,8 +7,9 @@ from django.core import mail
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from hc.api.models import Channel, Check, Notification
|
from hc.api.models import Channel, Check, Notification
|
||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
from mock import patch
|
from mock import patch, Mock
|
||||||
from requests.exceptions import ConnectionError, Timeout
|
from requests.exceptions import ConnectionError, Timeout
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
|
||||||
class NotifyTestCase(BaseTestCase):
|
class NotifyTestCase(BaseTestCase):
|
||||||
@ -636,3 +637,41 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
|
|
||||||
n = Notification.objects.get()
|
n = Notification.objects.get()
|
||||||
self.assertTrue("Monthly message limit exceeded" in n.error)
|
self.assertTrue("Monthly message limit exceeded" in n.error)
|
||||||
|
|
||||||
|
@patch("apprise.Apprise")
|
||||||
|
@override_settings(APPRISE_ENABLED=True)
|
||||||
|
def test_apprise_enabled(self, mock_apprise):
|
||||||
|
self._setup_data("apprise", "123")
|
||||||
|
|
||||||
|
mock_aobj = Mock()
|
||||||
|
mock_aobj.add.return_value = True
|
||||||
|
mock_aobj.notify.return_value = True
|
||||||
|
mock_apprise.return_value = mock_aobj
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
self.assertEqual(Notification.objects.count(), 1)
|
||||||
|
|
||||||
|
self.check.status = "up"
|
||||||
|
self.assertEqual(Notification.objects.count(), 1)
|
||||||
|
|
||||||
|
@patch("apprise.Apprise")
|
||||||
|
@override_settings(APPRISE_ENABLED=False)
|
||||||
|
def test_apprise_disabled(self, mock_apprise):
|
||||||
|
self._setup_data("apprise", "123")
|
||||||
|
|
||||||
|
mock_aobj = Mock()
|
||||||
|
mock_aobj.add.return_value = True
|
||||||
|
mock_aobj.notify.return_value = True
|
||||||
|
mock_apprise.return_value = mock_aobj
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
self.assertEqual(Notification.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_not_implimented(self):
|
||||||
|
self._setup_data("webhook", "http://example")
|
||||||
|
self.channel.kind = "invalid"
|
||||||
|
try:
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
# Code should not reach here
|
||||||
|
assert False
|
||||||
|
except NotImplementedError:
|
||||||
|
# We expect to be here
|
||||||
|
assert True
|
||||||
|
@ -8,6 +8,12 @@ from urllib.parse import quote, urlencode
|
|||||||
from hc.accounts.models import Profile
|
from hc.accounts.models import Profile
|
||||||
from hc.lib import emails
|
from hc.lib import emails
|
||||||
|
|
||||||
|
try:
|
||||||
|
import apprise
|
||||||
|
except ImportError:
|
||||||
|
# Enforce
|
||||||
|
settings.APPRISE_ENABLED = False
|
||||||
|
|
||||||
|
|
||||||
def tmpl(template_name, **ctx):
|
def tmpl(template_name, **ctx):
|
||||||
template_path = "integrations/%s" % template_name
|
template_path = "integrations/%s" % template_name
|
||||||
@ -273,7 +279,7 @@ class PagerTree(HttpTransport):
|
|||||||
class PagerTeam(HttpTransport):
|
class PagerTeam(HttpTransport):
|
||||||
def notify(self, check):
|
def notify(self, check):
|
||||||
url = self.channel.value
|
url = self.channel.value
|
||||||
headers = {"Conent-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
payload = {
|
payload = {
|
||||||
"incident_key": str(check.code),
|
"incident_key": str(check.code),
|
||||||
"event_type": "trigger" if check.status == "down" else "resolve",
|
"event_type": "trigger" if check.status == "down" else "resolve",
|
||||||
@ -461,3 +467,22 @@ class Trello(HttpTransport):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return self.post(self.URL, params=params)
|
return self.post(self.URL, params=params)
|
||||||
|
|
||||||
|
class Apprise(HttpTransport):
|
||||||
|
def notify(self, check):
|
||||||
|
|
||||||
|
if not settings.APPRISE_ENABLED:
|
||||||
|
# Not supported and/or enabled
|
||||||
|
return "Apprise is disabled and/or not installed."
|
||||||
|
|
||||||
|
a = apprise.Apprise()
|
||||||
|
title = tmpl("apprise_title.html", check=check)
|
||||||
|
body = tmpl("apprise_description.html", check=check)
|
||||||
|
|
||||||
|
a.add(self.channel.value)
|
||||||
|
|
||||||
|
notify_type = apprise.NotifyType.SUCCESS \
|
||||||
|
if check.status == "up" else apprise.NotifyType.FAILURE
|
||||||
|
|
||||||
|
return "Failed" if not \
|
||||||
|
a.notify(body=body, title=title, notify_type=notify_type) else None
|
||||||
|
@ -159,3 +159,8 @@ class AddMatrixForm(forms.Form):
|
|||||||
self.cleaned_data["room_id"] = doc["room_id"]
|
self.cleaned_data["room_id"] = doc["room_id"]
|
||||||
|
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class AddAppriseForm(forms.Form):
|
||||||
|
error_css_class = "has-error"
|
||||||
|
url = forms.CharField(max_length=512)
|
||||||
|
29
hc/front/tests/test_add_apprise.py
Normal file
29
hc/front/tests/test_add_apprise.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from hc.api.models import Channel
|
||||||
|
from hc.test import BaseTestCase
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(APPRISE_ENABLED=True)
|
||||||
|
class AddAppriseTestCase(BaseTestCase):
|
||||||
|
def test_instructions_work(self):
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.get("/integrations/add_apprise/")
|
||||||
|
self.assertContains(r, "Integration Settings", status_code=200)
|
||||||
|
|
||||||
|
def test_it_works(self):
|
||||||
|
form = {"url": "json://example.org"}
|
||||||
|
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.post("/integrations/add_apprise/", form)
|
||||||
|
self.assertRedirects(r, "/integrations/")
|
||||||
|
|
||||||
|
c = Channel.objects.get()
|
||||||
|
self.assertEqual(c.kind, "apprise")
|
||||||
|
self.assertEqual(c.value, "json://example.org")
|
||||||
|
self.assertEqual(c.project, self.project)
|
||||||
|
|
||||||
|
@override_settings(APPRISE_ENABLED=False)
|
||||||
|
def test_it_requires_client_id(self):
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.get("/integrations/add_apprise/")
|
||||||
|
self.assertEqual(r.status_code, 404)
|
@ -43,6 +43,7 @@ channel_urls = [
|
|||||||
path("add_trello/", views.add_trello, name="hc-add-trello"),
|
path("add_trello/", views.add_trello, name="hc-add-trello"),
|
||||||
path("add_trello/settings/", views.trello_settings, name="hc-trello-settings"),
|
path("add_trello/settings/", views.trello_settings, name="hc-trello-settings"),
|
||||||
path("add_matrix/", views.add_matrix, name="hc-add-matrix"),
|
path("add_matrix/", views.add_matrix, name="hc-add-matrix"),
|
||||||
|
path("add_apprise/", views.add_apprise, name="hc-add-apprise"),
|
||||||
path("<uuid:code>/checks/", views.channel_checks, name="hc-channel-checks"),
|
path("<uuid:code>/checks/", views.channel_checks, name="hc-channel-checks"),
|
||||||
path("<uuid:code>/name/", views.update_channel_name, name="hc-channel-name"),
|
path("<uuid:code>/name/", views.update_channel_name, name="hc-channel-name"),
|
||||||
path("<uuid:code>/test/", views.send_test_notification, name="hc-channel-test"),
|
path("<uuid:code>/test/", views.send_test_notification, name="hc-channel-test"),
|
||||||
|
@ -44,6 +44,7 @@ from hc.front.forms import (
|
|||||||
ChannelNameForm,
|
ChannelNameForm,
|
||||||
EmailSettingsForm,
|
EmailSettingsForm,
|
||||||
AddMatrixForm,
|
AddMatrixForm,
|
||||||
|
AddAppriseForm,
|
||||||
)
|
)
|
||||||
from hc.front.schemas import telegram_callback
|
from hc.front.schemas import telegram_callback
|
||||||
from hc.front.templatetags.hc_extras import num_down_title, down_title, sortchecks
|
from hc.front.templatetags.hc_extras import num_down_title, down_title, sortchecks
|
||||||
@ -236,6 +237,7 @@ def index(request):
|
|||||||
"enable_pd": settings.PD_VENDOR_KEY is not None,
|
"enable_pd": settings.PD_VENDOR_KEY is not None,
|
||||||
"enable_trello": settings.TRELLO_APP_KEY is not None,
|
"enable_trello": settings.TRELLO_APP_KEY is not None,
|
||||||
"enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
|
"enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
|
||||||
|
"enable_apprise": settings.APPRISE_ENABLED is True,
|
||||||
"registration_open": settings.REGISTRATION_OPEN,
|
"registration_open": settings.REGISTRATION_OPEN,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,6 +612,7 @@ def channels(request):
|
|||||||
"enable_pd": settings.PD_VENDOR_KEY is not None,
|
"enable_pd": settings.PD_VENDOR_KEY is not None,
|
||||||
"enable_trello": settings.TRELLO_APP_KEY is not None,
|
"enable_trello": settings.TRELLO_APP_KEY is not None,
|
||||||
"enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
|
"enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
|
||||||
|
"enable_apprise": settings.APPRISE_ENABLED is True,
|
||||||
"use_payments": settings.USE_PAYMENTS,
|
"use_payments": settings.USE_PAYMENTS,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1325,6 +1328,32 @@ def add_matrix(request):
|
|||||||
return render(request, "integrations/add_matrix.html", ctx)
|
return render(request, "integrations/add_matrix.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def add_apprise(request):
|
||||||
|
if not settings.APPRISE_ENABLED:
|
||||||
|
raise Http404("apprise integration is not available")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = AddAppriseForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
channel = Channel(project=request.project, kind="apprise")
|
||||||
|
channel.value = form.cleaned_data["url"]
|
||||||
|
channel.save()
|
||||||
|
|
||||||
|
channel.assign_all_checks()
|
||||||
|
messages.success(request, "The Apprise integration has been added!")
|
||||||
|
return redirect("hc-channels")
|
||||||
|
else:
|
||||||
|
form = AddAppriseForm()
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
"page": "channels",
|
||||||
|
"project": request.project,
|
||||||
|
"form": form,
|
||||||
|
}
|
||||||
|
return render(request, "integrations/add_apprise.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def trello_settings(request):
|
def trello_settings(request):
|
||||||
|
@ -204,6 +204,10 @@ MATRIX_HOMESERVER = os.getenv("MATRIX_HOMESERVER")
|
|||||||
MATRIX_USER_ID = os.getenv("MATRIX_USER_ID")
|
MATRIX_USER_ID = os.getenv("MATRIX_USER_ID")
|
||||||
MATRIX_ACCESS_TOKEN = os.getenv("MATRIX_ACCESS_TOKEN")
|
MATRIX_ACCESS_TOKEN = os.getenv("MATRIX_ACCESS_TOKEN")
|
||||||
|
|
||||||
|
# Apprise
|
||||||
|
APPRISE_ENABLED = envbool("APPRISE_ENABLED", "False")
|
||||||
|
|
||||||
|
|
||||||
if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")):
|
if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")):
|
||||||
from .local_settings import *
|
from .local_settings import *
|
||||||
else:
|
else:
|
||||||
|
BIN
static/img/integrations/apprise.png
Normal file
BIN
static/img/integrations/apprise.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -69,6 +69,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif ch.kind == "webhook" %}
|
{% elif ch.kind == "webhook" %}
|
||||||
Webhook
|
Webhook
|
||||||
|
{% elif ch.kind == "apprise" %}
|
||||||
|
Apprise
|
||||||
{% elif ch.kind == "pushbullet" %}
|
{% elif ch.kind == "pushbullet" %}
|
||||||
Pushbullet
|
Pushbullet
|
||||||
{% elif ch.kind == "discord" %}
|
{% elif ch.kind == "discord" %}
|
||||||
@ -211,6 +213,17 @@
|
|||||||
|
|
||||||
<a href="{% url 'hc-add-webhook' %}" class="btn btn-primary">Add Integration</a>
|
<a href="{% url 'hc-add-webhook' %}" class="btn btn-primary">Add Integration</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if enable_apprise %}
|
||||||
|
<li>
|
||||||
|
<img src="{% static 'img/integrations/apprise.png' %}"
|
||||||
|
class="icon" alt="Pushover icon" />
|
||||||
|
|
||||||
|
<h2>Apprise</h2>
|
||||||
|
<p>Receive instant push notifications using Apprise; see <a href="https://github.com/caronc/apprise#popular-notification-services" >all of the supported services here</a>.</p>
|
||||||
|
|
||||||
|
<a href="{% url 'hc-add-apprise' %}" class="btn btn-primary">Add Integration</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% if enable_pushover %}
|
{% if enable_pushover %}
|
||||||
<li>
|
<li>
|
||||||
<img src="{% static 'img/integrations/po.png' %}"
|
<img src="{% static 'img/integrations/po.png' %}"
|
||||||
|
@ -432,6 +432,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if enable_apprise %}
|
||||||
|
<div class="col-md-2 col-sm-4 col-xs-6">
|
||||||
|
<div class="integration">
|
||||||
|
<img src="{% static 'img/integrations/apprise.png' %}" class="icon" alt="Apprise icon" />
|
||||||
|
<h3>Apprise<br><small>>Push Notifications</small></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row tour-section">
|
<div class="row tour-section">
|
||||||
|
49
templates/integrations/add_apprise.html
Normal file
49
templates/integrations/add_apprise.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load humanize static hc_extras %}
|
||||||
|
|
||||||
|
{% block title %}Add Apprise - {% site_name %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<h1>Apprise</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Identify as many Apprise URLs as you wish. You can use a comma (,) to identify
|
||||||
|
more than on URL if you wish to.
|
||||||
|
|
||||||
|
For a detailed list of all supported Apprise Notification URLs simply
|
||||||
|
<a href="https://github.com/caronc/apprise#popular-notification-services" >click here</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Integration Settings</h2>
|
||||||
|
|
||||||
|
<form method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="form-group {{ form.room_id.css_classes }}">
|
||||||
|
<label for="url" class="col-sm-2 control-label">Apprise URL</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input
|
||||||
|
id="url"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="url"
|
||||||
|
value="{{ form.url.value|default:"" }}">
|
||||||
|
|
||||||
|
{% if form.url.errors %}
|
||||||
|
<div class="help-block">
|
||||||
|
{{ form.url.errors|join:"" }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-primary">Save Integration</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
5
templates/integrations/apprise_description.html
Normal file
5
templates/integrations/apprise_description.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% load humanize %}
|
||||||
|
{{ check.name_then_code }} is {{ check.status|upper }}.
|
||||||
|
{% if check.status == "down" %}
|
||||||
|
Last ping was {{ check.last_ping|naturaltime }}.
|
||||||
|
{% endif %}
|
1
templates/integrations/apprise_title.html
Normal file
1
templates/integrations/apprise_title.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
{{ check.name_then_code }} is {{ check.status|upper }}
|
Loading…
x
Reference in New Issue
Block a user