Browse Source

Add "Shell Commands" integration. Fixes #302

pull/307/head
Pēteris Caune 5 years ago
parent
commit
8d81ea8f9d
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
22 changed files with 404 additions and 32 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +12
    -0
      README.md
  3. +18
    -9
      hc/api/models.py
  4. +47
    -13
      hc/api/tests/test_notify.py
  5. +46
    -1
      hc/api/transports.py
  6. +10
    -0
      hc/front/forms.py
  7. +53
    -0
      hc/front/tests/test_add_shell.py
  8. +2
    -2
      hc/front/tests/test_channels.py
  9. +1
    -0
      hc/front/urls.py
  10. +28
    -0
      hc/front/views.py
  11. +21
    -0
      hc/lib/tests/test_string.py
  12. +3
    -0
      hc/settings.py
  13. +8
    -5
      static/css/icomoon.css
  14. BIN
      static/fonts/icomoon.eot
  15. +1
    -0
      static/fonts/icomoon.svg
  16. BIN
      static/fonts/icomoon.ttf
  17. BIN
      static/fonts/icomoon.woff
  18. BIN
      static/img/integrations/shell.png
  19. +26
    -1
      templates/front/channels.html
  20. +5
    -0
      templates/front/event_summary.html
  21. +121
    -0
      templates/integrations/add_shell.html
  22. +1
    -1
      templates/integrations/add_webhook.html

+ 1
- 0
CHANGELOG.md View File

