For HipChat integration, use HipChat's server-side installation flow.

This commit is contained in:
Pēteris Caune 2017-08-25 19:11:41 +03:00
parent de7160a0e6
commit bef71c0acc
14 changed files with 223 additions and 82 deletions

View File

@ -2,6 +2,7 @@
import hashlib import hashlib
import json import json
import time
import uuid import uuid
from datetime import datetime, timedelta as td from datetime import datetime, timedelta as td
@ -14,6 +15,7 @@ from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from hc.api import transports from hc.api import transports
from hc.lib import emails from hc.lib import emails
import requests
STATUSES = ( STATUSES = (
("up", "Up"), ("up", "Up"),
@ -377,6 +379,37 @@ class Channel(models.Model):
doc = json.loads(self.value) doc = json.loads(self.value)
return doc.get("name") 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): def latest_notification(self):
return Notification.objects.filter(channel=self).latest() return Notification.objects.filter(channel=self).latest()

View 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)

View File

@ -175,7 +175,9 @@ class HipChat(HttpTransport):
"message": text, "message": text,
"color": "green" if check.status == "up" else "red", "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): class OpsGenie(HttpTransport):

View File

@ -1,5 +1,9 @@
import json
from django.core import signing
from hc.api.models import Channel from hc.api.models import Channel
from hc.test import BaseTestCase from hc.test import BaseTestCase
from mock import patch
class AddHipChatTestCase(BaseTestCase): class AddHipChatTestCase(BaseTestCase):
@ -10,29 +14,38 @@ class AddHipChatTestCase(BaseTestCase):
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertContains(r, "appropriate HipChat room") self.assertContains(r, "appropriate HipChat room")
def test_it_works(self): def test_instructions_work_when_logged_out(self):
form = {"value": "http://example.org"} 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") self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, form) r = self.client.post(self.url)
self.assertRedirects(r, "/integrations/") 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() c = Channel.objects.get()
self.assertEqual(c.kind, "hipchat") 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): @patch("hc.api.models.Channel.refresh_hipchat_access_token")
form = {"value": "not an URL"} 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("/integrations/hipchat/callback/", payload,
r = self.client.post(self.url, form) content_type="application/json")
self.assertContains(r, "Enter a valid URL")
def test_it_trims_whitespace(self): self.assertEqual(r.status_code, 400)
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")

View File

