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


+ 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, "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):


+ 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.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):
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") 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() 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/$', 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"),


+ 58
- 12
hc/front/views.py 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,24 +553,69 @@ 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)
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) 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 @login_required
def add_pushbullet(request): def add_pushbullet(request):
if settings.PUSHBULLET_CLIENT_ID is None: 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; max-width: 400px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;


+ 1
- 1
static/css/channels.css 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;
} }


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


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

@ -9,17 +9,64 @@
<div class="col-sm-12"> <div class="col-sm-12">
<h1>HipChat</h1> <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> <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>
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>
<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>
<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> <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> </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>
<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>
<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 %}

+ 2
- 2
templates/integrations/add_slack.html 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">


Loading…
Cancel
Save