forked from GithubBackups/healthchecks
First stab at API, POST /api/v1/checks
This commit is contained in:
parent
144cf0da90
commit
5d2edfa4a0
@ -174,10 +174,7 @@ def set_password(request, token):
|
|||||||
messages.info(request, "Your password has been set!")
|
messages.info(request, "Your password has been set!")
|
||||||
return redirect("hc-profile")
|
return redirect("hc-profile")
|
||||||
|
|
||||||
ctx = {
|
return render(request, "accounts/set_password.html", {})
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, "accounts/set_password.html", ctx)
|
|
||||||
|
|
||||||
|
|
||||||
def unsubscribe_reports(request, username):
|
def unsubscribe_reports(request, username):
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from django.http import HttpResponseBadRequest
|
from django.contrib.auth.models import User
|
||||||
|
from django.http import HttpResponseBadRequest, JsonResponse
|
||||||
|
|
||||||
|
|
||||||
def uuid_or_400(f):
|
def uuid_or_400(f):
|
||||||
@ -14,3 +16,62 @@ def uuid_or_400(f):
|
|||||||
|
|
||||||
return f(request, *args, **kwds)
|
return f(request, *args, **kwds)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def make_error(msg):
|
||||||
|
return JsonResponse({"error": msg}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
def check_api_key(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(request, *args, **kwds):
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body.decode("utf-8"))
|
||||||
|
except ValueError:
|
||||||
|
return make_error("could not parse request body")
|
||||||
|
|
||||||
|
api_key = str(data.get("api_key", ""))
|
||||||
|
if api_key == "":
|
||||||
|
return make_error("wrong api_key")
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(profile__api_key=api_key)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return make_error("wrong api_key")
|
||||||
|
|
||||||
|
request.json = data
|
||||||
|
request.user = user
|
||||||
|
return f(request, *args, **kwds)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def validate_json(schema):
|
||||||
|
""" Validate request.json contents against `schema`.
|
||||||
|
|
||||||
|
Supports a tiny subset of JSON schema spec.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(request, *args, **kwds):
|
||||||
|
for key, spec in schema["properties"].items():
|
||||||
|
if key not in request.json:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = request.json[key]
|
||||||
|
if spec["type"] == "string":
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return make_error("%s is not a string" % key)
|
||||||
|
elif spec["type"] == "number":
|
||||||
|
if not isinstance(value, int):
|
||||||
|
return make_error("%s is not a number" % key)
|
||||||
|
if "minimum" in spec and value < spec["minimum"]:
|
||||||
|
return make_error("%s is too small" % key)
|
||||||
|
if "maximum" in spec and value > spec["maximum"]:
|
||||||
|
return make_error("%s is too large" % key)
|
||||||
|
|
||||||
|
return f(request, *args, **kwds)
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
8
hc/api/schemas.py
Normal file
8
hc/api/schemas.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
check = {
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"tags": {"type": "string"},
|
||||||
|
"timeout": {"type": "number", "minimum": 60, "maximum": 604800},
|
||||||
|
"grace": {"type": "number", "minimum": 60, "maximum": 604800}
|
||||||
|
}
|
||||||
|
}
|
67
hc/api/tests/test_create_check.py
Normal file
67
hc/api/tests/test_create_check.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from hc.api.models import Check
|
||||||
|
from hc.test import BaseTestCase
|
||||||
|
from hc.accounts.models import Profile
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCheckTestCase(BaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(CreateCheckTestCase, self).setUp()
|
||||||
|
self.profile = Profile(user=self.alice, api_key="abc")
|
||||||
|
self.profile.save()
|
||||||
|
|
||||||
|
def post(self, url, data):
|
||||||
|
return self.client.post(url, json.dumps(data),
|
||||||
|
content_type="application/json")
|
||||||
|
|
||||||
|
def test_it_works(self):
|
||||||
|
r = self.post("/api/v1/checks/", {
|
||||||
|
"api_key": "abc",
|
||||||
|
"name": "Foo",
|
||||||
|
"tags": "bar,baz",
|
||||||
|
"timeout": 3600,
|
||||||
|
"grace": 60
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(r.status_code, 201)
|
||||||
|
self.assertTrue("ping_url" in r.json())
|
||||||
|
|
||||||
|
self.assertEqual(Check.objects.count(), 1)
|
||||||
|
check = Check.objects.get()
|
||||||
|
self.assertEqual(check.name, "Foo")
|
||||||
|
self.assertEqual(check.tags, "bar,baz")
|
||||||
|
self.assertEqual(check.timeout.total_seconds(), 3600)
|
||||||
|
self.assertEqual(check.grace.total_seconds(), 60)
|
||||||
|
|
||||||
|
def test_it_handles_missing_request_body(self):
|
||||||
|
r = self.client.post("/api/v1/checks/",
|
||||||
|
content_type="application/json")
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
|
self.assertEqual(r.json()["error"], "wrong api_key")
|
||||||
|
|
||||||
|
def test_it_rejects_wrong_api_key(self):
|
||||||
|
r = self.post("/api/v1/checks/", {"api_key": "wrong"})
|
||||||
|
self.assertEqual(r.json()["error"], "wrong api_key")
|
||||||
|
|
||||||
|
def test_it_handles_invalid_json(self):
|
||||||
|
r = self.client.post("/api/v1/checks/", "this is not json",
|
||||||
|
content_type="application/json")
|
||||||
|
self.assertEqual(r.json()["error"], "could not parse request body")
|
||||||
|
|
||||||
|
def test_it_reject_small_timeout(self):
|
||||||
|
r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": 0})
|
||||||
|
self.assertEqual(r.json()["error"], "timeout is too small")
|
||||||
|
|
||||||
|
def test_it_rejects_large_timeout(self):
|
||||||
|
r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": 604801})
|
||||||
|
self.assertEqual(r.json()["error"], "timeout is too large")
|
||||||
|
|
||||||
|
def test_it_rejects_non_number_timeout(self):
|
||||||
|
r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": "oops"})
|
||||||
|
self.assertEqual(r.json()["error"], "timeout is not a number")
|
||||||
|
|
||||||
|
def test_it_rejects_non_string_name(self):
|
||||||
|
r = self.post("/api/v1/checks/", {"api_key": "abc", "name": False})
|
||||||
|
self.assertEqual(r.json()["error"], "name is not a string")
|
@ -1,17 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from hc.api.models import Check
|
|
||||||
|
|
||||||
|
|
||||||
class StatusTestCase(TestCase):
|
|
||||||
|
|
||||||
def test_it_works(self):
|
|
||||||
check = Check()
|
|
||||||
check.save()
|
|
||||||
|
|
||||||
r = self.client.get("/status/%s/" % check.code)
|
|
||||||
self.assertContains(r, "last_ping", status_code=200)
|
|
||||||
|
|
||||||
def test_it_handles_bad_uuid(self):
|
|
||||||
r = self.client.get("/status/not-uuid/")
|
|
||||||
assert r.status_code == 400
|
|
@ -5,6 +5,6 @@ from hc.api import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"),
|
url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"),
|
||||||
url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"),
|
url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"),
|
||||||
url(r'^status/([\w-]+)/$', views.status, name="hc-status"),
|
|
||||||
url(r'^handle_email/$', views.handle_email, name="hc-handle-email"),
|
url(r'^handle_email/$', views.handle_email, name="hc-handle-email"),
|
||||||
|
url(r'^api/v1/checks/$', views.create_check),
|
||||||
]
|
]
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
from datetime import timedelta as td
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest
|
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from hc.api.decorators import uuid_or_400
|
from hc.api import schemas
|
||||||
|
from hc.api.decorators import check_api_key, uuid_or_400, validate_json
|
||||||
from hc.api.models import Check, Ping
|
from hc.api.models import Check, Ping
|
||||||
|
|
||||||
|
|
||||||
@ -75,22 +76,25 @@ def handle_email(request):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@uuid_or_400
|
@csrf_exempt
|
||||||
def status(request, code):
|
@check_api_key
|
||||||
|
@validate_json(schemas.check)
|
||||||
|
def create_check(request):
|
||||||
|
if request.method != "POST":
|
||||||
|
return HttpResponse(status=405)
|
||||||
|
|
||||||
|
check = Check(user=request.user)
|
||||||
|
check.name = str(request.json.get("name", ""))
|
||||||
|
check.tags = str(request.json.get("tags", ""))
|
||||||
|
if "timeout" in request.json:
|
||||||
|
check.timeout = td(seconds=request.json["timeout"])
|
||||||
|
if "grace" in request.json:
|
||||||
|
check.grace = td(seconds=request.json["grace"])
|
||||||
|
|
||||||
|
check.save()
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
"last_ping": None,
|
"ping_url": check.url()
|
||||||
"last_ping_human": None,
|
|
||||||
"secs_to_alert": None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check = Check.objects.get(code=code)
|
return JsonResponse(response, status=201)
|
||||||
|
|
||||||
if check.last_ping and check.alert_after:
|
|
||||||
response["last_ping"] = check.last_ping.isoformat()
|
|
||||||
response["last_ping_human"] = naturaltime(check.last_ping)
|
|
||||||
|
|
||||||
duration = check.alert_after - timezone.now()
|
|
||||||
response["secs_to_alert"] = int(duration.total_seconds())
|
|
||||||
|
|
||||||
return HttpResponse(json.dumps(response),
|
|
||||||
content_type="application/javascript")
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user