Browse Source

Use PagerDuty Connect.

pull/133/head
Pēteris Caune 7 years ago
parent
commit
96e00df0ab
15 changed files with 182 additions and 90 deletions
  1. +16
    -0
      hc/api/models.py
  2. +14
    -0
      hc/api/tests/test_notify.py
  3. +3
    -2
      hc/api/transports.py
  4. +0
    -5
      hc/front/forms.py
  5. +22
    -12
      hc/front/tests/test_add_pd.py
  6. +1
    -0
      hc/front/urls.py
  7. +44
    -21
      hc/front/views.py
  8. +3
    -0
      hc/settings.py
  9. BIN
      static/img/integrations/pd_connect_button.png
  10. BIN
      static/img/integrations/setup_pd_1.png
  11. BIN
      static/img/integrations/setup_pd_2.png
  12. BIN
      static/img/integrations/setup_pd_3.png
  13. +8
    -2
      templates/front/channels.html
  14. +2
    -0
      templates/front/welcome.html
  15. +69
    -48
      templates/integrations/add_pd.html

+ 16
- 0
hc/api/models.py View File

@ -410,6 +410,22 @@ class Channel(models.Model):
tmpl = "https://api.hipchat.com/v2/room/%s/notification?auth_token=%s"
return tmpl % (doc["roomId"], doc.get("access_token"))
@property
def pd_service_key(self):
assert self.kind == "pd"
if not self.value.startswith("{"):
return self.value
doc = json.loads(self.value)
return doc["service_key"]
@property
def pd_account(self):
assert self.kind == "pd"
if self.value.startswith("{"):
doc = json.loads(self.value)
return doc["account"]
def latest_notification(self):
return Notification.objects.filter(channel=self).latest()


+ 14
- 0
hc/api/tests/test_notify.py View File

@ -178,6 +178,20 @@ class NotifyTestCase(BaseTestCase):
args, kwargs = mock_post.call_args
payload = kwargs["json"]
self.assertEqual(payload["event_type"], "trigger")
self.assertEqual(payload["service_key"], "123")
@patch("hc.api.transports.requests.request")
def test_pd_complex(self, mock_post):
self._setup_data("pd", json.dumps({"service_key": "456"}))
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["json"]
self.assertEqual(payload["event_type"], "trigger")
self.assertEqual(payload["service_key"], "456")
@patch("hc.api.transports.requests.request")
def test_slack(self, mock_post):


+ 3
- 2
hc/api/transports.py View File

@ -203,11 +203,12 @@ class PagerDuty(HttpTransport):
def notify(self, check):
description = tmpl("pd_description.html", check=check)
payload = {
"service_key": self.channel.value,
"vendor": settings.PD_VENDOR_KEY,
"service_key": self.channel.pd_service_key,
"incident_key": str(check.code),
"event_type": "trigger" if check.status == "down" else "resolve",
"description": description,
"client": "healthchecks.io",
"client": settings.SITE_NAME,
"client_url": settings.SITE_ROOT
}


+ 0
- 5
hc/front/forms.py View File

@ -31,11 +31,6 @@ class CronForm(forms.Form):
grace = forms.IntegerField(min_value=1, max_value=43200)
class AddPdForm(forms.Form):
error_css_class = "has-error"
value = forms.CharField(max_length=32)
class AddOpsGenieForm(forms.Form):
error_css_class = "has-error"
value = forms.CharField(max_length=40)


+ 22
- 12
hc/front/tests/test_add_pd.py View File

@ -1,32 +1,42 @@
from django.test.utils import override_settings
from hc.api.models import Channel
from hc.test import BaseTestCase
@override_settings(PD_VENDOR_KEY="foo")
class AddPdTestCase(BaseTestCase):
url = "/integrations/add_pd/"
def test_instructions_work(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertContains(r, "incident management system")
self.assertContains(r, "If your team uses")
def test_it_works(self):
# Integration key is 32 characters long
form = {"value": "12345678901234567890123456789012"}
session = self.client.session
session["pd"] = "1234567890AB" # 12 characters
session.save()
self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, form)
self.assertRedirects(r, "/integrations/")
url = "/integrations/add_pd/1234567890AB/?service_key=123"
r = self.client.get(url)
self.assertEqual(r.status_code, 302)
c = Channel.objects.get()
self.assertEqual(c.kind, "pd")
self.assertEqual(c.value, "12345678901234567890123456789012")
self.assertEqual(c.pd_service_key, "123")
def test_it_trims_whitespace(self):
form = {"value": " 123456 "}
def test_it_validates_code(self):
session = self.client.session
session["pd"] = "1234567890AB" # 12 characters
session.save()
self.client.login(username="[email protected]", password="password")
self.client.post(self.url, form)
c = Channel.objects.get()
self.assertEqual(c.value, "123456")
url = "/integrations/add_pd/XXXXXXXXXXXX/?service_key=123"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
@override_settings(PD_VENDOR_KEY=None)
def test_it_requires_vendor_key(self):
r = self.client.get("/integrations/add_pd/")
self.assertEqual(r.status_code, 404)

