forked from GithubBackups/healthchecks
Use PagerDuty Connect.
This commit is contained in:
parent
b3395f1314
commit
96e00df0ab
@ -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()
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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="alice@example.org", 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="alice@example.org", 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="alice@example.org", password="password")
|
||||
self.client.post(self.url, form)
|
||||
url = "/integrations/add_pd/XXXXXXXXXXXX/?service_key=123"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.value, "123456")
|
||||
@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)
|
||||
|
@ -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"),
|
||||
|
@ -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):
|
||||
|
@ -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
Normal file
BIN
static/img/integrations/pd_connect_button.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 93 KiB |
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 90 KiB |
BIN
static/img/integrations/setup_pd_3.png
Normal file
BIN
static/img/integrations/setup_pd_3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
@ -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" />
|
||||
|
@ -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>
|
||||
|
@ -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 > Services</strong>,
|
||||
and click on <strong>Add New Service</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>
|
||||
<p>
|
||||
Give it a descriptive name, and
|
||||
for Integration Type select
|
||||
<strong>Use our API directly</strong>.
|
||||
</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>
|
||||
<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="col-sm-6">
|
||||
<img
|
||||
class="ai-guide-screenshot"
|
||||
alt="Screenshot"
|
||||
src="{% static 'img/integrations/setup_pd_3.png' %}">
|
||||
</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>
|
||||
</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 %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user