forked from GithubBackups/healthchecks
API support for cron syntax
This commit is contained in:
parent
6ae12dbb18
commit
b93336a44d
@ -151,12 +151,17 @@ class Check(models.Model):
|
||||
"ping_url": self.url(),
|
||||
"pause_url": settings.SITE_ROOT + pause_rel_url,
|
||||
"tags": self.tags,
|
||||
"timeout": int(self.timeout.total_seconds()),
|
||||
"grace": int(self.grace.total_seconds()),
|
||||
"n_pings": self.n_pings,
|
||||
"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:
|
||||
result["last_ping"] = self.last_ping.isoformat()
|
||||
result["next_ping"] = (self.last_ping + self.timeout).isoformat()
|
||||
|
@ -5,6 +5,8 @@ check = {
|
||||
"tags": {"type": "string", "maxLength": 500},
|
||||
"timeout": {"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"},
|
||||
"unique": {
|
||||
"type": "array",
|
||||
|
@ -39,6 +39,9 @@ class CreateCheckTestCase(BaseTestCase):
|
||||
self.assertEqual(doc["last_ping"], None)
|
||||
self.assertEqual(doc["n_pings"], 0)
|
||||
|
||||
self.assertTrue("schedule" not in doc)
|
||||
self.assertTrue("tz" not in doc)
|
||||
|
||||
self.assertEqual(Check.objects.count(), 1)
|
||||
check = Check.objects.get()
|
||||
self.assertEqual(check.name, "Foo")
|
||||
@ -131,3 +134,51 @@ class CreateCheckTestCase(BaseTestCase):
|
||||
"name": "Foo",
|
||||
"unique": "not a list"
|
||||
}, 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)
|
||||
|
@ -8,7 +8,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
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, DEFAULT_TIMEOUT, DEFAULT_GRACE
|
||||
from hc.api.models import Check, Ping
|
||||
from hc.lib.badges import check_signature, get_badge_svg
|
||||
|
||||
|
||||
@ -46,6 +46,50 @@ def ping(request, code):
|
||||
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
|
||||
@check_api_key
|
||||
@validate_json(schemas.check)
|
||||
@ -56,45 +100,7 @@ def checks(request):
|
||||
return JsonResponse(doc)
|
||||
|
||||
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"
|
||||
return HttpResponse(status=405)
|
||||
|
@ -4,7 +4,9 @@ Supports only a tiny subset of jsonschema.
|
||||
|
||||
"""
|
||||
|
||||
from croniter import croniter
|
||||
from six import string_types
|
||||
from pytz import all_timezones
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
@ -17,6 +19,14 @@ def validate(obj, schema, obj_name="value"):
|
||||
raise ValidationError("%s is not a string" % obj_name)
|
||||
if "maxLength" in schema and len(obj) > schema["maxLength"]:
|
||||
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":
|
||||
if not isinstance(obj, int):
|
||||
|
@ -78,3 +78,11 @@ class JsonSchemaTestCase(TestCase):
|
||||
def test_it_rejects_a_value_not_in_enum(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
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"})
|
||||
|
@ -85,6 +85,11 @@ The response may contain a JSON document with additional data.
|
||||
values if omitted.
|
||||
</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>
|
||||
<table class="table">
|
||||
<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>Minimum: 60 (one minute), maximum: 604800 (one week).</p>
|
||||
<p>Example for 5 minute timeout:</p>
|
||||
<pre>{"timeout": 300}</pre>
|
||||
<pre>{"kind": "simple", "timeout": 300}</pre>
|
||||
</td>
|
||||
</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>
|
||||
</td>
|
||||
</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>
|
||||
<th>channels</th>
|
||||
<td>
|
||||
|
@ -1,28 +1,29 @@
|
||||
<div class="highlight"><pre><span></span><span class="p">{</span>
|
||||
<span class="nt">"checks"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"last_ping"</span><span class="p">:</span> <span class="s2">"2017-01-04T13:24:39.903464+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"ping_url"</span><span class="p">:</span> <span class="s2">"{{ PING_ENDPOINT }}662ebe36-ecab-48db-afe3-e20029cb71e6"</span><span class="p">,</span>
|
||||
<span class="nt">"next_ping"</span><span class="p">:</span> <span class="s2">"2017-01-04T14:24:39.903464+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"grace"</span><span class="p">:</span> <span class="mi">900</span><span class="p">,</span>
|
||||
<span class="nt">"last_ping"</span><span class="p">:</span> <span class="s2">"2016-07-09T13:58:43.366568+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"n_pings"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"name"</span><span class="p">:</span> <span class="s2">"Api test 1"</span><span class="p">,</span>
|
||||
<span class="nt">"next_ping"</span><span class="p">:</span> <span class="s2">"2016-07-09T14:58:43.366568+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"pause_url"</span><span class="p">:</span> <span class="s2">"{{ SITE_ROOT }}/api/v1/checks/25c55e7c-8092-4d21-ad06-7dacfbb6fc10/pause"</span><span class="p">,</span>
|
||||
<span class="nt">"ping_url"</span><span class="p">:</span> <span class="s2">"{{ PING_ENDPOINT }}25c55e7c-8092-4d21-ad06-7dacfbb6fc10"</span><span class="p">,</span>
|
||||
<span class="nt">"status"</span><span class="p">:</span> <span class="s2">"up"</span><span class="p">,</span>
|
||||
<span class="nt">"n_pings"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"tags"</span><span class="p">:</span> <span class="s2">"foo"</span><span class="p">,</span>
|
||||
<span class="nt">"timeout"</span><span class="p">:</span> <span class="mi">3600</span>
|
||||
<span class="nt">"pause_url"</span><span class="p">:</span> <span class="s2">"{{ SITE_ROOT }}/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause"</span><span class="p">,</span>
|
||||
<span class="nt">"timeout"</span><span class="p">:</span> <span class="mi">3600</span><span class="p">,</span>
|
||||
<span class="nt">"status"</span><span class="p">:</span> <span class="s2">"up"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"grace"</span><span class="p">:</span> <span class="mi">60</span><span class="p">,</span>
|
||||
<span class="nt">"last_ping"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"n_pings"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
|
||||
<span class="nt">"name"</span><span class="p">:</span> <span class="s2">"Api test 2"</span><span class="p">,</span>
|
||||
<span class="nt">"ping_url"</span><span class="p">:</span> <span class="s2">"{{ PING_ENDPOINT }}9d17c61f-5c4f-4cab-b517-11e6b2679ced"</span><span class="p">,</span>
|
||||
<span class="nt">"next_ping"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"pause_url"</span><span class="p">:</span> <span class="s2">"{{ SITE_ROOT }}/api/v1/checks/7e1b6e61-b16f-4671-bae3-e3233edd1b5e/pause"</span><span class="p">,</span>
|
||||
<span class="nt">"ping_url"</span><span class="p">:</span> <span class="s2">"{{ PING_ENDPOINT }}7e1b6e61-b16f-4671-bae3-e3233edd1b5e"</span><span class="p">,</span>
|
||||
<span class="nt">"status"</span><span class="p">:</span> <span class="s2">"new"</span><span class="p">,</span>
|
||||
<span class="nt">"grace"</span><span class="p">:</span> <span class="mi">3600</span><span class="p">,</span>
|
||||
<span class="nt">"name"</span><span class="p">:</span> <span class="s2">"Api test 2"</span><span class="p">,</span>
|
||||
<span class="nt">"n_pings"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
|
||||
<span class="nt">"tags"</span><span class="p">:</span> <span class="s2">"bar baz"</span><span class="p">,</span>
|
||||
<span class="nt">"timeout"</span><span class="p">:</span> <span class="mi">60</span>
|
||||
<span class="nt">"pause_url"</span><span class="p">:</span> <span class="s2">"{{ SITE_ROOT }}/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced/pause"</span><span class="p">,</span>
|
||||
<span class="nt">"tz"</span><span class="p">:</span> <span class="s2">"UTC"</span><span class="p">,</span>
|
||||
<span class="nt">"schedule"</span><span class="p">:</span> <span class="s2">"0/10 * * * *"</span><span class="p">,</span>
|
||||
<span class="nt">"status"</span><span class="p">:</span> <span class="s2">"new"</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
|
@ -1,28 +1,29 @@
|
||||
{
|
||||
"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,
|
||||
"last_ping": "2016-07-09T13:58:43.366568+00:00",
|
||||
"n_pings": 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",
|
||||
"timeout": 3600
|
||||
"pause_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause",
|
||||
"timeout": 3600,
|
||||
"status": "up"
|
||||
},
|
||||
{
|
||||
"grace": 60,
|
||||
"last_ping": null,
|
||||
"n_pings": 0,
|
||||
"name": "Api test 2",
|
||||
"ping_url": "PING_ENDPOINT9d17c61f-5c4f-4cab-b517-11e6b2679ced",
|
||||
"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",
|
||||
"timeout": 60
|
||||
"pause_url": "SITE_ROOT/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced/pause",
|
||||
"tz": "UTC",
|
||||
"schedule": "0/10 * * * *",
|
||||
"status": "new"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user