+ 1
- 0
hc/front/urls.py View File

@ -16,6 +16,7 @@ channel_urls = [
url(r'^add_email/$', views.add_email, name="hc-add-email"),
url(r'^add_webhook/$', views.add_webhook, name="hc-add-webhook"),
url(r'^add_pd/$', views.add_pd, name="hc-add-pd"),
url(r'^add_pd/([\w]{12})/$', views.add_pd, name="hc-add-pd-state"),
url(r'^add_slack/$', views.add_slack, name="hc-add-slack"),
url(r'^add_slack_btn/$', views.add_slack_btn, name="hc-add-slack-btn"),
url(r'^add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"),


+ 44
- 21
hc/front/views.py View File

@ -24,7 +24,7 @@ from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check,
Ping, Notification)
from hc.api.transports import Telegram
from hc.front.forms import (AddWebhookForm, NameTagsForm,
TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm,
TimeoutForm, AddUrlForm, AddEmailForm,
AddOpsGenieForm, CronForm, AddSmsForm)
from hc.front.schemas import telegram_callback
from hc.lib import jsonschema
@ -99,6 +99,7 @@ def index(request):
"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,
"enable_pd": settings.PD_VENDOR_KEY is not None,
"registration_open": settings.REGISTRATION_OPEN
}
@ -351,6 +352,7 @@ def channels(request):
"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,
"enable_pd": settings.PD_VENDOR_KEY is not None,
"added": request.GET.get("added")
}
@ -455,31 +457,13 @@ def add_webhook(request):
return render(request, "integrations/add_webhook.html", ctx)
@login_required
def add_pd(request):
if request.method == "POST":
form = AddPdForm(request.POST)
if form.is_valid():
channel = Channel(user=request.team.user, kind="pd")
channel.value = form.cleaned_data["value"]
channel.save()
channel.assign_all_checks()
return redirect("hc-channels")
else:
form = AddPdForm()
ctx = {"page": "channels", "form": form}
return render(request, "integrations/add_pd.html", ctx)
def _prepare_state(request, session_key):
state = get_random_string()
request.session[session_key] = state
return state
def _get_validated_code(request, session_key):
def _get_validated_code(request, session_key, key="code"):
if session_key not in request.session:
return None
@ -488,7 +472,46 @@ def _get_validated_code(request, session_key):
if session_state is None or session_state != request_state:
return None
return request.GET.get("code")
return request.GET.get(key)
def add_pd(request, state=None):
if settings.PD_VENDOR_KEY is None:
raise Http404("pagerduty integration is not available")
if state and request.user.is_authenticated():
if "pd" not in request.session:
return HttpResponseBadRequest()
session_state = request.session.pop("pd")
if session_state != state:
return HttpResponseBadRequest()
if request.GET.get("error") == "cancelled":
messages.warning(request, "PagerDuty setup was cancelled")
return redirect("hc-channels")
channel = Channel()
channel.user = request.team.user
channel.kind = "pd"
channel.value = json.dumps({
"service_key": request.GET.get("service_key"),
"account": request.GET.get("account")
})
channel.save()
channel.assign_all_checks()
messages.success(request, "The PagerDuty integration has been added!")
return redirect("hc-channels")
state = _prepare_state(request, "pd")
callback = settings.SITE_ROOT + reverse("hc-add-pd-state", args=[state])
connect_url = "https://connect.pagerduty.com/connect?" + urlencode({
"vendor": settings.PD_VENDOR_KEY,
"callback": callback
})
ctx = {"page": "channels", "connect_url": connect_url}
return render(request, "integrations/add_pd.html", ctx)
def add_slack(request):


+ 3
- 0
hc/settings.py View File

@ -162,6 +162,9 @@ TWILIO_ACCOUNT = None
TWILIO_AUTH = None
TWILIO_FROM = None
# PagerDuty
PD_VENDOR_KEY = None
if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")):
from .local_settings import *
else:


BIN
static/img/integrations/pd_connect_button.png View File

Before After
Width: 299  |  Height: 55  |  Size: 6.5 KiB

BIN
static/img/integrations/setup_pd_1.png View File

Before After
Width: 635  |  Height: 659  |  Size: 67 KiB Width: 750  |  Height: 581  |  Size: 93 KiB

BIN
static/img/integrations/setup_pd_2.png View File