@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
- In monthly reports, no downtime stats for the current month (month has just started)
- Add Microsoft Teams integration (#135)
- Add Profile.last_active_date field for more accurate inactive user detection
- Add "Shell Commands" integration (#302)
### Bug Fixes
- On mobile, "My Checks" page, always show the gear (Details) button (#286)


+ 12
- 0
README.md View File

@ -134,6 +134,7 @@ Configurations settings loaded from environment variables:
| MATRIX_USER_ID | `None`
| MATRIX_ACCESS_TOKEN | `None`
| APPRISE_ENABLED | `"False"`
| SHELL_ENABLED | `"False"`
Some useful settings keys to override are:
@ -361,6 +362,17 @@ pip install apprise
```
* enable the apprise functionality by setting the `APPRISE_ENABLED` environment variable.
### Shell Commands
The "Shell Commands" integration runs user-defined local shell commands when checks
go up or down. This integration is disabled by default, and can be enabled by setting
the `SHELL_ENABLED` environment variable to `True`.
Note: be careful when using "Shell Commands" integration, and only enable it when
you fully trust the users of your Healthchecks instance. The commands will be executed
by the `manage.py sendalerts` process, and will run with the same system permissions as
the `sendalerts` process.
## Running in Production
Here is a non-exhaustive list of pointers and things to check before launching a Healthchecks instance


+ 18
- 9
hc/api/models.py View File

@ -46,6 +46,7 @@ CHANNEL_KINDS = (
("apprise", "Apprise"),
("mattermost", "Mattermost"),
("msteams", "Microsoft Teams"),
("shell", "Shell Command"),
)
PO_PRIORITIES = {-2: "lowest", -1: "low", 0: "normal", 1: "high", 2: "emergency"}
@ -413,6 +414,8 @@ class Channel(models.Model):
return transports.Apprise(self)
elif self.kind == "msteams":
return transports.MsTeams(self)
elif self.kind == "shell":
return transports.Shell(self)
else:
raise NotImplementedError("Unknown channel kind: %s" % self.kind)
@ -438,6 +441,10 @@ class Channel(models.Model):
def icon_path(self):
return "img/integrations/%s.png" % self.kind
@property
def json(self):
return json.loads(self.value)
@property
def po_priority(self):
assert self.kind == "po"
@ -502,6 +509,16 @@ class Channel(models.Model):
def url_up(self):
return self.up_webhook_spec["url"]
@property
def cmd_down(self):
assert self.kind == "shell"
return self.json["cmd_down"]
@property
def cmd_up(self):
assert self.kind == "shell"
return self.json["cmd_up"]
@property
def slack_team(self):
assert self.kind == "slack"
@ -586,13 +603,6 @@ class Channel(models.Model):
return doc["value"]
return self.value
@property
def sms_label(self):
assert self.kind == "sms"
if self.value.startswith("{"):
doc = json.loads(self.value)
return doc["label"]
@property
def trello_token(self):
assert self.kind == "trello"
@ -620,8 +630,7 @@ class Channel(models.Model):
if not self.value.startswith("{"):
return self.value
doc = json.loads(self.value)
return doc.get("value")
return self.json["value"]
@property
def email_notify_up(self):


+ 47
- 13
hc/api/tests/test_notify.py View File

@ -73,19 +73,6 @@ class NotifyTestCase(BaseTestCase):
n = Notification.objects.get()
self.assertEqual(n.error, "Received status code 500")
@patch("hc.api.transports.requests.request")
def test_webhooks_support_tags(self, mock_get):
template = "http://host/$TAGS"
self._setup_data("webhook", template)
self.check.tags = "foo bar"
self.check.save()
self.channel.notify(self.check)
args, kwargs = mock_get.call_args
self.assertEqual(args[0], "get")
self.assertEqual(args[1], "http://host/foo%20bar")
@patch("hc.api.transports.requests.request")
def test_webhooks_support_variables(self, mock_get):
template = "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME"
@ -711,3 +698,50 @@ class NotifyTestCase(BaseTestCase):
args, kwargs = mock_post.call_args
payload = kwargs["json"]
self.assertEqual(payload["@type"], "MessageCard")
@patch("hc.api.transports.os.system")
@override_settings(SHELL_ENABLED=True)
def test_shell(self, mock_system):
definition = {"cmd_down": "logger hello", "cmd_up": ""}
self._setup_data("shell", json.dumps(definition))
mock_system.return_value = 0
self.channel.notify(self.check)
mock_system.assert_called_with("logger hello")
@patch("hc.api.transports.os.system")
@override_settings(SHELL_ENABLED=True)
def test_shell_handles_nonzero_exit_code(self, mock_system):
definition = {"cmd_down": "logger hello", "cmd_up": ""}
self._setup_data("shell", json.dumps(definition))
mock_system.return_value = 123
self.channel.notify(self.check)
n = Notification.objects.get()
self.assertEqual(n.error, "Command returned exit code 123")
@patch("hc.api.transports.os.system")
@override_settings(SHELL_ENABLED=True)
def test_shell_supports_variables(self, mock_system):
definition = {"cmd_down": "logger $NAME is $STATUS ($TAG1)", "cmd_up": ""}
self._setup_data("shell", json.dumps(definition))
mock_system.return_value = 0
self.check.name = "Database"
self.check.tags = "foo bar"
self.check.save()
self.channel.notify(self.check)
mock_system.assert_called_with("logger Database is down (foo)")
@patch("hc.api.transports.os.system")
@override_settings(SHELL_ENABLED=False)
def test_shell_disabled(self, mock_system):
definition = {"cmd_down": "logger hello", "cmd_up": ""}
self._setup_data("shell", json.dumps(definition))
self.channel.notify(self.check)
self.assertFalse(mock_system.called)
n = Notification.objects.get()
self.assertEqual(n.error, "Shell commands are not enabled")

+ 46
- 1
hc/api/transports.py View File

@ -1,3 +1,5 @@
import os
from django.conf import settings
from django.template.loader import render_to_string
from django.utils import timezone
@ -7,6 +9,7 @@ from urllib.parse import quote, urlencode
from hc.accounts.models import Profile
from hc.lib import emails
from hc.lib.string import replace
try:
import apprise
@ -90,6 +93,48 @@ class Email(Transport):
return not self.channel.email_notify_up
class Shell(Transport):
def prepare(self, template, check):
""" Replace placeholders with actual values. """
ctx = {
"$CODE": str(check.code),
"$STATUS": check.status,
"$NOW": timezone.now().replace(microsecond=0).isoformat(),
"$NAME": check.name,
"$TAGS": check.tags,
}
for i, tag in enumerate(check.tags_list()):
ctx["$TAG%d" % (i + 1)] = tag
return replace(template, ctx)
def is_noop(self, check):
if check.status == "down" and not self.channel.cmd_down:
return True
if check.status == "up" and not self.channel.cmd_up:
return True
return False
def notify(self, check):
if not settings.SHELL_ENABLED:
return "Shell commands are not enabled"
if check.status == "up":
cmd = self.channel.cmd_up
elif check.status == "down":
cmd = self.channel.cmd_down
cmd = self.prepare(cmd, check)
code = os.system(cmd)
if code != 0:
return "Command returned exit code %d" % code
class HttpTransport(Transport):
@classmethod
def _request(cls, method, url, **kwargs):
@ -479,7 +524,7 @@ class Apprise(HttpTransport):
if not settings.APPRISE_ENABLED:
# Not supported and/or enabled
return "Apprise is disabled and/or not installed."
return "Apprise is disabled and/or not installed"
a = apprise.Apprise()
title = tmpl("apprise_title.html", check=check)


+ 10
- 0
hc/front/forms.py View File

@ -125,6 +125,16 @@ class AddWebhookForm(forms.Form):
return json.dumps(dict(self.cleaned_data), sort_keys=True)
class AddShellForm(forms.Form):
error_css_class = "has-error"
cmd_down = forms.CharField(max_length=1000, required=False)
cmd_up = forms.CharField(max_length=1000, required=False)
def get_value(self):
return json.dumps(dict(self.cleaned_data), sort_keys=True)
phone_validator = RegexValidator(
regex="^\+\d{5,15}$", message="Invalid phone number format."
)


+ 53
- 0
hc/front/tests/test_add_shell.py View File

@ -0,0 +1,53 @@
from django.test.utils import override_settings
from hc.api.models import Channel
from hc.test import BaseTestCase
@override_settings(SHELL_ENABLED=True)
class AddShellTestCase(BaseTestCase):
url = "/integrations/add_shell/"
@override_settings(SHELL_ENABLED=False)
def test_it_is_disabled_by_default(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 404)
def test_instructions_work(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url)
self.assertContains(r, "Executes a local shell command")
def test_it_adds_two_commands_and_redirects(self):
form = {"cmd_down": "logger down", "cmd_up": "logger up"}
self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, form)
self.assertRedirects(r, "/integrations/")
c = Channel.objects.get()
self.assertEqual(c.project, self.project)
self.assertEqual(c.cmd_down, "logger down")
self.assertEqual(c.cmd_up, "logger up")
def test_it_adds_webhook_using_team_access(self):
form = {"cmd_down": "logger down", "cmd_up": "logger up"}
# Logging in as bob, not alice. Bob has team access so this
# should work.
self.client.login(username="[email protected]", password="password")
self.client.post(self.url, form)
c = Channel.objects.get()
self.assertEqual(c.project, self.project)
self.assertEqual(c.cmd_down, "logger down")
def test_it_handles_empty_down_command(self):
form = {"cmd_down": "", "cmd_up": "logger up"}
self.client.login(username="[email protected]", password="password")
self.client.post(self.url, form)
c = Channel.objects.get()
self.assertEqual(c.cmd_down, "")
self.assertEqual(c.cmd_up, "logger up")

+ 2
- 2
hc/front/tests/test_channels.py View File

@ -96,9 +96,9 @@ class ChannelsTestCase(BaseTestCase):
self.assertEqual(r.status_code, 200)
self.assertContains(r, "(up only)")
def test_it_shows_sms_label(self):
def test_it_shows_sms_number(self):
ch = Channel(kind="sms", project=self.project)
ch.value = json.dumps({"value": "+123", "label": "My Phone"})
ch.value = json.dumps({"value": "+123"})
ch.save()
self.client.login(username="[email protected]", password="password")


+ 1
- 0
hc/front/urls.py View File

@ -26,6 +26,7 @@ channel_urls = [
path("", views.channels, name="hc-channels"),
path("add_email/", views.add_email, name="hc-add-email"),
path("add_webhook/", views.add_webhook, name="hc-add-webhook"),
path("add_shell/", views.add_shell, name="hc-add-shell"),
path("add_pd/", views.add_pd, name="hc-add-pd"),
path("add_pd/<str:state>/", views.add_pd, name="hc-add-pd-state"),
path("add_pagertree/", views.add_pagertree, name="hc-add-pagertree"),


+ 28
- 0
hc/front/views.py View File

@ -46,6 +46,7 @@ from hc.front.forms import (
EmailSettingsForm,
AddMatrixForm,
AddAppriseForm,
AddShellForm,
)
from hc.front.schemas import telegram_callback
from hc.front.templatetags.hc_extras import num_down_title, down_title, sortchecks
@ -651,6 +652,7 @@ def channels(request):
"enable_trello": settings.TRELLO_APP_KEY is not None,
"enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
"enable_apprise": settings.APPRISE_ENABLED is True,
"enable_shell": settings.SHELL_ENABLED is True,
"use_payments": settings.USE_PAYMENTS,
}
@ -816,6 +818,32 @@ def add_webhook(request):
return render(request, "integrations/add_webhook.html", ctx)
@login_required
def add_shell(request):
if not settings.SHELL_ENABLED:
raise Http404("shell integration is not available")
if request.method == "POST":
form = AddShellForm(request.POST)
if form.is_valid():
channel = Channel(project=request.project, kind="shell")
channel.value = form.get_value()
channel.save()
channel.assign_all_checks()
return redirect("hc-channels")
else:
form = AddShellForm()
ctx = {
"page": "channels",
"project": request.project,
"form": form,
"now": timezone.now().replace(microsecond=0).isoformat(),
}
return render(request, "integrations/add_shell.html", ctx)
def _prepare_state(request, session_key):
state = get_random_string()
request.session[session_key] = state


+ 21
- 0
hc/lib/tests/test_string.py View File

@ -0,0 +1,21 @@
from django.test import TestCase
from hc.lib.string import replace
class StringTestCase(TestCase):
def test_it_works(self):
result = replace("$A is $B", {"$A": "aaa", "$B": "bbb"})
self.assertEqual(result, "aaa is bbb")
def test_it_ignores_placeholders_in_values(self):
result = replace("$A is $B", {"$A": "$B", "$B": "$A"})
self.assertEqual(result, "$B is $A")
def test_it_ignores_overlapping_placeholders(self):
result = replace("$$AB", {"$A": "", "$B": "text"})
self.assertEqual(result, "$B")
def test_it_preserves_non_placeholder_dollar_signs(self):
result = replace("$3.50", {"$A": "text"})
self.assertEqual(result, "$3.50")

+ 3
- 0
hc/settings.py View File

@ -207,6 +207,9 @@ MATRIX_ACCESS_TOKEN = os.getenv("MATRIX_ACCESS_TOKEN")
# Apprise
APPRISE_ENABLED = envbool("APPRISE_ENABLED", "False")
# Local shell commands
SHELL_ENABLED = envbool("SHELL_ENABLED", "False")
if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")):
from .local_settings import *


+ 8
- 5
static/css/icomoon.css View File

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('../fonts/icomoon.eot?tg9zp8');
src: url('../fonts/icomoon.eot?tg9zp8#iefix') format('embedded-opentype'),
url('../fonts/icomoon.ttf?tg9zp8') format('truetype'),
url('../fonts/icomoon.woff?tg9zp8') format('woff'),
url('../fonts/icomoon.svg?tg9zp8#icomoon') format('svg');
src: url('../fonts/icomoon.eot?r6898m');
src: url('../fonts/icomoon.eot?r6898m#iefix') format('embedded-opentype'),
url('../fonts/icomoon.ttf?r6898m') format('truetype'),
url('../fonts/icomoon.woff?r6898m') format('woff'),
url('../fonts/icomoon.svg?r6898m#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
@ -24,6 +24,9 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-shell:before {
content: "\e917";
}
.icon-msteams:before {
content: "\e916";
color: #4e56be;


BIN
static/fonts/icomoon.eot View File


+ 1
- 0
static/fonts/icomoon.svg View File

@ -41,4 +41,5 @@
<glyph unicode="&#xe914;" glyph-name="pagerteam" horiz-adv-x="981" d="M484.289 621.868c-67.542-1.322-134.652-26.387-187.221-70.263-56.96-47.541-94.861-114.277-106.117-186.879-3.371-21.755-3.183-14.333-3.367-130.672-0.144-87.121-0.309-106.544-0.894-107.030-31.25-1.325-94.191-0.875-94.191-0.875s-0.436-123.022-0.285-189.922l799.764-0.228-0.323 189.561-94.818 0.856-0.228 104.062c-0.235 112.101-0.168 109.401-3.138 130.159-1.569 10.965-4.997 27.461-7.798 37.528-26.226 94.196-95.127 169.766-186.194 204.207-32.295 12.214-64.792 18.42-101.666 19.439-4.502 0.124-9.021 0.145-13.524 0.057zM504.47 546.85c51.456 0 106.155-17.857 149.103-56.434s72.159-98.866 70.871-174.553c-1.391-48.523-73.925-47.248-73.61 1.293 0.97 57.004-18.643 93.453-46.506 118.48s-65.885 37.604-99.859 37.604c-49.962-0.897-49.962 74.507 0 73.61zM38.407 694.94c-0.476 0.022-1.035 0.035-1.596 0.035-20.33 0-36.811-16.481-36.811-36.811 0-13.706 7.49-25.662 18.6-31.998l0.181-0.095 156.359-91.3c5.323-3.162 11.735-5.030 18.583-5.030 20.347 0 36.841 16.494 36.841 36.841 0 13.498-7.259 25.301-18.087 31.717l-0.171 0.094-156.431 91.3c-4.992 3.055-10.98 4.971-17.393 5.245l-0.076 0.003zM232.796 876.532c-19.71-0.785-35.391-16.953-35.391-36.784 0-6.961 1.932-13.471 5.29-19.023l-0.092 0.165 87.202-150.968c6.296-11.786 18.515-19.67 32.577-19.67 20.33 0 36.811 16.481 36.811 36.811 0 7.295-2.122 14.094-5.782 19.814l0.088-0.148-87.13 150.968c-6.42 11.337-18.401 18.863-32.139 18.863-0.504 0-1.006-0.010-1.505-0.030l0.072 0.002zM492.029 959.996c-20.081-0.324-36.236-16.679-36.236-36.807 0-0.177 0.001-0.354 0.004-0.531v0.027-187.2c-0.002-0.155-0.004-0.338-0.004-0.521 0-20.33 16.481-36.811 36.811-36.811s36.811 16.481 36.811 36.811c0 0.183-0.001 0.366-0.004 0.548v-0.028 187.2c0.002 0.15 0.003 0.327 0.003 0.504 0 20.33-16.481 36.811-36.811 36.811-0.202 0-0.404-0.002-0.605-0.005h0.030zM945.507 690.842c-0.29 0.008-0.632 0.013-0.974 0.013-7.054 0-13.644-1.984-19.243-5.424l0.16 0.091-156.431-91.3c-11.571-6.357-19.282-18.463-19.282-32.37 0-20.33 16.481-36.811 36.811-36.811 7.253 0 14.016 2.098 19.716 5.719l-0.15-0.089 156.431 91.3c11.271 6.437 18.746 18.382 18.746 32.073 0 19.969-15.9 36.224-35.731 36.795l-0.053 0.001zM747.38 872.362c-0.089 0.001-0.193 0.001-0.298 0.001-13.728 0-25.7-7.514-32.029-18.654l-0.095-0.182-87.13-150.895c-3.572-5.572-5.694-12.371-5.694-19.666 0-20.33 16.481-36.811 36.811-36.811 14.061 0 26.281 7.884 32.48 19.472l0.096 0.198 87.202 150.895c3.256 5.381 5.182 11.882 5.182 18.832 0 20.23-16.319 36.648-36.51 36.81h-0.015z" />
<glyph unicode="&#xe915;" glyph-name="apprise" horiz-adv-x="1103" d="M419.207-63.539c-36.821 10.251-62.633 68.381-78.184 96.84s-44.871 79.948-44.871 79.948l144.118 77.477c0 0 60.549-101.414 89.638-152.536 19.503-48.548-37.228-71.026-70.145-90.61-12.11-6.879-26.274-13.39-40.556-11.119zM139.125 137.497c-83.563 6.246-150.932 89.762-137.383 173.161 0.044 28.578 33.377 106.495 61.177 57.277 41.786-74.223 86.086-147.054 127.101-221.634-9.907-13.558-36.039-7.416-50.895-8.805zM256.767 178.268c-51.040 82.94-97.903 168.519-147.818 252.248 31.046 22.803 61.092 39.433 87.762 60.464 113.646 71.464 237.133 203.369 288.762 347.602 13.484 45.244 66.37 79.001 93.522 38.262 100.485-174.847 203.317-348.42 302.511-523.936 17.51-66.627-63.993-53.787-103.86-44.62-133.333 17.402-276.261 7.503-394.63-61.032-41.186-22.873-80.753-48.963-122.811-70.028l-3.438 1.038zM1008.674 488.667c-59.824 20.665 2.515 73.201 14.237 107.157 44.133 94.328 5.38 215.539-83.422 269.141-47.146 29.856-104.57 37.992-159.139 29.894-49.006 8.783-26.794 61.723 19.937 63.521 135.186 15.694 273.035-84.419 296.526-219.010 18.169-86.287-5.187-184.47-69.789-246.399-5.822-2.236-11.938-5.013-18.349-4.303zM874.499 536.119c-56.018 26.015 12.996 72.844 8.156 111.868 9.085 66.073-58.288 124.609-122.441 110.005-37.378 8.906-34.985 58.261 13.385 63.11 100.043 8.227 190.553-92.3 170.885-191.055-6.546-34.584-27.598-94.615-69.985-93.926z" />
<glyph unicode="&#xe916;" glyph-name="msteams" horiz-adv-x="1082" d="M46.124 716.056c-25.473 0-46.124-20.65-46.124-46.124v-461.34c0-25.473 20.65-46.124 46.124-46.124h461.34c25.473 0 46.124 20.65 46.124 46.124v461.34c0 25.473-20.65 46.124-46.124 46.124zM155.407 589.183h242.773v-48.716h-92.223v-251.128h-58.755v251.128h-91.795zM875.863 565.077c22.27-4.283 38.848-24.124 38.305-47.545v-290.357c1.163-49.999-10.679-97.28-32.435-138.607 7.763-1.047 15.685-1.59 23.734-1.589h0.831c97.044 0 175.713 78.669 175.713 175.713v254.575c0 26.405-21.405 47.809-47.809 47.809zM1056.849 728.637c0-62.537-50.697-113.234-113.234-113.234s-113.234 50.697-113.234 113.234c0 62.537 50.697 113.234 113.234 113.234s113.234-50.697 113.234-113.234zM591.332 960c-90.332 0-163.56-73.229-163.56-163.56 0 0 0 0 0-0.001v0c0.070-13.428 1.748-26.431 4.85-38.871l-0.238 1.126h125.481c25.392-0.096 45.952-20.656 46.049-46.048v-79.234c84.737 6.734 150.952 77.144 150.978 163.024v0.002c0 0 0 0 0 0 0 90.332-73.228 163.56-163.56 163.56 0 0 0 0 0 0v0zM433.549 753.505c0.349-1.523 0.451-1.91 0.554-2.296l-0.259 1.142c-0.103 0.383-0.195 0.77-0.295 1.154zM445.484 722.433c0.5-1.032 0.572-1.168 0.643-1.303l-0.429 0.89c-0.071 0.138-0.144 0.275-0.214 0.413zM453.43 708.54c0.573-0.936 0.756-1.218 0.939-1.5l-0.386 0.634c-0.186 0.288-0.368 0.578-0.553 0.867zM462.734 695.437c0.587-0.757 0.913-1.165 1.241-1.573l-0.246 0.316c-0.334 0.416-0.664 0.836-0.995 1.256zM473.304 683.28c0.541-0.563 1.035-1.070 1.532-1.573l-0.017 0.017c-0.508 0.515-1.014 1.034-1.515 1.556zM484.912 672.322c0.495-0.431 1.21-1.036 1.93-1.635l0.266-0.215c-0.738 0.61-1.47 1.227-2.197 1.85zM497.604 662.494c0.406-0.305 1.331-0.945 2.263-1.576l0.577-0.368c-0.955 0.638-1.9 1.287-2.841 1.944zM511.254 653.922c0.322-0.22 1.457-0.849 2.601-1.465l0.87-0.428c-1.165 0.617-2.322 1.249-3.471 1.893zM525.667 646.743c0.317-0.191 1.683-0.778 3.058-1.347l1.085-0.398c-1.39 0.564-2.772 1.144-4.144 1.745zM540.6 641.060c0.485-0.213 2.136-0.735 3.798-1.232l1.158-0.297c-1.663 0.484-3.314 0.994-4.955 1.528zM557.738 636.421c-0.543 0.086 0.017-0.041 0.579-0.165l1.085-0.201c-0.556 0.117-1.11 0.242-1.664 0.366zM603.455 633.357c-0.696-0.041-1.385-0.082-2.071-0.121 1.133 0.057 1.851 0.1 2.568 0.148l-0.498-0.027zM602.36 565.077v-406.887c-0.125-18.659-11.432-35.421-28.686-42.525-5.493-2.324-11.397-3.522-17.362-3.523h-233.326c41.443-101.046 139.606-173.299 255.765-176.142 156.566 3.832 280.436 133.761 276.794 290.331v290.357c0.604 26.091-20.034 47.743-46.125 48.389z" />
<glyph unicode="&#xe917;" glyph-name="shell" d="M109.229 960c-60.512 0-109.229-48.717-109.229-109.229v-805.546c0-60.512 48.717-109.223 109.229-109.223h805.546c60.512 0 109.223 48.712 109.223 109.223v805.546c0 60.512-48.712 109.229-109.223 109.229h-805.546zM293.258 784.868h56.588v-76.746c21.22-2.829 40.317-8.607 57.293-17.33s31.36-20.161 43.149-34.308c12.025-13.911 21.217-30.412 27.583-49.51 6.602-18.862 9.905-40.082 9.905-63.66h-98.32c0 28.529-6.485 50.1-19.452 64.718-12.968 14.854-30.533 22.28-52.696 22.28-12.025 0-22.515-1.649-31.474-4.95-8.724-3.065-15.916-7.544-21.575-13.439-5.659-5.659-9.904-12.377-12.733-20.158-2.594-7.781-3.892-16.271-3.892-25.466s1.298-17.446 3.892-24.755c2.829-7.073 7.425-13.675 13.791-19.805 6.602-6.13 15.209-12.024 25.819-17.683 10.61-5.423 23.813-10.966 39.61-16.625 23.813-8.96 45.39-18.269 64.724-27.936 19.334-9.431 35.835-20.517 49.51-33.249 13.911-12.496 24.524-27.115 31.833-43.855 7.545-16.504 11.316-36.070 11.316-58.704 0-20.748-3.421-39.495-10.258-56.235-6.838-16.504-16.62-30.766-29.352-42.791s-28.058-21.696-45.977-29.005c-17.919-7.073-37.964-11.669-60.127-13.791v-69.315h-56.229v69.315c-20.041 1.886-39.495 6.248-58.357 13.086-18.862 7.073-35.603 17.213-50.221 30.416-14.382 13.204-25.931 29.827-34.655 49.868-8.724 20.277-13.086 44.561-13.086 72.854h98.673c0-16.976 2.474-31.243 7.425-42.796 4.951-11.317 11.319-20.393 19.1-27.23 8.016-6.602 17.092-11.315 27.23-14.144s20.512-4.244 31.122-4.244c25.228 0 44.326 5.894 57.293 17.683s19.452 26.992 19.452 45.619c0 9.903-1.532 18.627-4.597 26.172-2.829 7.781-7.542 14.856-14.144 21.222-6.366 6.366-14.856 12.143-25.466 17.33-10.374 5.423-22.987 10.726-37.841 15.914-23.813 8.488-45.507 17.446-65.076 26.877s-36.31 20.395-50.221 32.891c-13.675 12.496-24.282 26.998-31.827 43.502-7.545 16.74-11.316 36.545-11.316 59.415 0 20.041 3.415 38.314 10.253 54.818 6.838 16.74 16.509 31.241 29.005 43.502s27.583 22.16 45.266 29.705c17.683 7.545 37.371 12.38 59.063 14.502v76.040zM571.594 187.88h322.544v-76.746h-322.544v76.746z" />
</font></defs></svg>

BIN
static/fonts/icomoon.ttf View File


BIN
static/fonts/icomoon.woff View File


BIN
static/img/integrations/shell.png View File

Before After
Width: 94  |  Height: 94  |  Size: 1.8 KiB

+ 26
- 1
templates/front/channels.html View File

@ -105,7 +105,7 @@
{% elif ch.kind == "msteams" %}
Microsoft Teams
{% else %}
{{ ch.kind }}
{{ ch.get_kind_display }}
{% endif %}
</div>
</div>
@ -329,6 +329,18 @@
</li>
{% endif %}
{% if enable_shell %}
<li>
<img src="{% static 'img/integrations/shell.png' %}"
class="icon" alt="Shell icon" />
<h2>Shell Command</h2>
<p>Execute a local shell command when a check goes up or down.</p>
<a href="{% url 'hc-add-shell' %}" class="btn btn-primary">Add Integration</a>
</li>
{% endif %}
{% if enable_sms %}
<li>
<img src="{% static 'img/integrations/sms.png' %}"
@ -505,6 +517,19 @@
{% endif %}
{% endwith %}
{% endif %}
{% if ch.kind == "shell" %}
{% if ch.cmd_down %}
<p><strong>Execute on "down" events:</strong></p>
<pre>{{ ch.cmd_down }}</pre>
{% endif %}
{% if ch.cmd_up %}
<p><strong>Execute on "up" events:</strong></p>
<pre>{{ ch.cmd_up }}</pre>
{% endif %}
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>


+ 5
- 0
templates/front/event_summary.html View File

@ -37,6 +37,11 @@
{% elif event.channel.kind == "trello" %}
Added Trello card in
board "{{ event.channel.trello_board_list|first }}"
{% elif event.channel.kind == "shell" %}
Executed a shell command
{% if event.channel.name %}
({{ event.channel.name }})
{% endif %}
{% else %}
Sent alert to {{ event.channel.kind|capfirst }}
{% endif %}


+ 121
- 0
templates/integrations/add_shell.html View File

@ -0,0 +1,121 @@
{% extends "base.html" %}
{% load compress humanize static hc_extras %}
{% block title %}Add Shell Command - {% site_name %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<h1>Shell Command</h1>
<p>Executes a local shell command when a check
goes up or down.</p>
<p>
You can use placeholders <strong>$NAME</strong>, <strong>$STATUS</strong>
and others
<a href="#" data-toggle="modal" data-target="#reference-modal">(quick reference)</a>.
</p>
<br />
<form id="add-shell-form" method="post">
{% csrf_token %}
<div class="row">
<div class="col-sm-6">
<div class="form-group {{ form.cmd_down.css_classes }}">
<label class="control-label">Execute when a check goes <span class="label-down">down</span></label>
<textarea
class="form-control"
rows="3"
name="cmd_down"
placeholder='/home/user/notify.sh "$NAME has gone down"'>{{ form.cmd_down.value|default:"" }}</textarea>
{% if form.cmd_down.errors %}
<div class="help-block">
{{ form.cmd_down.errors|join:"" }}
</div>
{% endif %}
</div>
</div>
<div class="col-sm-6">
<div class="form-group {{ form.cmd_up.css_classes }}">
<label class="control-label">Execute when a check goes <span class="label-up">up</span></label>
<textarea
class="form-control"
rows="3"
name="cmd_up"
placeholder='/home/user/notify.sh "$NAME is back up"'>{{ form.cmd_up.value|default:"" }}</textarea>
{% if form.cmd_up.errors %}
<div class="help-block">
{{ form.cmd_up.errors|join:"" }}
</div>
{% endif %}
</div>
</div>
</div>
<div class="form-group" class="clearfix">
<br>
<br>
<div class="text-right">
<button type="submit" class="btn btn-primary">Save Integration</button>
</div>
</div>
</form>
</div>
</div>
<div id="reference-modal" class="modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4>Supported Placeholders</h4>
</div>
<div class="modal-body">
<p>
You can use the below placeholders in the command.
{% site_name %} will replace the placeholders
with the correct values.
</p>
<table id="webhook-variables" class="table modal-body">
<tr>
<th><code>$CODE</code></th>
<td>The UUID code of the check</td>
</tr>
<tr>
<th><code>$NAME</code></th>
<td>Name of the check</td>
</tr>
<tr>
<th><code>$NOW</code></th>
<td>
Current UTC time in ISO8601 format.<br />
Example: "{{ now }}"
</td>
</tr>
<tr>
<th><code>$STATUS</code></th>
<td>Check's current status ("up" or "down")</td>
</tr>
<tr>
<th><code>$TAGS</code></th>
<td>Check's tags, separated by spaces</td>
</tr>
<tr>
<th><code>$TAG1, $TAG2, …</code></th>
<td>Value of the first tag, the second tag, …</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Got It!</button>
</div>
</div>
</div>
</div>
{% endblock %}

+ 1
- 1
templates/integrations/add_webhook.html View File

@ -173,7 +173,7 @@
<tr>
<th><code>$NOW</code></th>
<td>
Current UTC time in ISO8601 format.
Current UTC time in ISO8601 format.<br />
Example: "{{ now }}"
</td>
</tr>


Loading…
Cancel
Save