forked from GithubBackups/healthchecks
For HipChat integration, use HipChat's server-side installation flow.
This commit is contained in:
parent
de7160a0e6
commit
bef71c0acc
@ -2,6 +2,7 @@
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime, timedelta as td
|
||||
|
||||
@ -14,6 +15,7 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from hc.api import transports
|
||||
from hc.lib import emails
|
||||
import requests
|
||||
|
||||
STATUSES = (
|
||||
("up", "Up"),
|
||||
@ -377,6 +379,37 @@ class Channel(models.Model):
|
||||
doc = json.loads(self.value)
|
||||
return doc.get("name")
|
||||
|
||||
def refresh_hipchat_access_token(self):
|
||||
assert self.kind == "hipchat"
|
||||
if not self.value.startswith("{"):
|
||||
return # Don't have OAuth credentials
|
||||
|
||||
doc = json.loads(self.value)
|
||||
if time.time() < doc.get("expires_at", 0):
|
||||
return # Current access token is still valid
|
||||
|
||||
url = "https://api.hipchat.com/v2/oauth/token"
|
||||
auth = (doc["oauthId"], doc["oauthSecret"])
|
||||
r = requests.post(url, auth=auth, data={
|
||||
"grant_type": "client_credentials",
|
||||
"scope": "send_notification"
|
||||
})
|
||||
|
||||
doc.update(r.json())
|
||||
doc["expires_at"] = int(time.time()) + doc["expires_in"] - 300
|
||||
self.value = json.dumps(doc)
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def hipchat_webhook_url(self):
|
||||
assert self.kind == "hipchat"
|
||||
if not self.value.startswith("{"):
|
||||
return self.value
|
||||
|
||||
doc = json.loads(self.value)
|
||||
tmpl = "https://api.hipchat.com/v2/room/%s/notification?auth_token=%s"
|
||||
return tmpl % (doc["roomId"], doc.get("access_token"))
|
||||
|
||||
def latest_notification(self):
|
||||
return Notification.objects.filter(channel=self).latest()
|
||||
|
||||
|
21
hc/api/tests/test_channel_model.py
Normal file
21
hc/api/tests/test_channel_model.py
Normal file
@ -0,0 +1,21 @@
|
||||
import json
|
||||
|
||||
from hc.api.models import Channel
|
||||
from hc.test import BaseTestCase
|
||||
from mock import patch
|
||||
|
||||
|
||||
class ChannelModelTestCase(BaseTestCase):
|
||||
|
||||
@patch("hc.api.models.requests.post")
|
||||
def test_it_refreshes_hipchat_access_token(self, mock_post):
|
||||
mock_post.return_value.json.return_value = {"expires_in": 100}
|
||||
|
||||
channel = Channel(kind="hipchat", user=self.alice, value=json.dumps({
|
||||
"oauthId": "foo",
|
||||
"oauthSecret": "bar"
|
||||
}))
|
||||
|
||||
channel.refresh_hipchat_access_token()
|
||||
self.assertTrue(mock_post.return_value.json.called)
|
||||
self.assertTrue("expires_at" in channel.value)
|
@ -175,7 +175,9 @@ class HipChat(HttpTransport):
|
||||
"message": text,
|
||||
"color": "green" if check.status == "up" else "red",
|
||||
}
|
||||
return self.post(self.channel.value, json=payload)
|
||||
|
||||
self.channel.refresh_hipchat_access_token()
|
||||
return self.post(self.channel.hipchat_webhook_url, json=payload)
|
||||
|
||||
|
||||
class OpsGenie(HttpTransport):
|
||||
|
@ -1,5 +1,9 @@
|
||||
import json
|
||||
|
||||
from django.core import signing
|
||||
from hc.api.models import Channel
|
||||
from hc.test import BaseTestCase
|
||||
from mock import patch
|
||||
|
||||
|
||||
class AddHipChatTestCase(BaseTestCase):
|
||||
@ -10,29 +14,38 @@ class AddHipChatTestCase(BaseTestCase):
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "appropriate HipChat room")
|
||||
|
||||
def test_it_works(self):
|
||||
form = {"value": "http://example.org"}
|
||||
def test_instructions_work_when_logged_out(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "Before adding HipChat integration, please")
|
||||
|
||||
def test_it_redirects_to_addons_install(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertRedirects(r, "/integrations/")
|
||||
r = self.client.post(self.url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
def test_it_returns_capabilities(self):
|
||||
r = self.client.get("/integrations/hipchat/capabilities/")
|
||||
self.assertContains(r, "callbackUrl")
|
||||
|
||||
@patch("hc.api.models.Channel.refresh_hipchat_access_token")
|
||||
def test_callback_works(self, mock_refresh):
|
||||
state = signing.TimestampSigner().sign("alice")
|
||||
payload = json.dumps({"relayState": state, "foo": "foobar"})
|
||||
|
||||
r = self.client.post("/integrations/hipchat/callback/", payload,
|
||||
content_type="application/json")
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.kind, "hipchat")
|
||||
self.assertEqual(c.value, "http://example.org")
|
||||
self.assertTrue("foobar" in c.value)
|
||||
|
||||
def test_it_rejects_bad_url(self):
|
||||
form = {"value": "not an URL"}
|
||||
@patch("hc.api.models.Channel.refresh_hipchat_access_token")
|
||||
def test_callback_rejects_bad_signature(self, mock_refresh):
|
||||
payload = json.dumps({"relayState": "alice:bad:sig", "foo": "foobar"})
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertContains(r, "Enter a valid URL")
|
||||
r = self.client.post("/integrations/hipchat/callback/", payload,
|
||||
content_type="application/json")
|
||||
|
||||
def test_it_trims_whitespace(self):
|
||||
form = {"value": " http://example.org "}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
self.client.post(self.url, form)
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.value, "http://example.org")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
@ -19,6 +19,8 @@ channel_urls = [
|
||||
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"),
|
||||
url(r'^hipchat/capabilities/$', views.hipchat_capabilities, name="hc-hipchat-capabilities"),
|
||||
url(r'^hipchat/callback/$', views.hipchat_callback, name="hc-hipchat-callback"),
|
||||
url(r'^add_pushbullet/$', views.add_pushbullet, name="hc-add-pushbullet"),
|
||||
url(r'^add_discord/$', views.add_discord, name="hc-add-discord"),
|
||||
url(r'^add_pushover/$', views.add_pushover, name="hc-add-pushover"),
|
||||
|
@ -6,11 +6,12 @@ import json
|
||||
from croniter import croniter
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core import signing
|
||||
from django.db.models import Count
|
||||
from django.http import (Http404, HttpResponse, HttpResponseBadRequest,
|
||||
HttpResponseForbidden)
|
||||
HttpResponseForbidden, JsonResponse)
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
@ -552,24 +553,69 @@ def add_slack_btn(request):
|
||||
return redirect("hc-channels")
|
||||
|
||||
|
||||
@login_required
|
||||
def add_hipchat(request):
|
||||
if request.method == "POST":
|
||||
form = AddUrlForm(request.POST)
|
||||
if form.is_valid():
|
||||
channel = Channel(user=request.team.user, kind="hipchat")
|
||||
channel.value = form.cleaned_data["value"]
|
||||
channel.save()
|
||||
username = request.team.user.username
|
||||
state = signing.TimestampSigner().sign(username)
|
||||
capabilities = settings.SITE_ROOT + reverse("hc-hipchat-capabilities")
|
||||
|
||||
channel.assign_all_checks()
|
||||
return redirect("hc-channels")
|
||||
else:
|
||||
form = AddUrlForm()
|
||||
url = "https://www.hipchat.com/addons/install?url=%s&relayState=%s" % \
|
||||
(capabilities, state)
|
||||
|
||||
ctx = {"page": "channels", "form": form}
|
||||
return redirect(url)
|
||||
|
||||
ctx = {"page": "channels"}
|
||||
return render(request, "integrations/add_hipchat.html", ctx)
|
||||
|
||||
|
||||
def hipchat_capabilities(request):
|
||||
return JsonResponse({
|
||||
"name": settings.SITE_NAME,
|
||||
"description": "Get Notified When Your Cron Jobs Fail",
|
||||
"key": "io.healthchecks.hipchat",
|
||||
"links": {
|
||||
"homepage": settings.SITE_ROOT,
|
||||
"self": settings.SITE_ROOT + reverse("hc-hipchat-capabilities")
|
||||
},
|
||||
"capabilities": {
|
||||
"installable": {
|
||||
"allowGlobal": False,
|
||||
"allowRoom": True,
|
||||
"callbackUrl":
|
||||
settings.SITE_ROOT + reverse("hc-hipchat-callback"),
|
||||
"installedUrl":
|
||||
settings.SITE_ROOT + reverse("hc-channels") + "?added=hipchat"
|
||||
},
|
||||
"hipchatApiConsumer": {
|
||||
"scopes": [
|
||||
"send_notification"
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def hipchat_callback(request):
|
||||
doc = json.loads(request.body.decode("utf-8"))
|
||||
try:
|
||||
signer = signing.TimestampSigner()
|
||||
username = signer.unsign(doc.get("relayState"), max_age=300)
|
||||
except signing.BadSignature:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
channel = Channel(kind="hipchat")
|
||||
channel.user = User.objects.get(username=username)
|
||||
channel.value = json.dumps(doc)
|
||||
channel.save()
|
||||
|
||||
channel.refresh_hipchat_access_token()
|
||||
channel.assign_all_checks()
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@login_required
|
||||
def add_pushbullet(request):
|
||||
if settings.PUSHBULLET_CLIENT_ID is None:
|
||||
|
@ -1,4 +1,4 @@
|
||||
.field-value {
|
||||
.results .field-value {
|
||||
max-width: 400px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -5,6 +5,7 @@
|
||||
.channels-table .channel-row > td {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.channels-table .value-cell {
|
||||
@ -166,7 +167,6 @@ table.channels-table > tbody > tr > th {
|
||||
}
|
||||
|
||||
.ai-guide-screenshot {
|
||||
border: ;
|
||||
max-width: 100%;
|
||||
border: 6px solid #EEE;
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
Binary file not shown.
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 32 KiB |
@ -89,6 +89,8 @@
|
||||
<span class="preposition">user</span>
|
||||
{% endif %}
|
||||
{{ ch.telegram_name }}
|
||||
{% elif ch.kind == "hipchat" %}
|
||||
{{ ch.hipchat_webhook_url }}
|
||||
{% else %}
|
||||
{{ ch.value }}
|
||||
{% endif %}
|
||||
|
@ -9,17 +9,64 @@
|
||||
<div class="col-sm-12">
|
||||
<h1>HipChat</h1>
|
||||
|
||||
<p>If your team uses <a href="https://www.hipchat.com/">HipChat</a>,
|
||||
you can set up {% site_name %} to post status updates directly to an
|
||||
appropriate HipChat room.</p>
|
||||
<div class="jumbotron">
|
||||
{% if request.user.is_authenticated %}
|
||||
<p>If your team uses <a href="https://www.hipchat.com/">HipChat</a>,
|
||||
you can set up {% site_name %} to post status updates directly to an
|
||||
appropriate HipChat room.</p>
|
||||
|
||||
<div class="text-center">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-lg btn-primary">
|
||||
Install HipChat Integration
|
||||
</button>
|
||||
</form>
|
||||
</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 HipChat 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>
|
||||
Log into your HipChat account and
|
||||
pick an appropriate room. From the options menu
|
||||
select <strong>Integrations...</strong>
|
||||
<span class="step-no">2</span>
|
||||
<p>
|
||||
After {% if request.user.is_authenticated %}{% else %}logging in and{% endif %}
|
||||
clicking on "Install HipChat Integration", you will be
|
||||
asked to log into HipChat.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<img
|
||||
@ -28,16 +75,13 @@
|
||||
src="{% static 'img/integrations/setup_hipchat_1.png' %}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row ai-step">
|
||||
<div class="col-sm-6">
|
||||
<span class="step-no">2</span>
|
||||
<span class="step-no">3</span>
|
||||
<p>
|
||||
From the list of available integrations, select
|
||||
<strong>Build Your Own</strong>. It's at the very top.
|
||||
</p>
|
||||
<p>
|
||||
Give it a descriptive name
|
||||
and click <strong>Create</strong>.
|
||||
Next, HipChat will let you select the chat room
|
||||
for receiving {% site_name %} notifications.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
@ -45,13 +89,20 @@
|
||||
class="ai-guide-screenshot"
|
||||
alt="Screenshot"
|
||||
src="{% static 'img/integrations/setup_hipchat_2.png' %}">
|
||||
</div> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row ai-step">
|
||||
<div class="col-sm-6">
|
||||
<span class="step-no">3</span>
|
||||
<p>Copy the displayed <strong>URL</strong> and paste it down below.</p>
|
||||
<p>Save the integration, and it's done!</p>
|
||||
<span class="step-no">4</span>
|
||||
<p>
|
||||
As the final step, HipChat will show you the permissions
|
||||
requested by {% site_name %}. There's only one permission
|
||||
needed–"Send Notification". After clicking on "Approve"
|
||||
you will be redirected back to
|
||||
"Integrations" page on {% site_name %} and see
|
||||
the new integration!
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<img
|
||||
@ -61,35 +112,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h2>Integration Settings</h2>
|
||||
|
||||
<form method="post" class="form-horizontal" action="{% url 'hc-add-hipchat' %}">
|
||||
{% csrf_token %}
|
||||
<div class="form-group {{ form.value.css_classes }}">
|
||||
<label for="callback-url" class="col-sm-2 control-label">Callback URL</label>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
id="callback-url"
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="value"
|
||||
placeholder="https://"
|
||||
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 %}
|
||||
|
@ -23,11 +23,11 @@
|
||||
|
||||
{% else %}
|
||||
<p>
|
||||
healthchecks.io is a <strong>free</strong> and
|
||||
{% 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 Slack integration, please log into
|
||||
{% site_root %}:</p>
|
||||
{% site_name %}:</p>
|
||||
|
||||
<div class="text-center">
|
||||
<form class="form-inline" action="{% url 'hc-login' %}" method="post">
|
||||
|
Loading…
x
Reference in New Issue
Block a user