mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
- Ensure that each diagnostic test category can be identified by easy prefix matching on the test ID. - Give a different unique IDs each different kind of test. More specific tests of a type get a different kind of ID. - Make comparison of diagnostic test results in test cases more comprehensive. - Simplify code that shows the number if issues identified. - In many languages, there is complex logic to write plural forms. Plurals can't be handled by assuming singular = 1 item and plural is > 1. Translation of messages in Notification does not support plurals properly. Avoid this for now by using sometimes incorrect plural form. - For i18n we should avoid joining phrases/words. Words don't always maintain order after translation. - Notify about the total number of issues in diagnostics and not just the most severe category. This is likely to draw more attention and avoid i18n complexity. - Dismiss the diagnostic notification if the latest run succeeded completely. Tests: - Unit tests pass. - Diagnostics for following apps works: networks (drop-in config), apache (daemon, listen address, internal firewall, external firewall), tor (netcat), torproxy (internal only firewall, torproxy url, torproxy using tor), privoxy (privoxy url, package available, package latest), - Untested: Is release file available method in upgrades app. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
138 lines
5.9 KiB
Python
138 lines
5.9 KiB
Python
"""Components for managing configuration files."""
|
|
|
|
import logging
|
|
import pathlib
|
|
|
|
from django.utils.text import format_lazy
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from plinth.privileged import config as privileged
|
|
|
|
from . import app as app_module
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DropinConfigs(app_module.FollowerComponent):
|
|
"""Component to manage config files dropped into /etc.
|
|
|
|
When configuring a daemon, it is often simpler to ship a configuration file
|
|
into the daemon's configuration directory. However, if the user modifies
|
|
this configuration file and freedombox ships a new version of this
|
|
configuration file, then a conflict arises between user's changes and
|
|
changes in the new version of configuration file shipped by freedombox.
|
|
This leads to freedombox package getting marked by unattended-upgrades as
|
|
not automatically upgradable. Dpkg's solution of resolving the conflicts is
|
|
to present the option to the user which is also not acceptable.
|
|
|
|
Further, if a package is purged from the system, sometimes the
|
|
configuration directories are fully removed by deb's scripts. This removes
|
|
files installed by freedombox package. Dpkg treats these files as if user
|
|
has explictly removed them and may lead to a configuration conflict
|
|
described above.
|
|
|
|
The approach freedombox takes to address these issues is using this
|
|
component. Files are shipped into /usr/share/freedombox/etc/ instead of
|
|
/etc/ (keeping the subpath unchanged). Then when an app is enabled, a
|
|
symlink or copy is created from the /usr/share/freedombox/etc/ into /etc/.
|
|
This way, user's understand the configuration file is not meant to be
|
|
edited. Even if they do, next upgrade of freedombox package will silently
|
|
overwrite those changes without causing merge conflicts. Also when purging
|
|
a package removes entire configuration directory, only symlinks/copies are
|
|
lost. They will recreated when the app is reinstalled/enabled.
|
|
"""
|
|
|
|
ROOT = '/' # To make writing tests easier
|
|
DROPIN_CONFIG_ROOT = '/usr/share/freedombox/'
|
|
|
|
def __init__(self, component_id, etc_paths=None, copy_only=False):
|
|
"""Initialize the drop-in configuration component.
|
|
|
|
component_id should be a unique ID across all components of an app and
|
|
across all components.
|
|
|
|
etc_paths is a list of all drop-in configuration files as absolute
|
|
paths in /etc/ which need to managed by this component. For each of the
|
|
paths, it is expected that the actual configuration file exists in
|
|
/usr/share/freedombox/etc/. A link to the file or copy of the file is
|
|
created in /etc/ when app is enabled and the link or file is removed
|
|
when app is disabled. For example, if etc_paths contains
|
|
/etc/apache/conf-enabled/myapp.conf then
|
|
/usr/share/freedombox/etc/apache/conf-enabled/myapp.conf must be
|
|
shipped and former path will be link to or be a copy of the latter when
|
|
app is enabled.
|
|
"""
|
|
super().__init__(component_id)
|
|
self.etc_paths = etc_paths or []
|
|
self.copy_only = copy_only
|
|
|
|
def setup(self, old_version):
|
|
"""Create symlinks or copies of files during app update.
|
|
|
|
During the transition from shipped configs to the symlink/copy
|
|
approach, files in /etc will be removed during .deb upgrade. This
|
|
method ensures that symlinks or copies are properly recreated.
|
|
"""
|
|
if self.app_id and self.app.is_enabled():
|
|
self.enable()
|
|
|
|
def enable(self):
|
|
"""Create a symlink or copy in /etc/ of the configuration file."""
|
|
for path in self.etc_paths:
|
|
etc_path = self._get_etc_path(path)
|
|
target = self._get_target_path(path)
|
|
if etc_path.exists() or etc_path.is_symlink():
|
|
if (not self.copy_only and etc_path.is_symlink()
|
|
and etc_path.readlink() == target):
|
|
continue
|
|
|
|
if (self.copy_only and etc_path.is_file()
|
|
and etc_path.read_text() == target.read_text()):
|
|
continue
|
|
|
|
logger.warning('Removing dropin configuration: %s', path)
|
|
privileged.dropin_unlink(self.app_id, path)
|
|
|
|
privileged.dropin_link(self.app_id, path, self.copy_only)
|
|
|
|
def disable(self):
|
|
"""Remove the links/copies in /etc/ of the configuration files."""
|
|
for path in self.etc_paths:
|
|
privileged.dropin_unlink(self.app_id, path, missing_ok=True)
|
|
|
|
def diagnose(self):
|
|
"""Check all links/copies and return generate diagnostic results."""
|
|
from plinth.modules.diagnostics.check import DiagnosticCheck, Result
|
|
|
|
results = []
|
|
for path in self.etc_paths:
|
|
etc_path = self._get_etc_path(path)
|
|
target = self._get_target_path(path)
|
|
if self.copy_only:
|
|
result = (etc_path.is_file()
|
|
and etc_path.read_text() == target.read_text())
|
|
else:
|
|
result = (etc_path.is_symlink()
|
|
and etc_path.readlink() == target)
|
|
|
|
check_id = f'dropin-config-{etc_path}'
|
|
result_string = Result.PASSED if result else Result.FAILED
|
|
template = _('Static configuration {etc_path} is setup properly')
|
|
description = format_lazy(template, etc_path=str(etc_path))
|
|
results.append(
|
|
DiagnosticCheck(check_id, description, result_string))
|
|
|
|
return results
|
|
|
|
@staticmethod
|
|
def _get_target_path(path):
|
|
"""Return Path object for a target path."""
|
|
target = pathlib.Path(DropinConfigs.ROOT)
|
|
target /= DropinConfigs.DROPIN_CONFIG_ROOT.lstrip('/')
|
|
return target / path.lstrip('/')
|
|
|
|
@staticmethod
|
|
def _get_etc_path(path):
|
|
"""Return Path object for etc path."""
|
|
return pathlib.Path(DropinConfigs.ROOT) / path.lstrip('/')
|