Before After
Width: 679  |  Height: 261  |  Size: 25 KiB Width: 821  |  Height: 461  |  Size: 90 KiB

BIN
static/img/integrations/setup_pd_3.png View File

Before After
Width: 976  |  Height: 394  |  Size: 36 KiB

+ 8
- 2
templates/front/channels.html View File

@ -40,8 +40,12 @@
<span class="channel-unconfirmed">(unconfirmed)</span>
{% endif %}
{% elif ch.kind == "pd" %}
<span class="preposition">API key</span>
{{ ch.value }}
{% if ch.pd_account %}
<span class="preposition">account</span>
{{ ch.pd_account}},
{% endif %}
<span class="preposition">service key</span>
{{ ch.pd_service_key }}
{% elif ch.kind == "opsgenie" %}
<span class="preposition">API key</span>
{{ ch.value }}
@ -215,6 +219,7 @@
<a href="{% url 'hc-add-telegram' %}" class="btn btn-primary">Add Integration</a>
</li>
{% endif %}
{% if enable_pd %}
<li>
<img src="{% static 'img/integrations/pd.png' %}"
class="icon" alt="PagerDuty icon" />
@ -224,6 +229,7 @@
<a href="{% url 'hc-add-pd' %}" class="btn btn-primary">Add Integration</a>
</li>
{% endif %}
<li>
<img src="{% static 'img/integrations/hipchat.png' %}"
class="icon" alt="HipChat icon" />


+ 2
- 0
templates/front/welcome.html View File

@ -310,12 +310,14 @@
</tr>
{% endif %}
{% if enable_pd %}
<tr>
<td>
<img width="22" height="22" alt="PagerDuty icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAIAAACR5s1WAAAEcElEQVRYw+1Ya0wcVRQ+9zEDKwtlWRp5tA1gISywtNH+tFqN0gZTU1MVTRtjfSRtolFTIw2VIi19aSPRvyZqfZDUxmj6UtKUaKPGGKOmlAUtYGpg2WV3h2IR2tmZe/wBuzu7MAPZwq4/PJlsZu+9c+53v3Pudx8EEcFg3Zc9Xd986/H0KWNjCVW3aISQPIejuqpyw73ra901cVXRnhRl7MCho/0Df8qyBEtpYU0rKylpeX2Pw5EbB2JgYHB3417GGKTKNE1rP3akrLRkBkRIGXvm+V2pRBDFceLTD202GwWA/QePpB4BAHDOd734CgDw7ss9/f2DS50HZjY+/rfP52flla6h4WFIn13p72fFq0onJ6fSCCIUUriiKAAIaTUuRJoRAABfXFlMEsRiOTIbDCHklphAJI6KgGS/mViu08CvxYQJY+E96++22WwJHWq69tXX5yWJz8uEKYjcshCTtZJNfTwzHD84zHd7PcfXUT6Do6K8XFFCrfv2yrKc4MRdXXWs/R1rMaSIYPYInbq2//zHiTU3lNtuGp6poN3346qaZ38SGpluCQCH21obm/aN+PxDw97oc/XqX53nL+x5bbeuC4uOuOX8RKFT1/ZfLr5azzI1Y5g2tJ8WGlu+1hvsLgAABASAQwdaNm1+JMNABufSuVOfA8BHn3T4fH6LnLDItZlfwgXhhgwQBDUGCCjIzOc4vSDpsiRLkhFEJBuQWHRkxQRGqhBxzmYIkXIEk5Zm7wtmgmDsa2Oz2e8xuPFTFWMeMFkmYj5mMUEwwjDGOiMz/+YYPSbLBBAgBMYHHVmF15msx1JCp4FLty+v9cdwEgQAj6eXMWp0qOt6j6evuqoSLJkgGx/aYlaXVfBPxrKp1Q//bnMmLrPqhNzb4SYMx67kAcCK4mJVVX2jo3SWPgohVq5cEVbV0UAwmXAQohOKE95syR4mhmaIZMKbfX0oR1dphGiRk5Pt8/uA0FlOIHdZTiAQSjIcVBbuHb+pE3LXy3XGcAABJmtgGHNWlv3dt98MBIIN2542ijSl9MyXn3HOdzy3M8nEBEBEQrmgsmYUq7mnM0BmZoZAHYAZQJCIVGCyiYlRnYB5FvxoS2EyRS09WIsVMQx04duORRUrALFAJjDG2aKLlQBTsbJaa4zzSEQEQ1gzgZZihYEeJ5U1613gdGVLa9vFrs64Y2c4vPOFl558/NGRkRGLLRYrvaPCrM7muEFlcemDGsLmoSHf6fzu+x/eOnow0TtjRQWFTc2tZJZ+xA32/rp6U53gQlcZoQsJBD7R8FhYVROFVVVPnTlHKZ3n1uC+B+v/C7vt/88d03FfyLlgyZlw5uUFgsE0M7F2TW16EbgqK+nGugdUVcX0WXNTI113153l5avTRYPdbi8qKiSIGAwpm7dslaRU3xgRQi50nuWcUwDId+Ydf/+9cDicyigAwMmOj6e3PLHL1MnJyYZtT127Np6aKJz+4mT0fEYSxGp42Lu/7XBPb++S8A/gcrneaG4qKio0lv8LIahMKT/4QwUAAAAASUVORK5CYII=" />
</td>
<td>Open and resolve incidents in <a href="https://www.pagerduty.com/">PagerDuty</a>.</td>
</tr>
{% endif %}
<tr>
<td>


