Browse Source

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

pull/133/head
Pēteris Caune 7 years ago
parent
commit
bef71c0acc
14 changed files with 223 additions and 82 deletions
  1. +33
    -0
      hc/api/models.py
  2. +21
    -0
      hc/api/tests/test_channel_model.py
  3. +3
    -1
      hc/api/transports.py
  4. +31
    -18
      hc/front/tests/test_add_hipchat.py
  5. +2
    -0
      hc/front/urls.py
  6. +58
    -12
      hc/front/views.py
  7. +1
    -1
      static/css/admin/channels.css
  8. +1
    -1
      static/css/channels.css
  9. BIN
      static/img/integrations/setup_hipchat_1.png
  10. BIN
      static/img/integrations/setup_hipchat_2.png
  11. BIN
      static/img/integrations/setup_hipchat_3.png
  12. +2
    -0
      templates/front/channels.html
  13. +69
    -47
      templates/integrations/add_hipchat.html
  14. +2
    -2
      templates/integrations/add_slack.html

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

@ -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
- 0
hc/api/tests/test_channel_model.py 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)

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

@ -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):


+ 31
- 18
hc/front/tests/test_add_hipchat.py View File

@ -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="[email protected]", 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)
c = Channel.objects.get()
self.assertEqual(c.kind, "hipchat")
self.assertEqual(c.value, "http://example.org")
def test_it_returns_capabilities(self):
r = self.client.get("/integrations/hipchat/capabilities/")
self.assertContains(r, "callbackUrl")
def test_it_rejects_bad_url(self):
form = {"value": "not an URL"}
@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"})
self.client.login(username="[email protected]", 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="[email protected]", password="password")
self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
c = Channel.objects.get()
self.assertEqual(c.value, "http://example.org")
self.assertEqual(c.kind, "hipchat")
self.assertTrue("foobar" in c.value)
@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"})
r = self.client.post("/integrations/hipchat/callback/", payload,
content_type="application/json")
self.assertEqual(r.status_code, 400)

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

@ -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"),


+ 58
- 12
hc/front/views.py View File

@ -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
- 1
static/css/admin/channels.css View File

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


+ 1
- 1
static/css/channels.css View File

@ -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;
}


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

Before After
Width: 593  |  Height: 257  |  Size: 25 KiB Width: 428  |  Height: 373  |  Size: 26 KiB

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

Before After
Width: 560  |  Height: 512  |  Size: 35 KiB Width: 507  |  Height: 292  |  Size: 20 KiB

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

Before After
Width: 789  |  Height: 502  |  Size: 54 KiB Width: 500  |  Height: 292  |  Size: 32 KiB

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

@ -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 %}


+ 69
- 47
templates/integrations/add_hipchat.html View File

@ -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>
<p>
From the list of available integrations, select
<strong>Build Your Own</strong>. It's at the very top.
</p>
<span class="step-no">3</span>
<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 %}

+ 2
- 2
templates/integrations/add_slack.html View File

@ -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…
Cancel
Save