Browse Source

API support for cron syntax

pull/114/head
Pēteris Caune 8 years ago
parent
commit
b93336a44d
9 changed files with 180 additions and 70 deletions
  1. +6
    -1
      hc/api/models.py
  2. +2
    -0
      hc/api/schemas.py
  3. +51
    -0
      hc/api/tests/test_create_check.py
  4. +46
    -40
      hc/api/views.py
  5. +10
    -0
      hc/lib/jsonschema.py
  6. +8
    -0
      hc/lib/tests/test_jsonschema.py
  7. +27
    -1
      templates/front/docs_api.html
  8. +15
    -14
      templates/front/snippets/list_checks_response.html
  9. +15
    -14
      templates/front/snippets/list_checks_response.txt

+ 6
- 1
hc/api/models.py View File

@ -151,12 +151,17 @@ class Check(models.Model):
"ping_url": self.url(), "ping_url": self.url(),
"pause_url": settings.SITE_ROOT + pause_rel_url, "pause_url": settings.SITE_ROOT + pause_rel_url,
"tags": self.tags, "tags": self.tags,
"timeout": int(self.timeout.total_seconds()),
"grace": int(self.grace.total_seconds()), "grace": int(self.grace.total_seconds()),
"n_pings": self.n_pings, "n_pings": self.n_pings,
"status": self.get_status() "status": self.get_status()
} }
if self.kind == "simple":
result["timeout"] = int(self.timeout.total_seconds())
elif self.kind == "cron":
result["schedule"] = self.schedule
result["tz"] = self.tz
if self.last_ping: if self.last_ping:
result["last_ping"] = self.last_ping.isoformat() result["last_ping"] = self.last_ping.isoformat()
result["next_ping"] = (self.last_ping + self.timeout).isoformat() result["next_ping"] = (self.last_ping + self.timeout).isoformat()


+ 2
- 0
hc/api/schemas.py View File

@ -5,6 +5,8 @@ check = {
"tags": {"type": "string", "maxLength": 500}, "tags": {"type": "string", "maxLength": 500},
"timeout": {"type": "number", "minimum": 60, "maximum": 604800}, "timeout": {"type": "number", "minimum": 60, "maximum": 604800},
"grace": {"type": "number", "minimum": 60, "maximum": 604800}, "grace": {"type": "number", "minimum": 60, "maximum": 604800},
"schedule": {"type": "string", "format": "cron", "maxLength": 100},
"tz": {"type": "string", "format": "timezone", "maxLength": 36},
"channels": {"type": "string"}, "channels": {"type": "string"},
"unique": { "unique": {
"type": "array", "type": "array",


+ 51
- 0
hc/api/tests/test_create_check.py View File

@ -39,6 +39,9 @@ class CreateCheckTestCase(BaseTestCase):
self.assertEqual(doc["last_ping"], None) self.assertEqual(doc["last_ping"], None)
self.assertEqual(doc["n_pings"], 0) self.assertEqual(doc["n_pings"], 0)
self.assertTrue("schedule" not in doc)
self.assertTrue("tz" not in doc)
self.assertEqual(Check.objects.count(), 1) self.assertEqual(Check.objects.count(), 1)
check = Check.objects.get() check = Check.objects.get()
self.assertEqual(check.name, "Foo") self.assertEqual(check.name, "Foo")
@ -131,3 +134,51 @@ class CreateCheckTestCase(BaseTestCase):
"name": "Foo", "name": "Foo",
"unique": "not a list" "unique": "not a list"
}, expected_fragment="not an array") }, expected_fragment="not an array")
def test_it_supports_cron_syntax(self):
r = self.post({
"api_key": "abc",
"schedule": "5 * * * *",
"tz": "Europe/Riga",
"grace": 60
})
self.assertEqual(r.status_code, 201)
doc = r.json()
self.assertEqual(doc["kind"], "cron")
self.assertEqual(doc["schedule"], "5 * * * *")
self.assertEqual(doc["tz"], "Europe/Riga")
self.assertEqual(doc["grace"], 60)
self.assertTrue("timeout" not in doc)
def test_it_validates_cron_expression(self):
r = self.post({
"api_key": "abc",
"kind": "cron",
"schedule": "not-a-cron-expression",
"tz": "Europe/Riga",
"grace": 60
})
self.assertEqual(r.status_code, 400)
def test_it_validates_timezone(self):
r = self.post({
"api_key": "abc",
"kind": "cron",
"schedule": "* * * * *",
"tz": "not-a-timezone",
"grace": 60
})
self.assertEqual(r.status_code, 400)
def test_it_sets_default_timeout(self):
r = self.post({"api_key": "abc"})
self.assertEqual(r.status_code, 201)
doc = r.json()
self.assertEqual(doc["timeout"], 86400)

