From 25fb11bb3e480e998c11d6a4a51fe476b1ebe851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Thu, 13 Jul 2017 23:47:54 +0300 Subject: [PATCH] Alerts to SMS, work in progress. --- hc/api/models.py | 5 ++- hc/api/transports.py | 21 ++++++++++ hc/front/forms.py | 10 +++++ hc/front/tests/test_add_sms.py | 46 ++++++++++++++++++++++ hc/front/urls.py | 1 + hc/front/views.py | 27 ++++++++++++- hc/settings.py | 5 +++ static/img/integrations/sms.png | Bin 0 -> 4011 bytes templates/front/channels.html | 11 ++++++ templates/front/welcome.html | 9 +++++ templates/integrations/add_discord.html | 7 ---- templates/integrations/add_email.html | 9 ----- templates/integrations/add_hipchat.html | 9 ----- templates/integrations/add_opsgenie.html | 9 ----- templates/integrations/add_pd.html | 9 ----- templates/integrations/add_sms.html | 44 +++++++++++++++++++++ templates/integrations/add_telegram.html | 9 ----- templates/integrations/add_victorops.html | 9 ----- templates/integrations/add_webhook.html | 9 ----- templates/integrations/sms_message.html | 1 + 20 files changed, 177 insertions(+), 73 deletions(-) create mode 100644 hc/front/tests/test_add_sms.py create mode 100644 static/img/integrations/sms.png create mode 100644 templates/integrations/add_sms.html create mode 100644 templates/integrations/sms_message.html diff --git a/hc/api/models.py b/hc/api/models.py index 5ce384f3..93c1b149 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -36,7 +36,8 @@ CHANNEL_KINDS = (("email", "Email"), ("opsgenie", "OpsGenie"), ("victorops", "VictorOps"), ("discord", "Discord"), - ("telegram", "Telegram")) + ("telegram", "Telegram"), + ("sms", "SMS")) PO_PRIORITIES = { -2: "lowest", @@ -270,6 +271,8 @@ class Channel(models.Model): return transports.Discord(self) elif self.kind == "telegram": return transports.Telegram(self) + elif self.kind == "sms": + return transports.Sms(self) else: raise NotImplementedError("Unknown channel kind: %s" % self.kind) diff --git a/hc/api/transports.py b/hc/api/transports.py index b32ef980..c6e06e6a 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -296,3 +296,24 @@ class Telegram(HttpTransport): def notify(self, check): text = tmpl("telegram_message.html", check=check) return self.send(self.channel.telegram_id, text) + + +class Sms(HttpTransport): + URL = 'https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json' + + def is_noop(self, check): + return check.status != "down" + + def notify(self, check): + url = self.URL % settings.TWILIO_ACCOUNT + auth = (settings.TWILIO_ACCOUNT, settings.TWILIO_AUTH) + text = tmpl("sms_message.html", check=check, + site_name=settings.SITE_NAME) + + data = { + 'From': settings.TWILIO_FROM, + 'To': self.channel.value, + 'Body': text, + } + + return self.post(url, data=data, auth=auth) diff --git a/hc/front/forms.py b/hc/front/forms.py index 3721adb1..79c5e3a4 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.core.validators import RegexValidator from hc.front.validators import (CronExpressionValidator, TimezoneValidator, WebhookValidator) @@ -64,3 +65,12 @@ class AddWebhookForm(forms.Form): def get_value(self): d = self.cleaned_data return "\n".join((d["value_down"], d["value_up"], d["post_data"])) + + +phone_validator = RegexValidator(regex='^\+\d{5,15}$', + message="Invalid phone number format.") + + +class AddSmsForm(forms.Form): + error_css_class = "has-error" + value = forms.CharField(max_length=16, validators=[phone_validator]) diff --git a/hc/front/tests/test_add_sms.py b/hc/front/tests/test_add_sms.py new file mode 100644 index 00000000..d92c7797 --- /dev/null +++ b/hc/front/tests/test_add_sms.py @@ -0,0 +1,46 @@ +from django.test.utils import override_settings +from hc.api.models import Channel +from hc.test import BaseTestCase + + +@override_settings(TWILIO_ACCOUNT="foo", TWILIO_AUTH="foo", TWILIO_FROM="123") +class AddSmsTestCase(BaseTestCase): + url = "/integrations/add_sms/" + + def test_instructions_work(self): + self.client.login(username="alice@example.org", password="password") + r = self.client.get(self.url) + self.assertContains(r, "Get a SMS message") + + def test_it_creates_channel(self): + form = {"value": "+1234567890"} + + self.client.login(username="alice@example.org", password="password") + r = self.client.post(self.url, form) + self.assertRedirects(r, "/integrations/") + + c = Channel.objects.get() + self.assertEqual(c.kind, "sms") + self.assertEqual(c.value, "+1234567890") + + def test_it_rejects_bad_number(self): + form = {"value": "not a phone number address"} + + self.client.login(username="alice@example.org", password="password") + r = self.client.post(self.url, form) + self.assertContains(r, "Invalid phone number format.") + + def test_it_trims_whitespace(self): + form = {"value": " +1234567890 "} + + self.client.login(username="alice@example.org", password="password") + self.client.post(self.url, form) + + c = Channel.objects.get() + self.assertEqual(c.value, "+1234567890") + + @override_settings(TWILIO_AUTH=None) + def test_it_requires_credentials(self): + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/integrations/add_sms/") + self.assertEqual(r.status_code, 404) diff --git a/hc/front/urls.py b/hc/front/urls.py index a6ce7df1..23553286 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -26,6 +26,7 @@ channel_urls = [ url(r'^add_victorops/$', views.add_victorops, name="hc-add-victorops"), url(r'^telegram/bot/$', views.telegram_bot), url(r'^add_telegram/$', views.add_telegram, name="hc-add-telegram"), + url(r'^add_sms/$', views.add_sms, name="hc-add-sms"), url(r'^([\w-]+)/checks/$', views.channel_checks, name="hc-channel-checks"), url(r'^([\w-]+)/remove/$', views.remove_channel, name="hc-remove-channel"), url(r'^([\w-]+)/verify/([\w-]+)/$', views.verify_email, diff --git a/hc/front/views.py b/hc/front/views.py index e38ccf0c..1eef280c 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -25,7 +25,7 @@ from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, from hc.api.transports import Telegram from hc.front.forms import (AddWebhookForm, NameTagsForm, TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm, - AddOpsGenieForm, CronForm) + AddOpsGenieForm, CronForm, AddSmsForm) from hc.front.schemas import telegram_callback from hc.lib import jsonschema from pytz import all_timezones @@ -106,6 +106,7 @@ def index(request): "enable_pushover": settings.PUSHOVER_API_TOKEN is not None, "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, "registration_open": settings.REGISTRATION_OPEN } @@ -350,7 +351,8 @@ def channels(request): "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None, "enable_discord": settings.DISCORD_CLIENT_ID is not None, - "enable_telegram": settings.TELEGRAM_TOKEN is not None + "enable_telegram": settings.TELEGRAM_TOKEN is not None, + "enable_sms": settings.TWILIO_AUTH is not None } return render(request, "front/channels.html", ctx) @@ -811,6 +813,27 @@ def add_telegram(request): return render(request, "integrations/add_telegram.html", ctx) +@login_required +def add_sms(request): + if settings.TWILIO_AUTH is None: + raise Http404("sms integration is not available") + + if request.method == "POST": + form = AddSmsForm(request.POST) + if form.is_valid(): + channel = Channel(user=request.team.user, kind="sms") + channel.value = form.cleaned_data["value"] + channel.save() + + channel.assign_all_checks() + return redirect("hc-channels") + else: + form = AddSmsForm() + + ctx = {"page": "channels", "form": form} + return render(request, "integrations/add_sms.html", ctx) + + def privacy(request): return render(request, "front/privacy.html", {}) diff --git a/hc/settings.py b/hc/settings.py index c2c49766..1cd71b5a 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -157,6 +157,11 @@ PUSHBULLET_CLIENT_SECRET = None TELEGRAM_BOT_NAME = "ExampleBot" TELEGRAM_TOKEN = None +# SMS (Twilio) integration -- override in local_settings.py +TWILIO_ACCOUNT = None +TWILIO_AUTH = None +TWILIO_FROM = 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/sms.png b/static/img/integrations/sms.png new file mode 100644 index 0000000000000000000000000000000000000000..8100074aea24e02bd47d88deecff50ab858001b2 GIT binary patch literal 4011 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 Integration + {% if enable_sms %} +
  • + SMS icon + +

    SMS

    +

    Get a text message to your phone when check goes down.

    + + Add Integration +
  • + {% endif %}
  • Webhook icon diff --git a/templates/front/welcome.html b/templates/front/welcome.html index 506ab88f..b8657c22 100644 --- a/templates/front/welcome.html +++ b/templates/front/welcome.html @@ -236,6 +236,15 @@ Good old email messages. + {% if enable_sms %} + + + Email icon + + SMS text messages. + + {% endif %} + Webhook icon diff --git a/templates/integrations/add_discord.html b/templates/integrations/add_discord.html index 92d5f1bb..0e15524b 100644 --- a/templates/integrations/add_discord.html +++ b/templates/integrations/add_discord.html @@ -28,10 +28,3 @@ {% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} -{% endblock %} diff --git a/templates/integrations/add_email.html b/templates/integrations/add_email.html index d181e9e9..4e2ff728 100644 --- a/templates/integrations/add_email.html +++ b/templates/integrations/add_email.html @@ -52,13 +52,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/add_hipchat.html b/templates/integrations/add_hipchat.html index 3f614edd..edb30b8e 100644 --- a/templates/integrations/add_hipchat.html +++ b/templates/integrations/add_hipchat.html @@ -92,13 +92,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/add_opsgenie.html b/templates/integrations/add_opsgenie.html index 8beef295..731a90dc 100644 --- a/templates/integrations/add_opsgenie.html +++ b/templates/integrations/add_opsgenie.html @@ -90,13 +90,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/add_pd.html b/templates/integrations/add_pd.html index 73d1f930..46e2b4e7 100644 --- a/templates/integrations/add_pd.html +++ b/templates/integrations/add_pd.html @@ -91,13 +91,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/add_sms.html b/templates/integrations/add_sms.html new file mode 100644 index 00000000..f8d44b86 --- /dev/null +++ b/templates/integrations/add_sms.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% load compress humanize staticfiles hc_extras %} + +{% block title %}Notification Channels - {% site_name %}{% endblock %} + + +{% block content %} +
    +
    +

    SMS

    + +

    Get a SMS message to your specified number when check goes down.

    + +

    Integration Settings

    + +
    + {% csrf_token %} +
    + +
    + + + {% if form.value.errors %} +
    + {{ form.value.errors|join:"" }} +
    + {% endif %} +
    +
    +
    +
    + +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/integrations/add_telegram.html b/templates/integrations/add_telegram.html index 836795e2..41451b37 100644 --- a/templates/integrations/add_telegram.html +++ b/templates/integrations/add_telegram.html @@ -89,16 +89,7 @@ src="{% static 'img/integrations/setup_telegram_3.png' %}"> - - {% endif %} {% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} -{% endblock %} diff --git a/templates/integrations/add_victorops.html b/templates/integrations/add_victorops.html index cd485853..753323a7 100644 --- a/templates/integrations/add_victorops.html +++ b/templates/integrations/add_victorops.html @@ -105,13 +105,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/add_webhook.html b/templates/integrations/add_webhook.html index 4ef91b01..1f388b34 100644 --- a/templates/integrations/add_webhook.html +++ b/templates/integrations/add_webhook.html @@ -113,13 +113,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/sms_message.html b/templates/integrations/sms_message.html new file mode 100644 index 00000000..1fdb6c4c --- /dev/null +++ b/templates/integrations/sms_message.html @@ -0,0 +1 @@ +{% load humanize %}{{ site_name }}: The check "{{ check.name_then_code }}" is DOWN. Last ping was {{ check.last_ping|naturaltime }}. \ No newline at end of file