+ 69
- 48
templates/integrations/add_pd.html View File

@ -9,27 +9,66 @@
<div class="col-sm-12">
<h1>PagerDuty</h1>
<p><a href="https://www.pagerduty.com/">PagerDuty</a> is
a well-known incident management system. It provides
alerting, on-call scheduling, escalation policies and incident tracking.
If you use or plan on using PagerDuty, you can can integrate it
with your {% site_name %} account in few simple steps.</p>
<div class="jumbotron">
{% if request.user.is_authenticated %}
<p>If your team uses <a href="https://www.pagerduty.com">PagerDuty</a>,
you can set up {% site_name %} to create a PagerDuty incident when
a check goes down, and resolve it when a check goes back up.</p>
<div class="text-center">
<div class="text-center">
<a href="{{ connect_url|safe }}">
<img
alt="Alert with PagerDuty"
height="55" width="299"
src="{% static 'img/integrations/pd_connect_button.png' %}" />
</a>
</div>
</div>
{% else %}
<p>
{% site_name %} is a <strong>free</strong> and
<a href="https://github.com/healthchecks/healthchecks">open source</a>
service for monitoring your cron jobs, background processes and
scheduled tasks. Before adding PagerDuty integration, please log into
{% site_name %}:</p>
<div class="text-center">
<form class="form-inline" action="{% url 'hc-login' %}" method="post">
{% csrf_token %}
<div class="form-group">
<div class="input-group input-group-lg">
<div class="input-group-addon">@</div>
<input
type="email"
class="form-control"
name="email"
autocomplete="email"
placeholder="Email">
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-lg btn-primary pull-right">
Log In
</button>
</div>
</form>
</div>
{% endif %}
</div>
<h2>Setup Guide</h2>
<div class="row ai-step">
<div class="col-sm-6">
<span class="step-no">1</span>
<p>
Log into your PagerDuty account,
go to <strong>Configuration &gt; Services</strong>,
and click on <strong>Add New Service</strong>.
</p>
<p>
Give it a descriptive name, and
for Integration Type select
<strong>Use our API directly</strong>.
After {% if request.user.is_authenticated %}{% else %}logging in and{% endif %}
clicking on "Alert with PagerDuty", you will be
asked to log into your PagerDuty account.
</p>
</div>
<div class="col-sm-6">
<img
@ -38,13 +77,14 @@
src="{% static 'img/integrations/setup_pd_1.png' %}">
</div>
</div>
<div class="row ai-step">
<div class="col-sm-6">
<span class="step-no">2</span>
After adding the new service, take note of its
<strong>Integration Key</strong>, a long string
of letters and digits.
<p>
Next, PagerDuty will let set the name and escalation policy
for this integration.
</p>
</div>
<div class="col-sm-6">
<img
@ -57,38 +97,19 @@
<div class="row ai-step">
<div class="col-sm-6">
<span class="step-no">3</span>
<p>Paste the Integration Key down below. Save the integration, and it's done!</p>
</div>
</div>
<h2>Integration Settings</h2>
<form method="post" class="form-horizontal" action="{% url 'hc-add-pd' %}">
{% csrf_token %}
<div class="form-group {{ form.value.css_classes }}">
<label for="api-key" class="col-sm-2 control-label">Integration Key</label>
<div class="col-sm-4">
<input
id="api-key"
type="text"
class="form-control"
name="value"
placeholder=""
value="{{ form.value.value|default:"" }}">
{% if form.value.errors %}
<div class="help-block">
{{ form.value.errors|join:"" }}
</div>
{% endif %}
</div>
<p>
And that is all! You will then be redirected back to
"Integrations" page on {% site_name %} and see
the new integration!
</p>
</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 class="col-sm-6">
<img
class="ai-guide-screenshot"
alt="Screenshot"
src="{% static 'img/integrations/setup_pd_3.png' %}">
</div>
</form>
</div>
</div>
</div>
{% endblock %}

Loading…
Cancel
Save