+ 46
- 40
hc/api/views.py View File

@ -8,7 +8,7 @@ from django.views.decorators.csrf import csrf_exempt
from hc.api import schemas from hc.api import schemas
from hc.api.decorators import check_api_key, uuid_or_400, validate_json from hc.api.decorators import check_api_key, uuid_or_400, validate_json
from hc.api.models import Check, Ping, DEFAULT_TIMEOUT, DEFAULT_GRACE
from hc.api.models import Check, Ping
from hc.lib.badges import check_signature, get_badge_svg from hc.lib.badges import check_signature, get_badge_svg
@ -46,6 +46,50 @@ def ping(request, code):
return response return response
def _create_check(user, spec):
check = Check(user=user)
check.name = spec.get("name", "")
check.tags = spec.get("tags", "")
if "timeout" in spec and "schedule" not in spec:
check.timeout = td(seconds=spec["timeout"])
if "grace" in spec:
check.grace = td(seconds=spec["grace"])
if "schedule" in spec:
check.kind = "cron"
check.schedule = spec["schedule"]
if "tz" in spec and "schedule" in spec:
check.tz = spec["tz"]
unique_fields = spec.get("unique", [])
if unique_fields:
existing_checks = Check.objects.filter(user=user)
if "name" in unique_fields:
existing_checks = existing_checks.filter(name=check.name)
if "tags" in unique_fields:
existing_checks = existing_checks.filter(tags=check.tags)
if "timeout" in unique_fields:
existing_checks = existing_checks.filter(timeout=check.timeout)
if "grace" in unique_fields:
existing_checks = existing_checks.filter(grace=check.grace)
if existing_checks.count() > 0:
# There might be more than one matching check, return first
first_match = existing_checks.first()
return JsonResponse(first_match.to_dict(), status=200)
check.save()
# This needs to be done after saving the check, because of
# the M2M relation between checks and channels:
if spec.get("channels") == "*":
check.assign_all_channels()
return JsonResponse(check.to_dict(), status=201)
@csrf_exempt @csrf_exempt
@check_api_key @check_api_key
@validate_json(schemas.check) @validate_json(schemas.check)
@ -56,45 +100,7 @@ def checks(request):
return JsonResponse(doc) return JsonResponse(doc)
elif request.method == "POST": elif request.method == "POST":
name = str(request.json.get("name", ""))
tags = str(request.json.get("tags", ""))
timeout = DEFAULT_TIMEOUT
if "timeout" in request.json:
timeout = td(seconds=request.json["timeout"])
grace = DEFAULT_GRACE
if "grace" in request.json:
grace = td(seconds=request.json["grace"])
unique_fields = request.json.get("unique", [])
if unique_fields:
existing_checks = Check.objects.filter(user=request.user)
if "name" in unique_fields:
existing_checks = existing_checks.filter(name=name)
if "tags" in unique_fields:
existing_checks = existing_checks.filter(tags=tags)
if "timeout" in unique_fields:
existing_checks = existing_checks.filter(timeout=timeout)
if "grace" in unique_fields:
existing_checks = existing_checks.filter(grace=grace)
if existing_checks.count() > 0:
# There might be more than one matching check, return first
first_match = existing_checks.first()
return JsonResponse(first_match.to_dict(), status=200)
check = Check(user=request.user, name=name, tags=tags,
timeout=timeout, grace=grace)
check.save()
# This needs to be done after saving the check, because of
# the M2M relation between checks and channels:
if request.json.get("channels") == "*":
check.assign_all_channels()
return JsonResponse(check.to_dict(), status=201)
return _create_check(request.user, request.json)
# If request is neither GET nor POST, return "405 Method not allowed" # If request is neither GET nor POST, return "405 Method not allowed"
return HttpResponse(status=405) return HttpResponse(status=405)


