mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-03-15 20:54:53 +00:00
Add "Shell Commands" integration. Fixes #302
This commit is contained in:
parent
f74860bc0c
commit
8d81ea8f9d
22 changed files with 404 additions and 32 deletions
|
@ -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
README.md
12
README.md
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
hc/front/tests/test_add_shell.py
Normal file
53
hc/front/tests/test_add_shell.py
Normal 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="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_instructions_work(self):
|
||||
self.client.login(username="alice@example.org", 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="alice@example.org", 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="bob@example.org", 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="alice@example.org", password="password")
|
||||
self.client.post(self.url, form)
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.cmd_down, "")
|
||||
self.assertEqual(c.cmd_up, "logger up")
|
|
@ -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="alice@example.org", password="password")
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
hc/lib/tests/test_string.py
Normal file
21
hc/lib/tests/test_string.py
Normal 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")
|
|
@ -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 *
|
||||
|
|
|
@ -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;
|
||||
|
|
Binary file not shown.
|
@ -41,4 +41,5 @@
|
|||
<glyph unicode="" 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="" 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="" 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="" 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>
|
Before (image error) Size: 38 KiB After (image error) Size: 40 KiB |
Binary file not shown.
Binary file not shown.
BIN
static/img/integrations/shell.png
Normal file
BIN
static/img/integrations/shell.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.8 KiB |
|
@ -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>
|
||||
|
|
|
@ -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
templates/integrations/add_shell.html
Normal file
121
templates/integrations/add_shell.html
Normal 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 %}
|
|
@ -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…
Reference in a new issue