diff --git a/CHANGELOG.md b/CHANGELOG.md index c1a8fd51..f3e0e108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. - Require confirmation codes (sent to email) before sensitive actions - Implement WebAuthn two-factor authentication - Implement badge mode (up/down vs up/late/down) selector (#282) +- Add Ping.exitstatus field, store client's reported exit status values (#455) ## v1.17.0 - 2020-10-14 diff --git a/hc/api/migrations/0076_auto_20201128_0951.py b/hc/api/migrations/0076_auto_20201128_0951.py new file mode 100644 index 00000000..80f1f743 --- /dev/null +++ b/hc/api/migrations/0076_auto_20201128_0951.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.2 on 2020-11-28 09:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0075_auto_20200805_1004'), + ] + + operations = [ + migrations.AddField( + model_name='ping', + name='exitstatus', + field=models.SmallIntegerField(null=True), + ), + migrations.AlterField( + model_name='channel', + name='kind', + field=models.CharField(choices=[('email', 'Email'), ('webhook', 'Webhook'), ('hipchat', 'HipChat'), ('slack', 'Slack'), ('pd', 'PagerDuty'), ('pagertree', 'PagerTree'), ('pagerteam', 'Pager Team'), ('po', 'Pushover'), ('pushbullet', 'Pushbullet'), ('opsgenie', 'OpsGenie'), ('victorops', 'VictorOps'), ('discord', 'Discord'), ('telegram', 'Telegram'), ('sms', 'SMS'), ('zendesk', 'Zendesk'), ('trello', 'Trello'), ('matrix', 'Matrix'), ('whatsapp', 'WhatsApp'), ('apprise', 'Apprise'), ('mattermost', 'Mattermost'), ('msteams', 'Microsoft Teams'), ('shell', 'Shell Command'), ('zulip', 'Zulip'), ('spike', 'Spike'), ('call', 'Phone Call'), ('linenotify', 'LINE Notify')], max_length=20), + ), + ] diff --git a/hc/api/models.py b/hc/api/models.py index b756f164..4cab92e9 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -252,7 +252,7 @@ class Check(models.Model): return result - def ping(self, remote_addr, scheme, method, ua, body, action): + def ping(self, remote_addr, scheme, method, ua, body, action, exitstatus=None): now = timezone.now() if self.status == "paused" and self.manual_resume: @@ -299,6 +299,7 @@ class Check(models.Model): # If User-Agent is longer than 200 characters, truncate it: ping.ua = ua[:200] ping.body = body[: settings.PING_BODY_LIMIT] + ping.exitstatus = exitstatus ping.save() def downtimes(self, months=3): @@ -351,6 +352,7 @@ class Ping(models.Model): method = models.CharField(max_length=10, blank=True) ua = models.CharField(max_length=200, blank=True) body = models.TextField(blank=True, null=True) + exitstatus = models.SmallIntegerField(null=True) def to_dict(self): return { diff --git a/hc/api/tests/test_ping.py b/hc/api/tests/test_ping.py index 81af3f84..726165be 100644 --- a/hc/api/tests/test_ping.py +++ b/hc/api/tests/test_ping.py @@ -11,7 +11,7 @@ class PingTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check.objects.create(project=self.project) - self.url = "/ping/%s/" % self.check.code + self.url = "/ping/%s" % self.check.code def test_it_works(self): r = self.client.get(self.url) @@ -26,6 +26,7 @@ class PingTestCase(BaseTestCase): self.assertEqual(ping.scheme, "http") self.assertEqual(ping.kind, None) self.assertEqual(ping.created, self.check.last_ping) + self.assertIsNone(ping.exitstatus) def test_it_changes_status_of_paused_check(self): self.check.status = "paused" @@ -234,6 +235,7 @@ class PingTestCase(BaseTestCase): ping = Ping.objects.latest("id") self.assertEqual(ping.kind, None) + self.assertEqual(ping.exitstatus, 0) def test_nonzero_exit_status_works(self): r = self.client.get("/ping/%s/123" % self.check.code) @@ -244,3 +246,4 @@ class PingTestCase(BaseTestCase): ping = Ping.objects.latest("id") self.assertEqual(ping.kind, "fail") + self.assertEqual(ping.exitstatus, 123) diff --git a/hc/api/views.py b/hc/api/views.py index d16b8e95..3103064e 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -30,7 +30,7 @@ class BadChannelException(Exception): @csrf_exempt @never_cache -def ping(request, code, action="success", exitstatus=0): +def ping(request, code, action="success", exitstatus=None): check = get_object_or_404(Check, code=code) headers = request.META @@ -41,13 +41,13 @@ def ping(request, code, action="success", exitstatus=0): ua = headers.get("HTTP_USER_AGENT", "") body = request.body.decode() - if exitstatus > 0: + if exitstatus is not None and exitstatus > 0: action = "fail" if check.methods == "POST" and method != "POST": action = "ign" - check.ping(remote_addr, scheme, method, ua, body, action) + check.ping(remote_addr, scheme, method, ua, body, action, exitstatus) response = HttpResponse("OK") response["Access-Control-Allow-Origin"] = "*" diff --git a/hc/front/tests/test_ping_details.py b/hc/front/tests/test_ping_details.py index 202ae892..ef14ea9f 100644 --- a/hc/front/tests/test_ping_details.py +++ b/hc/front/tests/test_ping_details.py @@ -59,3 +59,17 @@ class PingDetailsTestCase(BaseTestCase): self.client.login(username="alice@example.org", password="password") r = self.client.get("/checks/%s/pings/123/" % self.check.code) self.assertContains(r, "No additional information is", status_code=200) + + def test_it_shows_nonzero_exitstatus(self): + Ping.objects.create(owner=self.check, kind="fail", exitstatus=42) + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(self.url) + self.assertContains(r, "(failure, exit status 42)", status_code=200) + + def test_it_shows_zero_exitstatus(self): + Ping.objects.create(owner=self.check, exitstatus=0) + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(self.url) + self.assertContains(r, "(exit status 0)", status_code=200) diff --git a/templates/front/details_events.html b/templates/front/details_events.html index 4ada94f4..586c8a0b 100644 --- a/templates/front/details_events.html +++ b/templates/front/details_events.html @@ -10,7 +10,9 @@
/fail
endpoint)
{% elif ping.kind == "start" %}
(received via the /start
endpoint)