+ 10
- 0
hc/lib/jsonschema.py View File

@ -4,7 +4,9 @@ Supports only a tiny subset of jsonschema.
""" """
from croniter import croniter
from six import string_types from six import string_types
from pytz import all_timezones
class ValidationError(Exception): class ValidationError(Exception):
@ -17,6 +19,14 @@ def validate(obj, schema, obj_name="value"):
raise ValidationError("%s is not a string" % obj_name) raise ValidationError("%s is not a string" % obj_name)
if "maxLength" in schema and len(obj) > schema["maxLength"]: if "maxLength" in schema and len(obj) > schema["maxLength"]:
raise ValidationError("%s is too long" % obj_name) raise ValidationError("%s is too long" % obj_name)
if schema.get("format") == "cron":
try:
croniter(obj)
except:
raise ValidationError(
"%s is not a valid cron expression" % obj_name)
if schema.get("format") == "timezone" and obj not in all_timezones:
raise ValidationError("%s is not a valid timezone" % obj_name)
elif schema.get("type") == "number": elif schema.get("type") == "number":
if not isinstance(obj, int): if not isinstance(obj, int):


+ 8
- 0
hc/lib/tests/test_jsonschema.py View File

@ -78,3 +78,11 @@ class JsonSchemaTestCase(TestCase):
def test_it_rejects_a_value_not_in_enum(self): def test_it_rejects_a_value_not_in_enum(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
validate("baz", {"enum": ["foo", "bar"]}) validate("baz", {"enum": ["foo", "bar"]})
def test_it_checks_cron_format(self):
with self.assertRaises(ValidationError):
validate("x * * * *", {"type": "string", "format": "cron"})
def test_it_checks_timezone_format(self):
with self.assertRaises(ValidationError):
validate("X/Y", {"type": "string", "format": "timezone"})

+ 27
- 1
templates/front/docs_api.html View File

@ -85,6 +85,11 @@ The response may contain a JSON document with additional data.
values if omitted. values if omitted.
</p> </p>
<p>This API call can be used to create both "simple" and "cron" checks.
To create a "simple" check, specify the "timeout" parameter.
To create a "cron" check, specify the "schedule" and "tz" parameters.
</p>
<h3 class="api-section">Request Parameters</h3> <h3 class="api-section">Request Parameters</h3>
<table class="table"> <table class="table">
<tr> <tr>
@ -110,7 +115,7 @@ The response may contain a JSON document with additional data.
<p>A number of seconds, the expected period of this check.</p> <p>A number of seconds, the expected period of this check.</p>
<p>Minimum: 60 (one minute), maximum: 604800 (one week).</p> <p>Minimum: 60 (one minute), maximum: 604800 (one week).</p>
<p>Example for 5 minute timeout:</p> <p>Example for 5 minute timeout:</p>
<pre>{"timeout": 300}</pre>
<pre>{"kind": "simple", "timeout": 300}</pre>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -121,6 +126,27 @@ The response may contain a JSON document with additional data.
<p>Minimum: 60 (one minute), maximum: 604800 (one week).</p> <p>Minimum: 60 (one minute), maximum: 604800 (one week).</p>
</td> </td>
</tr> </tr>
<tr>
<th>schedule</th>
<td>
<p>string, optional, default value: "* * * * *".</p>
<p>A cron expression defining this check's schedule.</p>
<p>If you specify both "timeout" and "schedule" parameters,
"timeout" will be ignored and "schedule" will be used.</p>
<p>Example for a check running every half-hour:</p>
<pre>{"schedule": "0,30 * * * *"}</pre>
</td>
</tr>
<tr>
<th>tz</th>
<td>
<p>string, optional, default value: "UTC".</p>
<p>Server's timezone. This setting only has effect in combination
with the "schedule" paremeter.</p>
<p>Example:</p>
<pre>{"tz": "Europe/Riga"}</pre>
</td>
</tr>
<tr> <tr>
<th>channels</th> <th>channels</th>
<td> <td>


+ 15
- 14
templates/front/snippets/list_checks_response.html View File

@ -1,28 +1,29 @@
<div class="highlight"><pre><span></span><span class="p">{</span> <div class="highlight"><pre><span></span><span class="p">{</span>
<span class="nt">&quot;checks&quot;</span><span class="p">:</span> <span class="p">[</span> <span class="nt">&quot;checks&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span> <span class="p">{</span>
<span class="nt">&quot;last_ping&quot;</span><span class="p">:</span> <span class="s2">&quot;2017-01-04T13:24:39.903464+00:00&quot;</span><span class="p">,</span>
<span class="nt">&quot;ping_url&quot;</span><span class="p">:</span> <span class="s2">&quot;{{ PING_ENDPOINT }}662ebe36-ecab-48db-afe3-e20029cb71e6&quot;</span><span class="p">,</span>
<span class="nt">&quot;next_ping&quot;</span><span class="p">:</span> <span class="s2">&quot;2017-01-04T14:24:39.903464+00:00&quot;</span><span class="p">,</span>
<span class="nt">&quot;grace&quot;</span><span class="p">:</span> <span class="mi">900</span><span class="p">,</span> <span class="nt">&quot;grace&quot;</span><span class="p">:</span> <span class="mi">900</span><span class="p">,</span>
<span class="nt">&quot;last_ping&quot;</span><span class="p">:</span> <span class="s2">&quot;2016-07-09T13:58:43.366568+00:00&quot;</span><span class="p">,</span>
<span class="nt">&quot;n_pings&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;Api test 1&quot;</span><span class="p">,</span> <span class="nt">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;Api test 1&quot;</span><span class="p">,</span>
<span class="nt">&quot;next_ping&quot;</span><span class="p">:</span> <span class="s2">&quot;2016-07-09T14:58:43.366568+00:00&quot;</span><span class="p">,</span>
<span class="nt">&quot;pause_url&quot;</span><span class="p">:</span> <span class="s2">&quot;{{ SITE_ROOT }}/api/v1/checks/25c55e7c-8092-4d21-ad06-7dacfbb6fc10/pause&quot;</span><span class="p">,</span>
<span class="nt">&quot;ping_url&quot;</span><span class="p">:</span> <span class="s2">&quot;{{ PING_ENDPOINT }}25c55e7c-8092-4d21-ad06-7dacfbb6fc10&quot;</span><span class="p">,</span>
<span class="nt">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;up&quot;</span><span class="p">,</span>
<span class="nt">&quot;n_pings&quot;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="nt">&quot;tags&quot;</span><span class="p">:</span> <span class="s2">&quot;foo&quot;</span><span class="p">,</span> <span class="nt">&quot;tags&quot;</span><span class="p">:</span> <span class="s2">&quot;foo&quot;</span><span class="p">,</span>
<span class="nt">&quot;timeout&quot;</span><span class="p">:</span> <span class="mi">3600</span>
<span class="nt">&quot;pause_url&quot;</span><span class="p">:</span> <span class="s2">&quot;{{ SITE_ROOT }}/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause&quot;</span><span class="p">,</span>
<span class="nt">&quot;timeout&quot;</span><span class="p">:</span> <span class="mi">3600</span><span class="p">,</span>
<span class="nt">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;up&quot;</span>
<span class="p">},</span> <span class="p">},</span>
<span class="p">{</span> <span class="p">{</span>
<span class="nt">&quot;grace&quot;</span><span class="p">:</span> <span class="mi">60</span><span class="p">,</span>
<span class="nt">&quot;last_ping&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span> <span class="nt">&quot;last_ping&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;n_pings&quot;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="nt">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;Api test 2&quot;</span><span class="p">,</span>
<span class="nt">&quot;ping_url&quot;</span><span class="p">:</span> <span class="s2">&quot;{{ PING_ENDPOINT }}9d17c61f-5c4f-4cab-b517-11e6b2679ced&quot;</span><span class="p">,</span>
<span class="nt">&quot;next_ping&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span> <span class="nt">&quot;next_ping&quot;</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="nt">&quot;pause_url&quot;</span><span class="p">:</span> <span class="s2">&quot;{{ SITE_ROOT }}/api/v1/checks/7e1b6e61-b16f-4671-bae3-e3233edd1b5e/pause&quot;</span><span class="p">,</span>
<span class="nt">&quot;ping_url&quot;</span><span class="p">:</span> <span class="s2">&quot;{{ PING_ENDPOINT }}7e1b6e61-b16f-4671-bae3-e3233edd1b5e&quot;</span><span class="p">,</span>
<span class="nt">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;new&quot;</span><span class="p">,</span>
<span class="nt">&quot;grace&quot;</span><span class="p">:</span> <span class="mi">3600</span><span class="p">,</span>
<span class="nt">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;Api test 2&quot;</span><span class="p">,</span>
<span class="nt">&quot;n_pings&quot;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="nt">&quot;tags&quot;</span><span class="p">:</span> <span class="s2">&quot;bar baz&quot;</span><span class="p">,</span> <span class="nt">&quot;tags&quot;</span><span class="p">:</span> <span class="s2">&quot;bar baz&quot;</span><span class="p">,</span>
<span class="nt">&quot;timeout&quot;</span><span class="p">:</span> <span class="mi">60</span>
<span class="nt">&quot;pause_url&quot;</span><span class="p">:</span> <span class="s2">&quot;{{ SITE_ROOT }}/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced/pause&quot;</span><span class="p">,</span>
<span class="nt">&quot;tz&quot;</span><span class="p">:</span> <span class="s2">&quot;UTC&quot;</span><span class="p">,</span>
<span class="nt">&quot;schedule&quot;</span><span class="p">:</span> <span class="s2">&quot;0/10 * * * *&quot;</span><span class="p">,</span>
<span class="nt">&quot;status&quot;</span><span class="p">:</span> <span class="s2">&quot;new&quot;</span>
<span class="p">}</span> <span class="p">}</span>
<span class="p">]</span> <span class="p">]</span>
<span class="p">}</span> <span class="p">}</span>


