diagnostics: Show low system memory notifications

Containers specific case: if total memory taken from cgroups is lower
than system memory taken from psutil, calculate memory usage based on
information from cgroups. The formula idea is taken from
https://github.com/moby/moby/issues/40727#issuecomment-604155288

Closes #1780

Tests performed:
- In a non-container environment, filled the memory 90%
```
stress-ng --vm-bytes $(awk '/MemAvailable/{printf "%d\n", $2 * 0.9;}' \
< /proc/meminfo)k --vm-keep -m 1
```
and ensured that correct notification is shown.
- In a container, if no memory limitations are set, notifications are
based on host memory usage
- In a container, if memory limits are set
```
systemctl set-property systemd-nspawn@fbx-testing.service MemoryMax=200M
```
ensured that the notification is shown and is calculated based on
cgroups.

Signed-off-by: Veiko Aasa <veiko17@disroot.org>
[sunil: Fix i18n for notification message]
[sunil: Drop unnecessary type conversion]
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Veiko Aasa 2020-10-08 11:19:45 +03:00 committed by Sunil Mohan Adapa
parent 0960a13d49
commit d93f2f634d
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2

View File

@ -6,13 +6,15 @@ FreedomBox app for system diagnostics.
import collections
import importlib
import logging
import pathlib
import threading
import psutil
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_noop
from plinth import app as app_module
from plinth import daemon, menu
from plinth import cfg, daemon, glib, menu
from plinth.modules.apache.components import diagnose_url_on_all
from .manifest import backup # noqa, pylint: disable=unused-import
@ -55,6 +57,10 @@ class DiagnosticsApp(app_module.App):
'diagnostics:index', parent_url_name='system')
self.add(menu_item)
# Check periodically for low RAM space
interval = 180 if cfg.develop else 3600
glib.schedule(interval, _warn_about_low_ram_space)
def diagnose(self):
"""Run diagnostics and return the results."""
results = super().diagnose()
@ -126,3 +132,101 @@ def run_on_all_enabled_modules():
current_results['results'][app_id] = app.diagnose()
current_results['progress_percentage'] = \
int((current_index + 1) * 100 / len(apps))
def _get_memory_info_from_cgroups():
"""Return information about RAM usage from cgroups."""
cgroups_memory_path = pathlib.Path('/sys/fs/cgroup/memory')
memory_limit_file = cgroups_memory_path / 'memory.limit_in_bytes'
memory_usage_file = cgroups_memory_path / 'memory.usage_in_bytes'
memory_stat_file = cgroups_memory_path / 'memory.stat'
try:
memory_total = int(memory_limit_file.read_text())
memory_usage = int(memory_usage_file.read_text())
memory_stat_lines = memory_stat_file.read_text().split('\n')
except OSError:
return {}
memory_inactive = int([
line.rsplit(maxsplit=1)[1] for line in memory_stat_lines
if line.startswith('total_inactive_file')
][0])
memory_used = memory_usage - memory_inactive
return {
'total_bytes': memory_total,
'percent_used': memory_used * 100 / memory_total,
'free_bytes': memory_total - memory_used
}
def _get_memory_info():
"""Return RAM usage information."""
memory_info = psutil.virtual_memory()
cgroups_memory_info = _get_memory_info_from_cgroups()
if cgroups_memory_info and cgroups_memory_info[
'total_bytes'] < memory_info.total:
return cgroups_memory_info
return {
'total_bytes': memory_info.total,
'percent_used': memory_info.percent,
'free_bytes': memory_info.available
}
def _warn_about_low_ram_space(request):
"""Warn about insufficient RAM space."""
from plinth.notification import Notification
memory_info = _get_memory_info()
if memory_info['free_bytes'] < 1024**3:
# Translators: This is the unit of computer storage Mebibyte similar to
# Megabyte.
memory_available_unit = ugettext_noop('MiB')
memory_available = memory_info['free_bytes'] / 1024**2
else:
# Translators: This is the unit of computer storage Gibibyte similar to
# Gigabyte.
memory_available_unit = ugettext_noop('GiB')
memory_available = memory_info['free_bytes'] / 1024**3
show = False
if memory_info['percent_used'] > 90:
severity = 'error'
advice_message = ugettext_noop(
'You should disable some apps to reduce memory usage.')
show = True
elif memory_info['percent_used'] > 75:
severity = 'warning'
advice_message = ugettext_noop(
'You should not install any new apps on this system.')
show = True
if not show:
try:
Notification.get('diagnostics-low-ram-space').delete()
except KeyError:
pass
return
message = ugettext_noop(
# xgettext:no-python-format
'System is low on memory: {percent_used}% used, {memory_available} '
'{memory_available_unit} free. {advice_message}')
title = ugettext_noop('Low Memory')
data = {
'app_icon': 'fa-heartbeat',
'app_name': ugettext_noop('Diagnostics'),
'percent_used': f'{memory_info["percent_used"]:.1f}',
'memory_available': f'{memory_available:.1f}',
'memory_available_unit': 'translate:' + memory_available_unit,
'advice_message': 'translate:' + advice_message
}
actions = [{'type': 'dismiss'}]
Notification.update_or_create(id='diagnostics-low-ram-space',
app_id='diagnostics', severity=severity,
title=title, message=message,
actions=actions, data=data, group='admin')