diff --git a/plinth/modules/storage/__init__.py b/plinth/modules/storage/__init__.py index 26b551b11..71cfce75e 100644 --- a/plinth/modules/storage/__init__.py +++ b/plinth/modules/storage/__init__.py @@ -68,6 +68,9 @@ class StorageApp(app_module.App): # Schedule initialization of UDisks2 initialization glib.schedule(3, udisks2.init, repeat=False) + # Check periodically for a read-only root filesystem + glib.schedule(600, _warn_about_read_only_filesystem) + def setup(self, old_version): """Install and configure the app.""" super().setup(old_version) @@ -271,7 +274,7 @@ def get_error_message(error): return message_map.get(short_error, error) -def warn_about_low_disk_space(request): +def warn_about_low_disk_space(_data): """Warn about insufficient space on root partition.""" from plinth.notification import Notification @@ -341,3 +344,55 @@ def report_failing_drive(id, is_failing): 'type': 'dismiss' }], data=data, group='admin') note.dismiss(should_dismiss=not is_failing) + + +def is_partition_read_only(mount_point='/'): + """Return True if the partition is mounted read-only.""" + for partition in psutil.disk_partitions(all=True): + if partition.mountpoint == mount_point: + return 'ro' in partition.opts.split(',') + + raise ValueError('No such mount point') + + +def _warn_about_read_only_filesystem(_data): + """Create a notification if the root filesystem is mounted read-only. + + Remove the notification (if any) if the root filesystem is writable. + """ + from plinth.notification import Notification + + show = is_partition_read_only() + notification_id = 'storage-read-only-root-filesystem' + + if not show: + try: + Notification.get(notification_id).delete() + except KeyError: + pass + + return + + message = gettext_noop( + # xgettext:no-python-format + 'You cannot save configuration changes. Try rebooting the system. If ' + 'the problem persists after a reboot, check the storage device for ' + 'errors.') + title = gettext_noop('Read-only root filesystem') + data = { + 'app_icon': 'fa-hdd-o', + 'app_name': 'translate:' + gettext_noop('Storage'), + } + + # This is a serious issue so the notification can't be dismissed. + actions = [{ + 'type': 'link', + 'class': 'primary', + 'text': gettext_noop('Go to Power'), + 'url': 'power:index' + }] + + Notification.update_or_create(id=notification_id, app_id='storage', + severity='error', title=title, + message=message, actions=actions, data=data, + group='admin') diff --git a/plinth/modules/storage/tests/test_storage.py b/plinth/modules/storage/tests/test_storage.py index cb2672bd4..232c2bdf1 100644 --- a/plinth/modules/storage/tests/test_storage.py +++ b/plinth/modules/storage/tests/test_storage.py @@ -7,9 +7,12 @@ import contextlib import re import subprocess import tempfile +from unittest.mock import patch +import psutil import pytest +from plinth.modules import storage from plinth.modules.storage import privileged pytestmark = pytest.mark.usefixtures('mock_privileged') @@ -343,3 +346,17 @@ def test_validate_directory_writable(path, error): def test_validate_directory_creatable(path, error): """Test that directory creatable validation returns expected output.""" _assert_validate_directory(path, error, check_creatable=True) + + +@patch('psutil.disk_partitions') +def test_is_partition_read_only(disk_partitions): + """Test whether checking for ro partition works.""" + partition = psutil._common.sdiskpart # pylint: disable=protected-access + disk_partitions.return_value = [ + partition('/dev/root', '/', 'btrfs', 'rw,nosuid', 42, 42), + partition('/dev/root', '/foo', 'btrfs', 'rw', 321, 321), + partition('/dev/foo', '/bar', 'extfs', 'ro', 123, 123) + ] + assert not storage.is_partition_read_only('/') + assert not storage.is_partition_read_only('/foo') + assert storage.is_partition_read_only('/bar')