+ 15
- 14
templates/front/snippets/list_checks_response.txt View File

@ -1,28 +1,29 @@
{ {
"checks": [ "checks": [
{ {
"last_ping": "2017-01-04T13:24:39.903464+00:00",
"ping_url": "PING_ENDPOINT662ebe36-ecab-48db-afe3-e20029cb71e6",
"next_ping": "2017-01-04T14:24:39.903464+00:00",
"grace": 900, "grace": 900,
"last_ping": "2016-07-09T13:58:43.366568+00:00",
"n_pings": 1,
"name": "Api test 1", "name": "Api test 1",
"next_ping": "2016-07-09T14:58:43.366568+00:00",
"pause_url": "SITE_ROOT/api/v1/checks/25c55e7c-8092-4d21-ad06-7dacfbb6fc10/pause",
"ping_url": "PING_ENDPOINT25c55e7c-8092-4d21-ad06-7dacfbb6fc10",
"status": "up",
"n_pings": 1,
"tags": "foo", "tags": "foo",
"timeout": 3600
"pause_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause",
"timeout": 3600,
"status": "up"
}, },
{ {
"grace": 60,
"last_ping": null, "last_ping": null,
"n_pings": 0,
"name": "Api test 2",
"ping_url": "PING_ENDPOINT9d17c61f-5c4f-4cab-b517-11e6b2679ced",
"next_ping": null, "next_ping": null,
"pause_url": "SITE_ROOT/api/v1/checks/7e1b6e61-b16f-4671-bae3-e3233edd1b5e/pause",
"ping_url": "PING_ENDPOINT7e1b6e61-b16f-4671-bae3-e3233edd1b5e",
"status": "new",
"grace": 3600,
"name": "Api test 2",
"n_pings": 0,
"tags": "bar baz", "tags": "bar baz",
"timeout": 60
"pause_url": "SITE_ROOT/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced/pause",
"tz": "UTC",
"schedule": "0/10 * * * *",
"status": "new"
} }
] ]
} }

Loading…
Cancel
Save