@ -19,6 +19,8 @@ channel_urls = [
url(r'^add_slack/$', views.add_slack, name="hc-add-slack"), 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_slack_btn/$', views.add_slack_btn, name="hc-add-slack-btn"),
url(r'^add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"), 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_pushbullet/$', views.add_pushbullet, name="hc-add-pushbullet"),
url(r'^add_discord/$', views.add_discord, name="hc-add-discord"), url(r'^add_discord/$', views.add_discord, name="hc-add-discord"),
url(r'^add_pushover/$', views.add_pushover, name="hc-add-pushover"), url(r'^add_pushover/$', views.add_pushover, name="hc-add-pushover"),

View File

@ -6,11 +6,12 @@ import json
from croniter import croniter from croniter import croniter
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core import signing from django.core import signing
from django.db.models import Count from django.db.models import Count
from django.http import (Http404, HttpResponse, HttpResponseBadRequest, from django.http import (Http404, HttpResponse, HttpResponseBadRequest,
HttpResponseForbidden) HttpResponseForbidden, JsonResponse)
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
@ -552,22 +553,67 @@ def add_slack_btn(request):
return redirect("hc-channels") return redirect("hc-channels")
@login_required
def add_hipchat(request): def add_hipchat(request):
if request.method == "POST": if request.method == "POST":
form = AddUrlForm(request.POST) username = request.team.user.username
if form.is_valid(): state = signing.TimestampSigner().sign(username)
channel = Channel(user=request.team.user, kind="hipchat") capabilities = settings.SITE_ROOT + reverse("hc-hipchat-capabilities")
channel.value = form.cleaned_data["value"]
url = "https://www.hipchat.com/addons/install?url=%s&relayState=%s" % \
(capabilities, state)
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.save()
channel.refresh_hipchat_access_token()
channel.assign_all_checks() channel.assign_all_checks()
return redirect("hc-channels")
else:
form = AddUrlForm()
ctx = {"page": "channels", "form": form} return HttpResponse()
return render(request, "integrations/add_hipchat.html", ctx)
@login_required @login_required

View File

@ -1,4 +1,4 @@
.field-value { .results .field-value {
max-width: 400px; max-width: 400px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -5,6 +5,7 @@
.channels-table .channel-row > td { .channels-table .channel-row > td {
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
vertical-align: middle;
} }
.channels-table .value-cell { .channels-table .value-cell {
@ -166,7 +167,6 @@ table.channels-table > tbody > tr > th {
} }
.ai-guide-screenshot { .ai-guide-screenshot {
border: ;
max-width: 100%; max-width: 100%;
border: 6px solid #EEE; 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

View File

@ -89,6 +89,8 @@
<span class="preposition">user</span> <span class="preposition">user</span>
{% endif %} {% endif %}
{{ ch.telegram_name }} {{ ch.telegram_name }}
{% elif ch.kind == "hipchat" %}
{{ ch.hipchat_webhook_url }}
{% else %} {% else %}
{{ ch.value }} {{ ch.value }}
{% endif %} {% endif %}

View File

@ -9,17 +9,64 @@
<div class="col-sm-12"> <div class="col-sm-12">
<h1>HipChat</h1> <h1>HipChat</h1>
<div class="jumbotron">
{% if request.user.is_authenticated %}
<p>If your team uses <a href="https://www.hipchat.com/">HipChat</a>, <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 you can set up {% site_name %} to post status updates directly to an
appropriate HipChat room.</p> 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> <h2>Setup Guide</h2>
<div class="row ai-step"> <div class="row ai-step">
<div class="col-sm-6"> <div class="col-sm-6">
<span class="step-no">1</span> <span class="step-no">2</span>
Log into your HipChat account and <p>
pick an appropriate room. From the options menu After {% if request.user.is_authenticated %}{% else %}logging in and{% endif %}
select <strong>Integrations...</strong> clicking on "Install HipChat Integration", you will be
asked to log into HipChat.
</p>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<img <img
@ -28,16 +75,13 @@
src="{% static 'img/integrations/setup_hipchat_1.png' %}"> src="{% static 'img/integrations/setup_hipchat_1.png' %}">
</div> </div>
</div> </div>
<div class="row ai-step"> <div class="row ai-step">
<div class="col-sm-6"> <div class="col-sm-6">
<span class="step-no">2</span> <span class="step-no">3</span>
<p> <p>
From the list of available integrations, select Next, HipChat will let you select the chat room
<strong>Build Your Own</strong>. It's at the very top. for receiving {% site_name %} notifications.
</p>
<p>
Give it a descriptive name
and click <strong>Create</strong>.
</p> </p>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
@ -45,13 +89,20 @@
class="ai-guide-screenshot" class="ai-guide-screenshot"
alt="Screenshot" alt="Screenshot"
src="{% static 'img/integrations/setup_hipchat_2.png' %}"> src="{% static 'img/integrations/setup_hipchat_2.png' %}">
</div> </div> </div>
</div>
<div class="row ai-step"> <div class="row ai-step">
<div class="col-sm-6"> <div class="col-sm-6">
<span class="step-no">3</span> <span class="step-no">4</span>
<p>Copy the displayed <strong>URL</strong> and paste it down below.</p> <p>
<p>Save the integration, and it's done!</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>
<div class="col-sm-6"> <div class="col-sm-6">
<img <img
@ -61,35 +112,6 @@
</div> </div>
</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>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -23,11 +23,11 @@
{% else %} {% else %}
<p> <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> <a href="https://github.com/healthchecks/healthchecks">open source</a>
service for monitoring your cron jobs, background processes and service for monitoring your cron jobs, background processes and
scheduled tasks. Before adding Slack integration, please log into scheduled tasks. Before adding Slack integration, please log into
{% site_root %}:</p> {% site_name %}:</p>
<div class="text-center"> <div class="text-center">
<form class="form-inline" action="{% url 'hc-login' %}" method="post"> <form class="form-inline" action="{% url 'hc-login' %}" method="post">