forked from GithubBackups/healthchecks
API call for updating checks
This commit is contained in:
parent
b63f19f415
commit
20b046cba7
@ -144,11 +144,13 @@ class Check(models.Model):
|
||||
return [t.strip() for t in self.tags.split(" ") if t.strip()]
|
||||
|
||||
def to_dict(self):
|
||||
update_rel_url = reverse("hc-api-update", args=[self.code])
|
||||
pause_rel_url = reverse("hc-api-pause", args=[self.code])
|
||||
|
||||
result = {
|
||||
"name": self.name,
|
||||
"ping_url": self.url(),
|
||||
"update_url": settings.SITE_ROOT + update_rel_url,
|
||||
"pause_url": settings.SITE_ROOT + pause_rel_url,
|
||||
"tags": self.tags,
|
||||
"grace": int(self.grace.total_seconds()),
|
||||
|
@ -48,7 +48,10 @@ class ListChecksTestCase(BaseTestCase):
|
||||
self.assertEqual(checks["Alice 1"]["last_ping"], self.now.isoformat())
|
||||
self.assertEqual(checks["Alice 1"]["n_pings"], 1)
|
||||
self.assertEqual(checks["Alice 1"]["status"], "new")
|
||||
pause_url = "{0}/api/v1/checks/%s/pause".format(getattr(settings, "SITE_ROOT")) % self.a1.code
|
||||
|
||||
update_url = settings.SITE_ROOT + "/api/v1/checks/%s" % self.a1.code
|
||||
pause_url = update_url + "/pause"
|
||||
self.assertEqual(checks["Alice 1"]["update_url"], update_url)
|
||||
self.assertEqual(checks["Alice 1"]["pause_url"], pause_url)
|
||||
|
||||
next_ping = self.now + td(seconds=3600)
|
||||
|
@ -32,3 +32,17 @@ class PauseTestCase(BaseTestCase):
|
||||
HTTP_X_API_KEY="abc")
|
||||
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def test_it_validates_uuid(self):
|
||||
url = "/api/v1/checks/not-uuid/pause"
|
||||
r = self.client.post(url, "", content_type="application/json",
|
||||
HTTP_X_API_KEY="abc")
|
||||
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def test_it_handles_missing_check(self):
|
||||
url = "/api/v1/checks/07c2f548-9850-4b27-af5d-6c9dc157ec02/pause"
|
||||
r = self.client.post(url, "", content_type="application/json",
|
||||
HTTP_X_API_KEY="abc")
|
||||
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
77
hc/api/tests/test_update_check.py
Normal file
77
hc/api/tests/test_update_check.py
Normal file
@ -0,0 +1,77 @@
|
||||
import json
|
||||
|
||||
from hc.api.models import Channel, Check
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
|
||||
class UpdateCheckTestCase(BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(UpdateCheckTestCase, self).setUp()
|
||||
self.check = Check(user=self.alice)
|
||||
self.check.save()
|
||||
|
||||
def post(self, code, data):
|
||||
url = "/api/v1/checks/%s" % code
|
||||
r = self.client.post(url, json.dumps(data),
|
||||
content_type="application/json")
|
||||
|
||||
return r
|
||||
|
||||
def test_it_works(self):
|
||||
r = self.post(self.check.code, {
|
||||
"api_key": "abc",
|
||||
"name": "Foo",
|
||||
"tags": "bar,baz",
|
||||
"timeout": 3600,
|
||||
"grace": 60
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
doc = r.json()
|
||||
assert "ping_url" in doc
|
||||
self.assertEqual(doc["name"], "Foo")
|
||||
self.assertEqual(doc["tags"], "bar,baz")
|
||||
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)
|
||||
|
||||
self.check.refresh_from_db()
|
||||
self.assertEqual(self.check.name, "Foo")
|
||||
self.assertEqual(self.check.tags, "bar,baz")
|
||||
self.assertEqual(self.check.timeout.total_seconds(), 3600)
|
||||
self.assertEqual(self.check.grace.total_seconds(), 60)
|
||||
|
||||
def test_it_unassigns_channels(self):
|
||||
channel = Channel(user=self.alice)
|
||||
channel.save()
|
||||
|
||||
self.check.assign_all_channels()
|
||||
|
||||
r = self.post(self.check.code, {
|
||||
"api_key": "abc",
|
||||
"channels": ""
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
check = Check.objects.get()
|
||||
self.assertEqual(check.channel_set.count(), 0)
|
||||
|
||||
def test_it_requires_post(self):
|
||||
url = "/api/v1/checks/%s" % self.check.code
|
||||
r = self.client.get(url, HTTP_X_API_KEY="abc")
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
||||
def test_it_handles_invalid_uuid(self):
|
||||
r = self.post("not-an-uuid", {"api_key": "abc"})
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def test_it_handles_missing_check(self):
|
||||
made_up_code = "07c2f548-9850-4b27-af5d-6c9dc157ec02"
|
||||
r = self.post(made_up_code, {"api_key": "abc"})
|
||||
self.assertEqual(r.status_code, 400)
|
@ -6,6 +6,7 @@ urlpatterns = [
|
||||
url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"),
|
||||
url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"),
|
||||
url(r'^api/v1/checks/$', views.checks),
|
||||
url(r'^api/v1/checks/([\w-]+)$', views.update, name="hc-api-update"),
|
||||
url(r'^api/v1/checks/([\w-]+)/pause$', views.pause, name="hc-api-pause"),
|
||||
url(r'^badge/([\w-]+)/([\w-]{8})/([\w-]+).svg$', views.badge, name="hc-badge"),
|
||||
]
|
||||
|
@ -46,10 +46,30 @@ 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", "")
|
||||
def _lookup(user, spec):
|
||||
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=spec.get("name"))
|
||||
if "tags" in unique_fields:
|
||||
existing_checks = existing_checks.filter(tags=spec.get("tags"))
|
||||
if "timeout" in unique_fields:
|
||||
timeout = td(seconds=spec["timeout"])
|
||||
existing_checks = existing_checks.filter(timeout=timeout)
|
||||
if "grace" in unique_fields:
|
||||
grace = td(seconds=spec["grace"])
|
||||
existing_checks = existing_checks.filter(grace=grace)
|
||||
|
||||
return existing_checks.first()
|
||||
|
||||
|
||||
def _update(check, spec):
|
||||
if "name" in spec:
|
||||
check.name = spec["name"]
|
||||
|
||||
if "tags" in spec:
|
||||
check.tags = spec["tags"]
|
||||
|
||||
if "timeout" in spec and "schedule" not in spec:
|
||||
check.timeout = td(seconds=spec["timeout"])
|
||||
@ -63,31 +83,17 @@ def _create_check(user, spec):
|
||||
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()
|
||||
if "channels" in spec:
|
||||
if spec["channels"] == "*":
|
||||
check.assign_all_channels()
|
||||
elif spec["channels"] == "":
|
||||
check.channel_set.clear()
|
||||
|
||||
return JsonResponse(check.to_dict(), status=201)
|
||||
return check
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@ -100,13 +106,39 @@ def checks(request):
|
||||
return JsonResponse(doc)
|
||||
|
||||
elif request.method == "POST":
|
||||
return _create_check(request.user, request.json)
|
||||
created = False
|
||||
check = _lookup(request.user, request.json)
|
||||
if check is None:
|
||||
check = Check(user=request.user)
|
||||
created = True
|
||||
|
||||
_update(check, request.json)
|
||||
|
||||
return JsonResponse(check.to_dict(), status=201 if created else 200)
|
||||
|
||||
# If request is neither GET nor POST, return "405 Method not allowed"
|
||||
return HttpResponse(status=405)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@uuid_or_400
|
||||
@check_api_key
|
||||
@validate_json(schemas.check)
|
||||
def update(request, code):
|
||||
if request.method != "POST":
|
||||
return HttpResponse(status=405) # method not allowed
|
||||
|
||||
try:
|
||||
check = Check.objects.get(code=code, user=request.user)
|
||||
except Check.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
_update(check, request.json)
|
||||
return JsonResponse(check.to_dict(), status=200)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@uuid_or_400
|
||||
@check_api_key
|
||||
def pause(request, code):
|
||||
if request.method != "POST":
|
||||
|
@ -42,6 +42,8 @@ class Command(BaseCommand):
|
||||
_process("list_checks_response", lexers.JsonLexer())
|
||||
_process("create_check_request_a", lexers.BashLexer())
|
||||
_process("create_check_request_b", lexers.BashLexer())
|
||||
_process("update_check_request_a", lexers.BashLexer())
|
||||
_process("update_check_request_b", lexers.BashLexer())
|
||||
_process("create_check_response", lexers.JsonLexer())
|
||||
_process("pause_check_request", lexers.BashLexer())
|
||||
_process("pause_check_response", lexers.JsonLexer())
|
||||
|
@ -12,6 +12,7 @@ This is early days for healtchecks.io REST API. For now, there's API calls to:
|
||||
<ul>
|
||||
<li><a href="#list-checks">List existing checks</a></li>
|
||||
<li><a href="#create-check">Create a new check</a></li>
|
||||
<li><a href="#update-check">Update an existing check</a></li>
|
||||
<li><a href="#pause-check">Pause monitoring of a check</a></li>
|
||||
</ul>
|
||||
|
||||
@ -202,9 +203,112 @@ To create a "cron" check, specify the "schedule" and "tz" parameters.
|
||||
<h3 class="api-section">Example Response</h3>
|
||||
{% include "front/snippets/create_check_response.html" %}
|
||||
|
||||
|
||||
<!-- ********************************************************************** /-->
|
||||
|
||||
<a class="section" name="update-check">
|
||||
<h2 class="rule">Update an existing check</h2>
|
||||
</a>
|
||||
|
||||
<div class="api-path">POST {{ SITE_ROOT }}/api/v1/checks/<code></div>
|
||||
|
||||
<strong></strong>
|
||||
|
||||
<p>
|
||||
Updates an existing check. All request parameters are optional. The
|
||||
check is updated only with the supplied request parameters.
|
||||
If any parameter is omitted, its value is left unchanged.
|
||||
</p>
|
||||
|
||||
<h3 class="api-section">Request Parameters</h3>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>name</th>
|
||||
<td>
|
||||
<p>string, optional.</p>
|
||||
<p>Name for the check.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>tags</th>
|
||||
<td>
|
||||
<p>string, optional.</p>
|
||||
<p>A space-delimited list of tags for the check.</p>
|
||||
<p>Example:</p>
|
||||
<pre>{"tags": "reports staging"}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>timeout</th>
|
||||
<td>
|
||||
<p>number, optional.</p>
|
||||
<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>{"kind": "simple", "timeout": 300}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>grace</th>
|
||||
<td>
|
||||
<p>number, optional.</p>
|
||||
<p>A number of seconds, the grace period for this check.</p>
|
||||
<p>Minimum: 60 (one minute), maximum: 604800 (one week).</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>schedule</th>
|
||||
<td>
|
||||
<p>string, optional.</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.</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>
|
||||
<p>string, optional.</p>
|
||||
<p>Set this field to a special value "*"
|
||||
to automatically assign all existing notification channels.
|
||||
</p>
|
||||
<p>Set this field to a special value "" (empty string)
|
||||
to automatically <em>unassign</em> all notification channels.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3 class="api-section">Response Codes</h3>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>200 OK</th>
|
||||
<td>Returned if the check was successfully updated.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3 class="api-section">Example Request</h3>
|
||||
{% include "front/snippets/update_check_request_a.html" %}
|
||||
<br>
|
||||
<p>Or, alternatively:</p>
|
||||
{% include "front/snippets/update_check_request_b.html" %}
|
||||
|
||||
|
||||
<h3 class="api-section">Example Response</h3>
|
||||
{% include "front/snippets/create_check_response.html" %}
|
||||
|
||||
<!-- ********************************************************************** /-->
|
||||
|
||||
<a class="section" name="pause-check">
|
||||
<h2 class="rule">Pause Monitoring of a Check</h2>
|
||||
|
@ -8,6 +8,7 @@
|
||||
<span class="nt">"ping_url"</span><span class="p">:</span> <span class="s2">"{{ PING_ENDPOINT }}f618072a-7bde-4eee-af63-71a77c5723bc"</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">"tags"</span><span class="p">:</span> <span class="s2">"prod www"</span><span class="p">,</span>
|
||||
<span class="nt">"timeout"</span><span class="p">:</span> <span class="mi">3600</span>
|
||||
<span class="nt">"timeout"</span><span class="p">:</span> <span class="mi">3600</span><span class="p">,</span>
|
||||
<span class="nt">"update_url"</span><span class="p">:</span> <span class="s2">"{{ SITE_ROOT }}/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc"</span><span class="p">,</span>
|
||||
<span class="p">}</span>
|
||||
</pre></div>
|
||||
|
@ -8,5 +8,6 @@
|
||||
"ping_url": "PING_ENDPOINTf618072a-7bde-4eee-af63-71a77c5723bc",
|
||||
"status": "new",
|
||||
"tags": "prod www",
|
||||
"timeout": 3600
|
||||
"timeout": 3600,
|
||||
"update_url": "SITE_ROOT/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc",
|
||||
}
|
@ -10,7 +10,8 @@
|
||||
<span class="nt">"tags"</span><span class="p">:</span> <span class="s2">"foo"</span><span class="p">,</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="nt">"status"</span><span class="p">:</span> <span class="s2">"up"</span><span class="p">,</span>
|
||||
<span class="nt">"update_url"</span><span class="p">:</span> <span class="s2">"{{ SITE_ROOT }}/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"last_ping"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
@ -23,7 +24,8 @@
|
||||
<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="nt">"status"</span><span class="p">:</span> <span class="s2">"new"</span><span class="p">,</span>
|
||||
<span class="nt">"update_url"</span><span class="p">:</span> <span class="s2">"{{ SITE_ROOT }}/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced"</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
|
@ -10,7 +10,8 @@
|
||||
"tags": "foo",
|
||||
"pause_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause",
|
||||
"timeout": 3600,
|
||||
"status": "up"
|
||||
"status": "up",
|
||||
"update_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6"
|
||||
},
|
||||
{
|
||||
"last_ping": null,
|
||||
@ -23,7 +24,8 @@
|
||||
"pause_url": "SITE_ROOT/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced/pause",
|
||||
"tz": "UTC",
|
||||
"schedule": "0/10 * * * *",
|
||||
"status": "new"
|
||||
"status": "new",
|
||||
"update_url": "SITE_ROOT/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced"
|
||||
}
|
||||
]
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
<span class="nt">"ping_url"</span><span class="p">:</span> <span class="s2">"{{ PING_ENDPOINT }}f618072a-7bde-4eee-af63-71a77c5723bc"</span><span class="p">,</span>
|
||||
<span class="nt">"status"</span><span class="p">:</span> <span class="s2">"paused"</span><span class="p">,</span>
|
||||
<span class="nt">"tags"</span><span class="p">:</span> <span class="s2">"prod www"</span><span class="p">,</span>
|
||||
<span class="nt">"timeout"</span><span class="p">:</span> <span class="mi">3600</span>
|
||||
<span class="nt">"timeout"</span><span class="p">:</span> <span class="mi">3600</span><span class="p">,</span>
|
||||
<span class="nt">"update_url"</span><span class="p">:</span> <span class="s2">"{{ SITE_ROOT }}/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc"</span>
|
||||
<span class="p">}</span>
|
||||
</pre></div>
|
||||
|
@ -8,5 +8,6 @@
|
||||
"ping_url": "PING_ENDPOINTf618072a-7bde-4eee-af63-71a77c5723bc",
|
||||
"status": "paused",
|
||||
"tags": "prod www",
|
||||
"timeout": 3600
|
||||
"timeout": 3600,
|
||||
"update_url": "SITE_ROOT/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc"
|
||||
}
|
4
templates/front/snippets/update_check_request_a.html
Normal file
4
templates/front/snippets/update_check_request_a.html
Normal file
@ -0,0 +1,4 @@
|
||||
<div class="highlight"><pre><span></span>curl {{ SITE_ROOT }}/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc <span class="se">\</span>
|
||||
--header <span class="s2">"X-Api-Key: your-api-key"</span> <span class="se">\</span>
|
||||
--data <span class="s1">'{"name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'</span>
|
||||
</pre></div>
|
3
templates/front/snippets/update_check_request_a.txt
Normal file
3
templates/front/snippets/update_check_request_a.txt
Normal file
@ -0,0 +1,3 @@
|
||||
curl SITE_ROOT/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc \
|
||||
--header "X-Api-Key: your-api-key" \
|
||||
--data '{"name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'
|
3
templates/front/snippets/update_check_request_b.html
Normal file
3
templates/front/snippets/update_check_request_b.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="highlight"><pre><span></span>curl {{ SITE_ROOT }}/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc <span class="se">\</span>
|
||||
--data <span class="s1">'{"api_key": "your-api-key", "name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'</span>
|
||||
</pre></div>
|
2
templates/front/snippets/update_check_request_b.txt
Normal file
2
templates/front/snippets/update_check_request_b.txt
Normal file
@ -0,0 +1,2 @@
|
||||
curl SITE_ROOT/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc \
|
||||
--data '{"api_key": "your-api-key", "name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'
|
Loading…
x
Reference in New Issue
Block a user