mirror of
https://github.com/caronc/apprise-api.git
synced 2025-08-16 17:40:56 +02:00
Healthcheck web improvements + PUID & PGID support added to Docker (#198)
This commit is contained in:
@ -37,9 +37,9 @@
|
||||
</a>
|
||||
<h1>{% trans "Apprise API" %}</h1>
|
||||
<ul>
|
||||
<li>APPRISE v{{APPRISE_VERSION}}</li>
|
||||
<li class="theme"><a href="{{ request.path }}?theme={{request.next_theme}}"><i class="material-icons">invert_colors</i></a></li>
|
||||
</ul>
|
||||
<li>APPRISE v{{APPRISE_VERSION}}</li>
|
||||
<li class="theme"><a href="{{ request.path }}?theme={{request.next_theme}}"><i class="material-icons">invert_colors</i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page Layout here -->
|
||||
@ -50,10 +50,10 @@
|
||||
<ul class="collection z-depth-1">
|
||||
<a class="collection-item" href="{% url 'config' DEFAULT_CONFIG_ID %}"><i class="material-icons">settings</i>
|
||||
{% trans "Configuration Manager" %}</a>
|
||||
{% if not CONFIG_LOCK %}
|
||||
{% if not CONFIG_LOCK %}
|
||||
<a class="collection-item" href="{% url 'config' UNIQUE_CONFIG_ID %}"><i class="material-icons">refresh</i>
|
||||
{% trans "New Configuration" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="collection z-depth-1">
|
||||
@ -80,6 +80,28 @@
|
||||
</div>
|
||||
|
||||
<div class="col s9">
|
||||
<div id="health-check" class="section" style="display: none">
|
||||
<h4><i class="material-icons" style="color: orange">warning</i> {% trans "Apprise Health Check Failed" %} <i class="material-icons" style="color: orange">warning</i></h4>
|
||||
{% blocktrans %}The following disk access errors have been detected with your Apprise instance{% endblocktrans %}:
|
||||
<ul>
|
||||
<li class="can_write_config" style="display: none"><strong>
|
||||
<i class="material-icons"
|
||||
style="color: red">cancel</i> {% trans "Configuration Write Failure" %}</strong>
|
||||
<p>{% blocktrans %}Apprise can not write new configuration information to the directory:{% endblocktrans %} <code>{{CONFIG_DIR}}</code>.</p>
|
||||
<p>{% blocktrans %}<em>Note:</em> If this is the expected behavior, you should pre-set the environment variable <code>APPRISE_CONFIG_LOCK=yes</code> and reload your Apprise instance.{% endblocktrans %}</p>
|
||||
</li>
|
||||
|
||||
<li class="can_write_attach" style="display: none"><strong>
|
||||
<i class="material-icons"
|
||||
style="color: red">cancel</i> {% trans "Attachment Temporary Storage Write Failure" %}</strong>
|
||||
<p>{% blocktrans %}Apprise can not circulate attachments (if provided) along to supported endpoints due to not having write access to the directory:{% endblocktrans %} <code>{{ATTACH_DIR}}</code>.</p>
|
||||
<p>{% blocktrans %}<em>Note:</em> If this is the expected behavior, you should pre-set the environment variable <code>APPRISE_ATTACH_SIZE=0</code> and reload your Apprise instance.{% endblocktrans %}</p>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>{% blocktrans %}Under most circumstances, the issue(s) identified here are usually related to permission issues. Make sure you set the correct <code>PUID</code> and <code>GUID</code> to reflect the permissions you wish Apprise to utilize when it is reading and writing its files. In addition to this, you may need to make sure the permissions are set correctly on the directories you mapped them too.{% endblocktrans %}</p>
|
||||
<p>{% blocktrans %}The issue(s) identified here can also be associated with SELinux too. You may wish to rule out SELinux by first temporarily disabling it using the command <code>setenforce 0</code>. You can always re-enstate it with <code>setenforce 1</code>{% endblocktrans %}.</p>
|
||||
</div>
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
@ -91,9 +113,37 @@
|
||||
M.AutoInit();
|
||||
// highlightjs
|
||||
hljs.initHighlightingOnLoad();
|
||||
{% block onload %} {% endblock %}
|
||||
{% block onload %}{% endblock %}
|
||||
// healthcheck
|
||||
health_check()
|
||||
});
|
||||
{% block jsfooter %} {% endblock %}
|
||||
function health_check() {
|
||||
// perform our health check
|
||||
document.querySelector('#health-check').style.display = 'none';
|
||||
document.querySelector('#health-check li.can_write_config').style.display = 'none';
|
||||
document.querySelector('#health-check li.can_write_attach').style.display = 'none';
|
||||
let response = fetch('{% url "health" %}', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json;charset=utf-8'
|
||||
},
|
||||
|
||||
}).then(function(response) {
|
||||
if(response.status != 200)
|
||||
{
|
||||
response.json().then(function(content) {
|
||||
if (content['status']['can_write_config'] === false && content['config_lock'] === false) {
|
||||
document.querySelector('#health-check li.can_write_config').style.display = '';
|
||||
}
|
||||
if (content['status']['can_write_attach'] === false && content['attach_lock'] === false) {
|
||||
document.querySelector('#health-check li.can_write_attach').style.display = '';
|
||||
}
|
||||
document.querySelector('#health-check').style.display = '';
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
@ -188,7 +188,7 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block jsfooter %}
|
||||
|
||||
{{ block.super }}
|
||||
{% if STATEFUL_MODE != 'disabled' %}
|
||||
function update_count() {
|
||||
const p_count = document.querySelectorAll('#url-list li.card-panel.selected').length;
|
||||
@ -695,8 +695,8 @@ function notify_init() {
|
||||
{% endblock %}
|
||||
|
||||
{% block onload %}
|
||||
{% if STATEFUL_MODE != 'disabled' %}
|
||||
{{ block.super }}
|
||||
{% if STATEFUL_MODE != 'disabled' %}
|
||||
document.querySelector('label [for="id_tag"]')
|
||||
{
|
||||
// create a new div with the class 'chips' assigned to it
|
||||
|
@ -1,6 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block body %}
|
||||
<h4>{% trans "The Apprise API" %}</h4>
|
||||
<p>
|
||||
|
@ -64,6 +64,7 @@ class HealthCheckTests(SimpleTestCase):
|
||||
content = loads(response.content)
|
||||
assert content == {
|
||||
'config_lock': False,
|
||||
'attach_lock': False,
|
||||
'status': {
|
||||
'can_write_config': True,
|
||||
'can_write_attach': True,
|
||||
@ -87,6 +88,7 @@ class HealthCheckTests(SimpleTestCase):
|
||||
content = loads(response.content)
|
||||
assert content == {
|
||||
'config_lock': True,
|
||||
'attach_lock': False,
|
||||
'status': {
|
||||
'can_write_config': False,
|
||||
'can_write_attach': True,
|
||||
@ -109,6 +111,7 @@ class HealthCheckTests(SimpleTestCase):
|
||||
content = loads(response.content)
|
||||
assert content == {
|
||||
'config_lock': False,
|
||||
'attach_lock': False,
|
||||
'status': {
|
||||
'can_write_config': False,
|
||||
'can_write_attach': True,
|
||||
@ -131,6 +134,7 @@ class HealthCheckTests(SimpleTestCase):
|
||||
content = loads(response.content)
|
||||
assert content == {
|
||||
'config_lock': False,
|
||||
'attach_lock': True,
|
||||
'status': {
|
||||
'can_write_config': True,
|
||||
'can_write_attach': False,
|
||||
@ -153,9 +157,10 @@ class HealthCheckTests(SimpleTestCase):
|
||||
content = loads(response.content)
|
||||
assert content == {
|
||||
'config_lock': False,
|
||||
'attach_lock': False,
|
||||
'status': {
|
||||
'can_write_config': True,
|
||||
'can_write_attach': False,
|
||||
'can_write_attach': True,
|
||||
'details': ['OK']
|
||||
}
|
||||
}
|
||||
|
@ -181,6 +181,7 @@ class NotifyTests(SimpleTestCase):
|
||||
# Reset our mock object
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# A setting of zero means unlimited attachments are allowed
|
||||
with override_settings(APPRISE_MAX_ATTACHMENTS=0):
|
||||
|
||||
# Preare our form data
|
||||
@ -196,6 +197,67 @@ class NotifyTests(SimpleTestCase):
|
||||
form = NotifyForm(form_data, attach_data)
|
||||
assert form.is_valid()
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form.cleaned_data)
|
||||
|
||||
# We're good!
|
||||
assert response.status_code == 200
|
||||
assert mock_notify.call_count == 1
|
||||
|
||||
# Reset our mock object
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# Only allow 1 attachment, but we'll attempt to send more...
|
||||
with override_settings(APPRISE_MAX_ATTACHMENTS=1):
|
||||
|
||||
# Preare our form data
|
||||
form_data = {
|
||||
'body': 'test notifiction',
|
||||
}
|
||||
|
||||
# At a minimum, just a body is required
|
||||
form = NotifyForm(form_data)
|
||||
|
||||
assert form.is_valid()
|
||||
# Required to prevent None from being passed into self.client.post()
|
||||
del form.cleaned_data['attachment']
|
||||
|
||||
data = {
|
||||
**form.cleaned_data,
|
||||
'file1': SimpleUploadedFile(
|
||||
"attach1.txt", b"content here", content_type="text/plain"),
|
||||
'file2': SimpleUploadedFile(
|
||||
"attach2.txt", b"more content here", content_type="text/plain"),
|
||||
}
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), data, format='multipart')
|
||||
|
||||
# Too many attachments
|
||||
assert response.status_code == 400
|
||||
assert mock_notify.call_count == 0
|
||||
|
||||
# Reset our mock object
|
||||
mock_notify.reset_mock()
|
||||
|
||||
# A setting of zero means unlimited attachments are allowed
|
||||
with override_settings(APPRISE_ATTACH_SIZE=0):
|
||||
|
||||
# Preare our form data
|
||||
form_data = {
|
||||
'body': 'test notifiction',
|
||||
}
|
||||
attach_data = {
|
||||
'attachment': SimpleUploadedFile(
|
||||
"attach.txt", b"content here", content_type="text/plain")
|
||||
}
|
||||
|
||||
# At a minimum, just a body is required
|
||||
form = NotifyForm(form_data, attach_data)
|
||||
assert form.is_valid()
|
||||
|
||||
# Send our notification
|
||||
response = self.client.post(
|
||||
'/notify/{}'.format(key), form.cleaned_data)
|
||||
|
@ -207,6 +207,22 @@ class HTTPAttachment(A_MGR['http']):
|
||||
pass
|
||||
|
||||
|
||||
def touch(fname, mode=0o666, dir_fd=None, **kwargs):
|
||||
"""
|
||||
Acts like a Linux touch and updates a file with a current timestamp
|
||||
"""
|
||||
flags = os.O_CREAT | os.O_APPEND
|
||||
try:
|
||||
with os.fdopen(os.open(fname, flags=flags, mode=mode, dir_fd=dir_fd)) as f:
|
||||
os.utime(f.fileno() if os.utime in os.supports_fd else fname,
|
||||
dir_fd=None if os.supports_fd else dir_fd, **kwargs)
|
||||
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def parse_attachments(attachment_payload, files_request):
|
||||
"""
|
||||
Takes the payload provided in a `/notify` call and extracts the
|
||||
@ -230,15 +246,12 @@ def parse_attachments(attachment_payload, files_request):
|
||||
count += 1
|
||||
|
||||
if settings.APPRISE_ATTACH_SIZE <= 0:
|
||||
raise ValueError("The attachment size is restricted to 0MB")
|
||||
raise ValueError("Attachment support has been disabled")
|
||||
|
||||
if settings.APPRISE_MAX_ATTACHMENTS <= 0 or \
|
||||
(settings.APPRISE_MAX_ATTACHMENTS > 0 and
|
||||
count > settings.APPRISE_MAX_ATTACHMENTS):
|
||||
if settings.APPRISE_MAX_ATTACHMENTS > 0 and count > settings.APPRISE_MAX_ATTACHMENTS:
|
||||
raise ValueError(
|
||||
"There is a maximum of %d attachments" %
|
||||
settings.APPRISE_MAX_ATTACHMENTS
|
||||
if settings.APPRISE_MAX_ATTACHMENTS > 0 else 0)
|
||||
settings.APPRISE_MAX_ATTACHMENTS)
|
||||
|
||||
if isinstance(attachment_payload, (tuple, list, set)):
|
||||
for no, entry in enumerate(attachment_payload, start=1):
|
||||
@ -772,67 +785,70 @@ def healthcheck(lazy=True):
|
||||
|
||||
if not (settings.APPRISE_STATEFUL_MODE == AppriseStoreMode.DISABLED or settings.APPRISE_CONFIG_LOCK):
|
||||
# Update our Configuration Check Block
|
||||
path = os.path.join(ConfigCache.root, '.tmp_healthcheck')
|
||||
path = os.path.join(ConfigCache.root, '.tmp_hc')
|
||||
if lazy:
|
||||
try:
|
||||
modify_date = datetime.fromtimestamp(os.path.getmtime(path))
|
||||
delta = (datetime.now() - modify_date).total_seconds()
|
||||
if delta <= 7200.00: # 2hrs
|
||||
if delta <= 30.00: # 30s
|
||||
response['can_write_config'] = True
|
||||
|
||||
except FileNotFoundError:
|
||||
# No worries... continue with below testing
|
||||
pass
|
||||
|
||||
if not response['can_write_config']:
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
# Write a small file
|
||||
with tempfile.TemporaryFile(mode='w+b', dir=path) as fp:
|
||||
# Test writing 1 block
|
||||
fp.write(b'.')
|
||||
# Read it back
|
||||
fp.seek(0)
|
||||
fp.read(1) == b'.'
|
||||
# Toggle our status
|
||||
response['can_write_config'] = True
|
||||
|
||||
except OSError:
|
||||
# Permission Issue or something else likely
|
||||
# We can take an early exit
|
||||
response['details'].append('CONFIG_PERMISSION_ISSUE')
|
||||
|
||||
if settings.APPRISE_MAX_ATTACHMENTS > 0 and settings.APPRISE_ATTACH_SIZE > 0:
|
||||
if not (response['can_write_config'] or 'CONFIG_PERMISSION_ISSUE' in response['details']):
|
||||
try:
|
||||
os.makedirs(ConfigCache.root, exist_ok=True)
|
||||
if touch(path):
|
||||
# Toggle our status
|
||||
response['can_write_config'] = True
|
||||
|
||||
else:
|
||||
# We can take an early exit as there is already a permission issue detected
|
||||
response['details'].append('CONFIG_PERMISSION_ISSUE')
|
||||
|
||||
except OSError:
|
||||
# We can take an early exit as there is already a permission issue detected
|
||||
response['details'].append('CONFIG_PERMISSION_ISSUE')
|
||||
|
||||
if settings.APPRISE_ATTACH_SIZE > 0:
|
||||
# Test our ability to access write attachments
|
||||
|
||||
# Update our Configuration Check Block
|
||||
path = os.path.join(settings.APPRISE_ATTACH_DIR, '.tmp_healthcheck')
|
||||
path = os.path.join(settings.APPRISE_ATTACH_DIR, '.tmp_hc')
|
||||
if lazy:
|
||||
try:
|
||||
modify_date = datetime.fromtimestamp(os.path.getmtime(path))
|
||||
delta = (datetime.now() - modify_date).total_seconds()
|
||||
if delta <= 7200.00: # 2hrs
|
||||
if delta <= 30.00: # 30s
|
||||
response['can_write_attach'] = True
|
||||
|
||||
except FileNotFoundError:
|
||||
# No worries... continue with below testing
|
||||
pass
|
||||
|
||||
if not response['can_write_attach']:
|
||||
except OSError:
|
||||
# We can take an early exit as there is already a permission issue detected
|
||||
response['details'].append('ATTACH_PERMISSION_ISSUE')
|
||||
|
||||
if not (response['can_write_attach'] or 'ATTACH_PERMISSION_ISSUE' in response['details']):
|
||||
# No lazy mode set or content require a refresh
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
# Write a small file
|
||||
with tempfile.TemporaryFile(mode='w+b', dir=path) as fp:
|
||||
# Test writing 1 block
|
||||
fp.write(b'.')
|
||||
# Read it back
|
||||
fp.seek(0)
|
||||
fp.read(1) == b'.'
|
||||
os.makedirs(settings.APPRISE_ATTACH_DIR, exist_ok=True)
|
||||
if touch(path):
|
||||
# Toggle our status
|
||||
response['can_write_attach'] = True
|
||||
|
||||
else:
|
||||
# We can take an early exit as there is already a permission issue detected
|
||||
response['details'].append('ATTACH_PERMISSION_ISSUE')
|
||||
|
||||
except OSError:
|
||||
# We can take an early exit
|
||||
response['details'].append('ATTACH_PERMISSION_ISSUE')
|
||||
|
@ -161,8 +161,8 @@ class HealthCheckView(View):
|
||||
and ACCEPT_ALL.match(request.headers.get('accept', '')) else \
|
||||
MIME_IS_JSON.match(request.headers.get('accept', '')) is not None
|
||||
|
||||
# Run our healthcheck
|
||||
response = healthcheck()
|
||||
# Run our healthcheck; allow ?force which will cause the check to run each time
|
||||
response = healthcheck(lazy='force' not in request.GET)
|
||||
|
||||
# Prepare our response
|
||||
status = ResponseCode.okay if 'OK' in response['details'] else ResponseCode.expectation_failed
|
||||
@ -172,6 +172,7 @@ class HealthCheckView(View):
|
||||
return HttpResponse(response, status=status, content_type='text/plain') \
|
||||
if not json_response else JsonResponse({
|
||||
'config_lock': settings.APPRISE_CONFIG_LOCK,
|
||||
'attach_lock': settings.APPRISE_ATTACH_SIZE <= 0,
|
||||
'status': response,
|
||||
}, encoder=JSONEncoder, safe=False, status=status)
|
||||
|
||||
|
Reference in New Issue
Block a user