diff --git a/.gitignore b/.gitignore index 812d5c4ad..9440cf52b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ doc/manual/*/*.pdf doc/manual/*/*.html doc/manual/*/*.xml -doc/plinth.1 +doc/*.1 doc/dev/_build \#* .#* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a5e07e227..832400875 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -56,7 +56,7 @@ build: build-backports: extends: .build-package variables: - RELEASE: bookworm-backports + RELEASE: trixie-backports build i386: extends: .build-package-i386 diff --git a/Makefile b/Makefile index fecc2c3ba..db908fbf1 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,7 @@ DIRECTORIES_TO_CREATE := \ STATIC_FILES_DIRECTORY := $(DESTDIR)/usr/share/plinth/static BIN_DIR := $(DESTDIR)/usr/bin +LIB_DIR := $(DESTDIR)/usr/lib FIND_ARGS := \ -not -iname "*.log" \ @@ -102,7 +103,7 @@ install: rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/COPYING.md && \ rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/direct_url.json && \ $(INSTALL) -D -t $(BIN_DIR) bin/plinth - $(INSTALL) -D -t $(BIN_DIR) bin/freedombox-privileged + $(INSTALL) -D -t $(LIB_DIR)/freedombox bin/freedombox-privileged $(INSTALL) -D -t $(BIN_DIR) bin/freedombox-cmd # Static web server files diff --git a/data/usr/lib/systemd/system/freedombox-privileged.service b/data/usr/lib/systemd/system/freedombox-privileged.service index 9cce91e34..68aaebda8 100644 --- a/data/usr/lib/systemd/system/freedombox-privileged.service +++ b/data/usr/lib/systemd/system/freedombox-privileged.service @@ -5,10 +5,12 @@ Description=FreedomBox Privileged Service Documentation=https://wiki.debian.org/FreedomBox/ # Don't hit the start rate limiting. StartLimitIntervalSec=0 +# Stop/restart along with .socket unit (invoked from dpkg scripts). +PartOf=freedombox-privileged.socket [Service] Type=notify -ExecStart=/usr/bin/freedombox-privileged +ExecStart=/usr/lib/freedombox/freedombox-privileged TimeoutSec=300s User=root Group=root diff --git a/debian/changelog b/debian/changelog index 14654e89c..fbf86ff0a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,72 @@ +freedombox (25.13) unstable; urgency=medium + + [ Burak Yavuz ] + * Translated using Weblate (Turkish) + + [ 大王叫我来巡山 ] + * Translated using Weblate (Chinese (Simplified Han script)) + + [ Максим Горпиніч ] + * Translated using Weblate (Ukrainian) + + [ 109247019824 ] + * Translated using Weblate (Bulgarian) + + [ Jiří Podhorecký ] + * Translated using Weblate (Czech) + + [ Sunil Mohan Adapa ] + * glib: Add schedule parameter for setting interval in develop mode + * diagnostics: In development mode, run diagnostics more rarely + * backups: Ignore a typing error with mypy + * Makefile: Move privileged daemon to /usr/lib/freedombox + * action_utils: Handle capture_output argument in run wrapper + * privileged_daemon: Fix showing errors for freedombox-cmd command + * doc: Add manual page for freedombox-cmd + * storage: Fix disk usage checking with disconnected SSH mounts + * actions: Work with older privileged daemon + * privileged_daemon: Implement handling termination signal + * d/rules: Drop a workaround for dh_installsytemd needed for /usr/lib + * actions: Log method arguments in privileged daemon + * backups: Fix robust handling of known errors + * config: Set home page to FreedomBox for invalid values + * actions: Log full exception from privileged daemon on error + * setup: Log full exception traceback when setup fails + * actions: Fix lifetime of thread local storage + * actions_utils: Fix issue with collecting stdout/stderr + * *: Use action_utils.run instead of subprocess.run + * *: Use action_utils.run instead of subprocess.check_call + * *: Use action_utils.run instead of subprocess.call + * *: Use action_utils.run instead of subprocess.check_output + * *: Collect output for all privileged sub-processes + * ci: Switch backports test to trixie-backports + * actions: Raise an exception if privileged server response is empty + * debian: Stop privileged service during upgrade or removal + * backups: Don't show enable/disable button as app can't be disabled + * locale: Fix a string formatting issue in Italian translation + * daemon: When ensuring running state handle not-installed state + * miniflux: Fix DB connection issues during install/uninstall + * zoph: Additional dbconfig configuration keys + + [ Dietmar ] + * Translated using Weblate (German) + * Translated using Weblate (Italian) + + [ Roman Akimov ] + * Translated using Weblate (Russian) + + [ Veiko Aasa ] + * gitweb: Use Git credential helper when cloning URLs with credentials + + [ Besnik Bleta ] + * Translated using Weblate (Albanian) + + [ James Valleroy ] + * locale: Update translation strings + * doc: Fetch latest manual + + -- James Valleroy Mon, 06 Oct 2025 20:30:14 -0400 + freedombox (25.12~bpo13+1) trixie-backports; urgency=medium * Rebuild for trixie-backports. diff --git a/debian/freedombox.lintian-overrides b/debian/freedombox.lintian-overrides index 1bda5b605..b3a57a7da 100644 --- a/debian/freedombox.lintian-overrides +++ b/debian/freedombox.lintian-overrides @@ -19,3 +19,8 @@ freedombox binary: web-application-works-only-with-apache # Not documentation freedombox: package-contains-documentation-outside-usr-share-doc [usr/share/plinth/static/jslicense.html] freedombox: package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/plinth-*.dist-info/top_level.txt] + +# This executable is meant to executed from systemd service file and is not +# meant for user. However, don't install to /usr/libexec and follow systemd +# convention instead. +freedombox: executable-in-usr-lib [usr/lib/freedombox/freedombox-privileged] diff --git a/debian/freedombox.manpages b/debian/freedombox.manpages index 1b517d232..f96cd2ce6 100644 --- a/debian/freedombox.manpages +++ b/debian/freedombox.manpages @@ -1 +1,2 @@ +./doc/freedombox-cmd.1 ./doc/plinth.1 diff --git a/debian/rules b/debian/rules index 50a2cc0c1..de56899b6 100755 --- a/debian/rules +++ b/debian/rules @@ -29,10 +29,6 @@ ifneq ($(FBX_VERSION),$(DEB_VERSION)) endif override_dh_installsystemd: - # Do not enable or start any service other than FreedomBox service. Use - # of --tmpdir is a hack to workaround an issue with dh_installsystemd - # (as of debhelper 13.5.2) that still has hardcoded search path of - # /lib/systemd/system for searching systemd services. See #987989 and - # reversion of its changes. - dh_installsystemd --tmpdir=debian/tmp/usr --package=freedombox \ - plinth.service freedombox-privileged.socket + # Do not enable or start any service other than FreedomBox service. + dh_installsystemd --package=freedombox plinth.service \ + freedombox-privileged.socket diff --git a/doc/Makefile b/doc/Makefile index 95489391b..37ccf8465 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -13,7 +13,7 @@ SCRIPTS_DIR=scripts manual-pdfs=$(foreach lang,$(MANUAL_LANGUAGES),manual/$(lang)/freedombox-manual.pdf) manual-xmls=$(patsubst %.pdf,%.xml,$(manual-pdfs)) -OUTPUTS=$(manual-pdfs) plinth.1 +OUTPUTS=$(manual-pdfs) plinth.1 freedombox-cmd.1 INSTALL_OPTS=-D --mode=644 diff --git a/doc/freedombox-cmd.xml b/doc/freedombox-cmd.xml new file mode 100644 index 000000000..64eb6d3f4 --- /dev/null +++ b/doc/freedombox-cmd.xml @@ -0,0 +1,160 @@ + + + + + + freedombox-cmd + 1 + FreedomBox Command Line Utility + + + + + freedombox-cmd + + command line utility to perform FreedomBox operations + + + + + + freedombox-cmd + + module + action + + + + + + Description + + FreedomBox is a community project to develop, design and promote + personal servers running free software for private, personal + communications. It is a networking appliance designed to allow + interfacing with the rest of the Internet under conditions of + protected privacy and data security. It hosts applications such + as blog, wiki, website, social network, email, web proxy and a + Tor relay on a device that can replace a wireless router so that + data stays with the users. + + + freedombox-cmd is a command line interface to some of the operations + performed by FreedomBox. It is typically not needed by the end users who + use FreedomBox's web interface. The command may be used in some cases + while debugging problems, especially where the web interface is not + accessible or when a piece of functionality that is not provided in the + web interface needs to be triggered. + + + The command is simply a client to the FreedomBox's privileged daemon and + relays user's request to it. It waits for the request to complete and + prints the output of the operation or an error message collected form the + daemon. The daemon only allows connections from an pre-allowed list of + user accounts. So, be sure to run the command as 'root' superuser. + + + + + Options + + + + + + Name of the module from which to execute an action. + + + + + + + + Name of the action to execute. It should found in the provided + module. + + + + + + + + Don't try to read the arguments to the command on the standard + input. Instead, assume that the operation does not have any + arguments and execute the method without arguments. + + + + + + + + Show brief help about arguments allowed for this command. + + + + + + + + Examples + + + Re-run FreedomBox network setup + $ sudo freedombox-cmd networks setup --no-args + + When FreedomBox starts for the first time, it will setup Network Manager + connections suitable for the hardware found. If you wish to re-create + these connections at a later time, you can re-run setup for the Networks + app using the web interface or run this command on a terminal. + + + + + Delete a user account from LDAP database + $ echo '{"args": ["USERNAME", "AUTH_USER", "AUTH_PASSWORD"], "kwargs": {}}' | sudo freedombox-cmd users remove_user + + USERNAME is the name of the user account that must be removed. AUTH_USER + is name of the user account that is authorizing this operation. + AUTH_PASSWORD is the password for user account that is authorizing this + operation. This operation may be needed if FreedomBox's sqlite3 database + is wiped, removing the user accounts in the database but the + corresponding entries from LDAP database are not removed. A new user + with that name can't be created until the LDAP account is also removed. + + + + + Set the logging mode to persistent + $ echo '{"args": ["persistent"], "kwargs": {}}' | sudo freedombox-cmd config set_logging_mode + + By default, FreedomBox sets up systemd-journald to 'volatile' logging. + This means that logs will not be stored on the disk and will be lost + after a reboot. If you are tackling a problem and wish to store the logs + persistently, you can change the setting in the web interface or run + this command. + + + + + + Bugs + + See FreedomBox + issue tracker for a full list of known issues and TODO items. + + + + + Author + + + FreedomBox Developers + Original author + + + + diff --git a/doc/manual/en/ReleaseNotes.raw.wiki b/doc/manual/en/ReleaseNotes.raw.wiki index 6a75d982b..ebacfd9a8 100644 --- a/doc/manual/en/ReleaseNotes.raw.wiki +++ b/doc/manual/en/ReleaseNotes.raw.wiki @@ -8,6 +8,47 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f The following are the release notes for each !FreedomBox version. +== FreedomBox 25.13 (2025-10-06) == + +=== Highlights === + + * backups: Fix robust handling of known errors + * debian: Stop privileged service during upgrade or removal + * miniflux: Fix DB connection issues during install/uninstall + +=== Other Changes === + + * *: Collect output for all privileged sub-processes + * *: Use action_utils.run instead of subprocess.call + * *: Use action_utils.run instead of subprocess.check_call + * *: Use action_utils.run instead of subprocess.check_output + * *: Use action_utils.run instead of subprocess.run + * action_utils: Handle capture_output argument in run wrapper + * actions: Fix lifetime of thread local storage + * actions: Log full exception from privileged daemon on error + * actions: Log method arguments in privileged daemon + * actions: Raise an exception if privileged server response is empty + * actions: Work with older privileged daemon + * actions_utils: Fix issue with collecting stdout/stderr + * backups: Don't show enable/disable button as app can't be disabled + * backups: Ignore a typing error with mypy + * ci: Switch backports test to trixie-backports + * config: Set home page to !FreedomBox for invalid values + * d/rules: Drop a workaround for dh_installsytemd needed for /usr/lib + * daemon: When ensuring running state handle not-installed state + * diagnostics: In development mode, run diagnostics more rarely + * doc: Add manual page for freedombox-cmd + * gitweb: Use Git credential helper when cloning URLs with credentials + * glib: Add schedule parameter for setting interval in develop mode + * locale: Fix a string formatting issue in Italian translation + * locale: Update translations for Albanian, Bulgarian, Chinese (Simplified Han script), Czech, German, Italian, Russian, Turkish, Ukrainian + * Makefile: Move privileged daemon to /usr/lib/freedombox + * privileged_daemon: Fix showing errors for freedombox-cmd command + * privileged_daemon: Implement handling termination signal + * setup: Log full exception traceback when setup fails + * storage: Fix disk usage checking with disconnected SSH mounts + * zoph: Additional dbconfig configuration keys + == FreedomBox 25.12 (2025-09-22) == === Highlights === diff --git a/doc/manual/es/ReleaseNotes.raw.wiki b/doc/manual/es/ReleaseNotes.raw.wiki index 6a75d982b..ebacfd9a8 100644 --- a/doc/manual/es/ReleaseNotes.raw.wiki +++ b/doc/manual/es/ReleaseNotes.raw.wiki @@ -8,6 +8,47 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f The following are the release notes for each !FreedomBox version. +== FreedomBox 25.13 (2025-10-06) == + +=== Highlights === + + * backups: Fix robust handling of known errors + * debian: Stop privileged service during upgrade or removal + * miniflux: Fix DB connection issues during install/uninstall + +=== Other Changes === + + * *: Collect output for all privileged sub-processes + * *: Use action_utils.run instead of subprocess.call + * *: Use action_utils.run instead of subprocess.check_call + * *: Use action_utils.run instead of subprocess.check_output + * *: Use action_utils.run instead of subprocess.run + * action_utils: Handle capture_output argument in run wrapper + * actions: Fix lifetime of thread local storage + * actions: Log full exception from privileged daemon on error + * actions: Log method arguments in privileged daemon + * actions: Raise an exception if privileged server response is empty + * actions: Work with older privileged daemon + * actions_utils: Fix issue with collecting stdout/stderr + * backups: Don't show enable/disable button as app can't be disabled + * backups: Ignore a typing error with mypy + * ci: Switch backports test to trixie-backports + * config: Set home page to !FreedomBox for invalid values + * d/rules: Drop a workaround for dh_installsytemd needed for /usr/lib + * daemon: When ensuring running state handle not-installed state + * diagnostics: In development mode, run diagnostics more rarely + * doc: Add manual page for freedombox-cmd + * gitweb: Use Git credential helper when cloning URLs with credentials + * glib: Add schedule parameter for setting interval in develop mode + * locale: Fix a string formatting issue in Italian translation + * locale: Update translations for Albanian, Bulgarian, Chinese (Simplified Han script), Czech, German, Italian, Russian, Turkish, Ukrainian + * Makefile: Move privileged daemon to /usr/lib/freedombox + * privileged_daemon: Fix showing errors for freedombox-cmd command + * privileged_daemon: Implement handling termination signal + * setup: Log full exception traceback when setup fails + * storage: Fix disk usage checking with disconnected SSH mounts + * zoph: Additional dbconfig configuration keys + == FreedomBox 25.12 (2025-09-22) == === Highlights === diff --git a/plinth/__init__.py b/plinth/__init__.py index 665ddf78b..00a1c2fc9 100644 --- a/plinth/__init__.py +++ b/plinth/__init__.py @@ -3,4 +3,4 @@ Package init file. """ -__version__ = '25.12' +__version__ = '25.13' diff --git a/plinth/action_utils.py b/plinth/action_utils.py index e7db636a1..b6805d6fa 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -33,20 +33,18 @@ def is_systemd_running(): def systemd_get_default() -> str: """Return the default target that systemd will boot into.""" - process = subprocess.run(['systemctl', 'get-default'], - stdout=subprocess.PIPE, check=True) + process = run(['systemctl', 'get-default'], check=True) return process.stdout.decode().strip() def systemd_set_default(target: str): """Set the default target that systemd will boot into.""" - subprocess.run(['systemctl', 'set-default', target], check=True) + run(['systemctl', 'set-default', target], check=True) def service_daemon_reload(): """Reload systemd to ensure that newer unit files are read.""" - subprocess.run(['systemctl', 'daemon-reload'], check=True, - stdout=subprocess.DEVNULL) + run(['systemctl', 'daemon-reload'], check=True) def service_is_running(servicename): @@ -55,8 +53,7 @@ def service_is_running(servicename): Does not need to run as root. """ try: - subprocess.run(['systemctl', 'status', servicename], check=True, - stdout=subprocess.DEVNULL) + run(['systemctl', 'status', servicename], check=True) return True except subprocess.CalledProcessError: # If a service is not running we get a status code != 0 and @@ -102,9 +99,7 @@ def service_is_enabled(service_name, strict_check=False): """ try: - process = subprocess.run(['systemctl', 'is-enabled', service_name], - check=True, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL) + process = run(['systemctl', 'is-enabled', service_name], check=True) if not strict_check: return True @@ -115,13 +110,13 @@ def service_is_enabled(service_name, strict_check=False): def service_enable(service_name: str, check: bool = False): """Enable and start a service in systemd.""" - subprocess.run(['systemctl', 'enable', service_name], check=check) + run(['systemctl', 'enable', service_name], check=check) service_start(service_name, check=check) def service_disable(service_name: str, check: bool = False): """Disable and stop service in systemd.""" - subprocess.run(['systemctl', 'disable', service_name], check=check) + run(['systemctl', 'disable', service_name], check=check) try: service_stop(service_name, check=check) except subprocess.CalledProcessError: @@ -130,12 +125,12 @@ def service_disable(service_name: str, check: bool = False): def service_mask(service_name: str, check: bool = False): """Mask a service""" - subprocess.run(['systemctl', 'mask', service_name], check=check) + run(['systemctl', 'mask', service_name], check=check) def service_unmask(service_name: str, check: bool = False): """Unmask a service""" - subprocess.run(['systemctl', 'unmask', service_name], check=check) + run(['systemctl', 'unmask', service_name], check=check) def service_start(service_name: str, check: bool = False): @@ -181,14 +176,14 @@ def service_get_logs(service_name: str) -> str: command = [ 'journalctl', '--no-pager', '--lines=200', '--unit', service_name ] - process = subprocess.run(command, check=False, stdout=subprocess.PIPE) + process = run(command, check=False) return process.stdout.decode() def service_show(service_name: str) -> dict[str, str]: """Return the status of the service in dictionary format.""" command = ['systemctl', 'show', service_name] - process = subprocess.run(command, check=False, stdout=subprocess.PIPE) + process = run(command, check=False) status = {} for line in process.stdout.decode().splitlines(): parts = line.partition('=') @@ -199,8 +194,7 @@ def service_show(service_name: str) -> dict[str, str]: def service_action(service_name: str, action: str, check: bool = False): """Perform the given action on the service_name.""" - subprocess.run(['systemctl', action, service_name], - stdout=subprocess.DEVNULL, check=check) + run(['systemctl', action, service_name], check=check) def webserver_is_enabled(name, kind='config'): @@ -211,8 +205,7 @@ def webserver_is_enabled(name, kind='config'): option_map = {'config': '-c', 'site': '-s', 'module': '-m'} try: # Don't print anything on the terminal - subprocess.check_output(['a2query', option_map[kind], name], - stderr=subprocess.STDOUT) + run(['a2query', option_map[kind], name], check=True) return True except subprocess.CalledProcessError: return False @@ -234,7 +227,7 @@ def webserver_enable(name, kind='config', apply_changes=True): 'site': 'a2ensite', 'module': 'a2enmod' } - subprocess.check_output([command_map[kind], name]) + run([command_map[kind], name], check=True) action_required = 'restart' if kind == 'module' else 'reload' @@ -263,7 +256,7 @@ def webserver_disable(name, kind='config', apply_changes=True): 'site': 'a2dissite', 'module': 'a2dismod' } - subprocess.check_output([command_map[kind], name]) + run([command_map[kind], name], check=True) action_required = 'restart' if kind == 'module' else 'reload' @@ -391,7 +384,7 @@ def get_ip_addresses() -> list[dict[str, str | bool]]: """Return a list of IP addresses assigned to the system.""" addresses = [] - output = subprocess.check_output(['ip', '-o', 'addr']) + output = run(['ip', '-o', 'addr'], check=True).stdout for line in output.decode().splitlines(): parts = line.split() address: dict[str, str | bool] = { @@ -417,7 +410,7 @@ def get_ip_addresses() -> list[dict[str, str | bool]]: def get_hostname(): """Return the current hostname.""" - return subprocess.check_output(['hostname']).decode().strip() + return run(['hostname'], check=True).stdout.decode().strip() def dpkg_reconfigure(package, config): @@ -440,7 +433,7 @@ Owners: {package} env['DEBCONF_DB_OVERRIDE'] = 'File{' + override_file.name + \ ' readonly:true}' env['DEBIAN_FRONTEND'] = 'noninteractive' - subprocess.run(['dpkg-reconfigure', package], env=env, check=False) + run(['dpkg-reconfigure', package], env=env, check=False) try: os.remove(override_file.name) @@ -454,12 +447,12 @@ def debconf_set_selections(presets): # Workaround Debian Bug #487300. In some situations, debconf complains # it can't find the question being answered even though it is supposed # to create a dummy question for it. - subprocess.run(['/usr/share/debconf/fix_db.pl'], check=True) + run(['/usr/share/debconf/fix_db.pl'], check=True) except (FileNotFoundError, PermissionError): pass presets = '\n'.join(presets) - subprocess.check_output(['debconf-set-selections'], input=presets.encode()) + run(['debconf-set-selections'], input=presets.encode(), check=True) def is_disk_image(): @@ -472,8 +465,7 @@ def is_disk_image(): return os.path.exists('/var/lib/freedombox/is-freedombox-disk-image') -def run_apt_command(arguments, stdout=subprocess.DEVNULL, - enable_triggers: bool = False): +def run_apt_command(arguments, enable_triggers: bool = False): """Run apt-get with provided arguments.""" command = ['apt-get', '--assume-yes', '--quiet=2'] + arguments @@ -481,8 +473,7 @@ def run_apt_command(arguments, stdout=subprocess.DEVNULL, env['DEBIAN_FRONTEND'] = 'noninteractive' if not enable_triggers: env['FREEDOMBOX_INVOKED'] = 'true' - process = subprocess.run(command, stdin=subprocess.DEVNULL, stdout=stdout, - env=env, check=False) + process = run(command, stdin=subprocess.DEVNULL, env=env, check=False) return process.returncode @@ -503,25 +494,24 @@ def apt_hold(packages): held_packages = [] try: for package in packages: - current_hold = subprocess.check_output( - ['apt-mark', 'showhold', package]) + current_hold = run(['apt-mark', 'showhold', package], + check=True).stdout if not current_hold: - process = subprocess.run(['apt-mark', 'hold', package], - check=False) + process = run(['apt-mark', 'hold', package], check=False) if process.returncode == 0: # success held_packages.append(package) yield held_packages finally: for package in held_packages: - subprocess.check_call(['apt-mark', 'unhold', package]) + run(['apt-mark', 'unhold', package], check=True) @contextmanager def apt_hold_freedombox(): """Prevent freedombox package from being removed during apt operations.""" - current_hold = subprocess.check_output( - ['apt-mark', 'showhold', 'freedombox']) + current_hold = run(['apt-mark', 'showhold', 'freedombox'], + check=True).stdout try: if current_hold: # Package is already held, possibly by administrator. @@ -530,7 +520,7 @@ def apt_hold_freedombox(): # Set the flag. apt_hold_flag.parent.mkdir(mode=0o755, parents=True, exist_ok=True) apt_hold_flag.touch(mode=0o660) - yield subprocess.check_call(['apt-mark', 'hold', 'freedombox']) + yield run(['apt-mark', 'hold', 'freedombox'], check=True) finally: # Was the package held, either in this process or a previous one? if not current_hold or apt_hold_flag.exists(): @@ -539,9 +529,7 @@ def apt_hold_freedombox(): def apt_unhold_freedombox(): """Remove any hold on freedombox package, and clear flag.""" - subprocess.run(['apt-mark', 'unhold', 'freedombox'], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - check=False) + run(['apt-mark', 'unhold', 'freedombox'], check=False) if apt_hold_flag.exists(): apt_hold_flag.unlink() @@ -552,7 +540,7 @@ def is_package_manager_busy(): is open which indicates that the package manager is busy""" LOCK_FILE = '/var/lib/dpkg/lock' try: - subprocess.check_output(['lsof', LOCK_FILE]) + run(['lsof', LOCK_FILE], check=True) return True except subprocess.CalledProcessError: return False @@ -568,15 +556,14 @@ def podman_create(container_name: str, image_name: str, volume_name: str, service_stop(container_name) # Data is kept - subprocess.run(['podman', 'volume', 'rm', '--force', volume_name], - check=False) + run(['podman', 'volume', 'rm', '--force', volume_name], check=False) directory = pathlib.Path('/etc/containers/systemd') directory.mkdir(parents=True, exist_ok=True) # Fetch the image before creating the container. The systemd service for # the container won't timeout due to slow internet connectivity. - subprocess.run(['podman', 'image', 'pull', image_name], check=True) + run(['podman', 'image', 'pull', image_name], check=True) pathlib.Path(volume_path).mkdir(parents=True, exist_ok=True) # Create storage volume @@ -735,10 +722,8 @@ def podman_disable(container_name: str): def podman_uninstall(container_name: str, volume_name: str, image_name: str, volume_path: str): """Remove a podman container's components and systemd unit.""" - subprocess.run(['podman', 'volume', 'rm', '--force', volume_name], - check=True) - subprocess.run(['podman', 'image', 'rm', '--ignore', image_name], - check=True) + run(['podman', 'volume', 'rm', '--force', volume_name], check=True) + run(['podman', 'image', 'rm', '--ignore', image_name], check=True) volume_file = pathlib.Path( '/etc/containers/systemd/') / f'{volume_name}.volume' volume_file.unlink(missing_ok=True) @@ -825,8 +810,10 @@ def run_as_user(command, username, **kwargs): def run(command, **kwargs): """Run subprocess.run but capture stdout and stderr in thread storage.""" - collect_stdout = ('stdout' not in kwargs) - collect_stderr = ('stderr' not in kwargs) + collect_stdout = ('stdout' not in kwargs + and 'capture_output' not in kwargs) + collect_stderr = ('stderr' not in kwargs + and 'capture_output' not in kwargs) if collect_stdout: kwargs['stdout'] = subprocess.PIPE @@ -834,11 +821,20 @@ def run(command, **kwargs): if collect_stderr: kwargs['stderr'] = subprocess.PIPE - process = subprocess.run(command, **kwargs) - if collect_stdout and actions.thread_storage: - actions.thread_storage.stdout += process.stdout + try: + process = subprocess.run(command, **kwargs) + if collect_stdout and hasattr(actions.thread_storage, 'stdout'): + actions.thread_storage.stdout += process.stdout - if collect_stderr and actions.thread_storage: - actions.thread_storage.stderr += process.stderr + if collect_stderr and hasattr(actions.thread_storage, 'stderr'): + actions.thread_storage.stderr += process.stderr + except subprocess.CalledProcessError as exception: + if exception.stdout and hasattr(actions.thread_storage, 'stdout'): + actions.thread_storage.stdout += exception.stdout + + if exception.stderr and hasattr(actions.thread_storage, 'stderr'): + actions.thread_storage.stderr += exception.stderr + + raise exception return process diff --git a/plinth/actions.py b/plinth/actions.py index 0f195da73..0e75fc0f4 100644 --- a/plinth/actions.py +++ b/plinth/actions.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) socket_path = '/run/freedombox/privileged.socket' -thread_storage = None +thread_storage = threading.local() # An alias for 'str' to mark some strings as sensitive. Sensitive strings are @@ -88,6 +88,9 @@ def _read_from_server(client_socket: socket.socket) -> bytes: response += chunk + if not response: + raise ConnectionError('Server returned empty response') + return json.loads(response) @@ -170,8 +173,8 @@ def _wait_for_server_response(func, module_name, action_name, args, kwargs, module = importlib.import_module(return_value['exception']['module']) exception_class = getattr(module, return_value['exception']['name']) exception = exception_class(*return_value['exception']['args']) - exception.stdout = return_value['exception']['stdout'].encode() - exception.stderr = return_value['exception']['stderr'].encode() + exception.stdout = return_value['exception'].get('stdout', b'').encode() + exception.stderr = return_value['exception'].get('stderr', b'').encode() def _get_html_message(): """Return an HTML format error that can be shown in messages.""" @@ -364,7 +367,6 @@ class JSONEncoder(json.JSONEncoder): def _setup_thread_storage(): """Setup collection of stdout/stderr from any process in this thread.""" global thread_storage - thread_storage = threading.local() thread_storage.stdout = b'' thread_storage.stderr = b'' @@ -376,14 +378,13 @@ def _clear_thread_storage(): cleaned up after a thread terminates. """ global thread_storage - if thread_storage: - thread_storage.stdout = None - thread_storage.stderr = None - thread_storage = None + thread_storage.stdout = None + thread_storage.stderr = None def get_return_value_from_exception(exception): """Return the value to return from server when an exception is raised.""" + global thread_storage return_value = { 'result': 'exception', 'exception': { @@ -391,14 +392,10 @@ def get_return_value_from_exception(exception): 'name': type(exception).__name__, 'args': exception.args, 'traceback': traceback.format_tb(exception.__traceback__), - 'stdout': '', - 'stderr': '' + 'stdout': getattr(thread_storage, 'stdout', b'').decode(), + 'stderr': getattr(thread_storage, 'stderr', b'').decode(), } } - if thread_storage: - return_value['exception']['stdout'] = thread_storage.stdout.decode() - return_value['exception']['stderr'] = thread_storage.stderr.decode() - return return_value @@ -431,8 +428,6 @@ def privileged_handle_json_request( try: request = _parse_request() - logger.info('Received request for %s..%s(..)', request['module'], - request['action']) arguments = {'args': request['args'], 'kwargs': request['kwargs']} _setup_thread_storage() return_value = _privileged_call(request['module'], request['action'], @@ -495,6 +490,8 @@ def _privileged_call(module_name, action_name, arguments): _privileged_assert_valid_arguments(func, arguments) + _log_action(func, module_name, action_name, arguments['args'], + arguments['kwargs'], run_in_background=False) try: return_values = func(*arguments['args'], **arguments['kwargs']) if isinstance(return_values, io.BufferedReader): @@ -503,6 +500,11 @@ def _privileged_call(module_name, action_name, arguments): return_value = {'result': 'success', 'return': return_values} except Exception as exception: return_value = get_return_value_from_exception(exception) + logger.exception( + 'Error running action: %s..%s(..): %s\nstdout:\n%s\nstderr:\n%s\n', + module_name, action_name, exception, + return_value['exception']['stdout'], + return_value['exception']['stderr']) return return_value diff --git a/plinth/conftest.py b/plinth/conftest.py index dee221380..a67589b22 100644 --- a/plinth/conftest.py +++ b/plinth/conftest.py @@ -6,7 +6,6 @@ pytest configuration for all tests. import importlib import os import pathlib -import subprocess from unittest.mock import patch import pytest @@ -188,7 +187,8 @@ def fixture_mock_run_as_user(): """A fixture to override action_utils.run_as_user.""" def _bypass_runuser(*args, username, **kwargs): - return subprocess.run(*args, **kwargs) + from plinth import action_utils + return action_utils.run(*args, **kwargs) with patch('plinth.action_utils.run_as_user') as mock: mock.side_effect = _bypass_runuser diff --git a/plinth/daemon.py b/plinth/daemon.py index 9c8fba8d9..8b91a8148 100644 --- a/plinth/daemon.py +++ b/plinth/daemon.py @@ -91,6 +91,13 @@ class Daemon(app.LeaderComponent, log.LogEmitter): def ensure_running(self): """Ensure a service is running and return to previous state.""" from plinth.privileged import service as service_privileged + + if action_utils.service_show(self.unit)['LoadState'] == 'not-found': + # The service's package not installed yet, don't try to start it + # and later stop it after it is installed. + yield False # Not running + return + starting_state = self.is_running() if not starting_state: service_privileged.enable(self.unit) diff --git a/plinth/db/mariadb.py b/plinth/db/mariadb.py index d7680c34e..c5f52433e 100644 --- a/plinth/db/mariadb.py +++ b/plinth/db/mariadb.py @@ -6,12 +6,14 @@ Uses utilities from 'mysql-client' package such as 'mysql' and 'mysqldump'. import subprocess +from .. import action_utils + def run_query(database_name: str, query: str) -> subprocess.CompletedProcess: """Run a database query using 'root' user. Does not ensure that the database server is running. """ - return subprocess.run( + return action_utils.run( ['mysql', '--user=root', '--database', database_name], input=query.encode('utf-8'), check=True) diff --git a/plinth/db/postgres.py b/plinth/db/postgres.py index 1170c4608..1ddf618d6 100644 --- a/plinth/db/postgres.py +++ b/plinth/db/postgres.py @@ -6,7 +6,6 @@ Uses utilities from 'postgres' package such as 'psql' and 'pg_dump'. import os import pathlib -import subprocess from plinth import action_utils @@ -14,7 +13,7 @@ from plinth import action_utils def _run_as(command, **kwargs): """Run a command as 'postgres' user.""" command = ['sudo', '--user', 'postgres'] + command - return subprocess.run(command, check=True, **kwargs) + return action_utils.run(command, check=True, **kwargs) def run_query(query): diff --git a/plinth/glib.py b/plinth/glib.py index 5ef5fbec8..f4612d2a4 100644 --- a/plinth/glib.py +++ b/plinth/glib.py @@ -51,8 +51,13 @@ def _run(): def schedule(interval, method, data=None, in_thread=True, repeat=True, - add_jitter=True): - """Schedule a recurring call to a method with fixed interval.""" + add_jitter=True, develop_interval=None): + """Schedule a recurring call to a method with fixed interval. + + develop_interval is number of seconds to schedule the task after when + running in development mode. A value of 180 seconds assumed if the value is + set to None or 0. + """ def _runner(): """Run the target method and log and exceptions.""" @@ -74,8 +79,8 @@ def schedule(interval, method, data=None, in_thread=True, repeat=True, # When running in development mode, reduce the interval for tasks so that # they are triggered quickly and frequently to facilitate debugging. - if cfg.develop and interval > 180: - interval = 180 + if cfg.develop: + interval = min(interval, develop_interval or 180) if add_jitter: # Add or subtract 5% random jitter to given interval to avoid many diff --git a/plinth/locale/ar/LC_MESSAGES/django.po b/plinth/locale/ar/LC_MESSAGES/django.po index df54c1b5f..6200344c5 100644 --- a/plinth/locale/ar/LC_MESSAGES/django.po +++ b/plinth/locale/ar/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-04-16 02:28+0000\n" "Last-Translator: MohammedSaalif <2300031323@kluniversity.in>\n" "Language-Team: Arabic \n" "Language-Team: Arabic (Saudi Arabia) \n" "Language-Team: Bulgarian администратори могат да променят приложенията и настройките на " "системата." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Потребители и групи" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Достъп до всички услуги и системни настройки" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Проверете записа на LDAP „{search_item}“" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Проверете настройката на nslcd „{key} {value}“" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Проверете настройката на nsswitch „{database}“" @@ -9068,13 +9050,7 @@ msgid "The following administrator accounts exist in the system." msgstr "В системата съществуват следните администраторски профили." #: plinth/modules/users/templates/users_firstboot.html:56 -#, fuzzy, python-format -#| msgid "" -#| "Delete these accounts from command line and refresh the page to create an " -#| "account that is usable with %(box_name)s. On the command line run the " -#| "command \"echo '{\"args\": [\"USERNAME\", \"PASSWORD\"], \"kwargs\": {}}' " -#| "| sudo /usr/share/plinth/actions/actions users remove_user\". If an " -#| "account is already usable with %(box_name)s, skip this step." +#, python-format msgid "" "Delete these accounts from command line and refresh the page to create an " "account that is usable with %(box_name)s. On the command line run the " @@ -9085,7 +9061,7 @@ msgstr "" "За да създадете профил, който може да бъде използван с %(box_name)s, " "премахнете тези профили от командния ред и презаредете страницата. От " "команднен ред изпълнете командата „echo '{\"args\": [\"USERNAME\", " -"\"PASSWORD\"], \"kwargs\": {}}' | sudo /usr/share/plinth/actions/actions " +"\"AUTH_USER\", \"AUTH_PASSWORD\"], \"kwargs\": {}}' | sudo freedombox-cmd " "users remove_user“. Ако профилът вече може да се използва с %(box_name)s, " "прескочете тази стъпка." @@ -10016,10 +9992,8 @@ msgid "Clear all tags" msgstr "Изчистване на всички етикети" #: plinth/templates/toolbar.html:39 plinth/templates/toolbar.html:40 -#, fuzzy -#| msgid "Logs" msgid "View Logs" -msgstr "Дневник" +msgstr "Преглед на дневника" #: plinth/templates/toolbar.html:46 plinth/templates/toolbar.html:47 msgid "Backup" diff --git a/plinth/locale/bn/LC_MESSAGES/django.po b/plinth/locale/bn/LC_MESSAGES/django.po index e6d4b1788..74438b562 100644 --- a/plinth/locale/bn/LC_MESSAGES/django.po +++ b/plinth/locale/bn/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-04-01 03:02+0000\n" "Last-Translator: MURALA SAI GANESH \n" "Language-Team: Bengali \n" "Language-Team: Catalan \n" "Language-Team: Czech \n" @@ -33,27 +33,27 @@ msgstr "Kontejner {container_name} je spuštěn" msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "Služba {service_name} je spuštěná" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "Spojení očekáváno na {kind} portu {listen_address}:{port}" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "Spojení očekáváno na {kind} portu {port}" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "Připojit k {host}:{port}" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "Nedaří se připojit k {host}:{port}" @@ -215,12 +215,12 @@ msgstr "mDNS" msgid "Backups allows creating and managing backup archives." msgstr "Zálohy umožňují vytváření a správu zálohových archivů." -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "Zálohy" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." @@ -228,19 +228,19 @@ msgstr "" "Povolit automatický plán zálohování pro bezpečnost dat. Upřednostněte " "šifrované vzdálené umístění zálohování nebo další připojený disk." -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "Povolení plánu zálohování" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "Jít na {app_name}" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " @@ -249,7 +249,7 @@ msgstr "" "Naplánované zálohování se nezdařilo. Minulé pokusy o zálohování s počtem " "chyb {error_count} nebyly úspěšné. Poslední chyba je: {error_message}" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "Chyba při zálohování" @@ -1640,54 +1640,56 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Tato aplikace také zobrazuje protokoly pro služby " +"{box_name}." #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Diagnostika" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "přeskočeno" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "prošlo" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "selhalo" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "chyba" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "varování" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "MiB" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "GiB" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "Měli byste zakázat některé aplikace, abyste snížili využití paměti." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "Do tohoto systému byste neměli instalovat žádné nové aplikace." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1696,24 +1698,24 @@ msgstr "" "Systém má málo paměti: {percent_used}% využité, {memory_available} " "{memory_available_unit} volné. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Málo paměti" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "Spuštění diagnostiky" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "Při rutinních testech bylo nalezeno {issue_count} problémů." -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "Výsledky diagnostiky" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "Přejít na výsledky diagnostiky" @@ -9067,7 +9069,7 @@ msgstr "" msgid "Distribution Update" msgstr "Aktualizace distribuce" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "Kontrola zadržení balíčku" @@ -9478,7 +9480,7 @@ msgstr "Spouštění přechodu na novější verzi se nezdařilo." msgid "Frequent feature updates activated." msgstr "Aktivovány časté aktualizace funkcí." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9489,7 +9491,7 @@ msgstr "" "aby uživatelský účet byl součástí skupiny, která uživatele opravňuje k " "přístupu k aplikaci." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9501,25 +9503,25 @@ msgstr "" "nebo nastavení systému však mohou měnit pouze uživatelé skupiny admin." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Uživatelé a skupiny" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Přístup ke všem službám a nastavení systému" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Zkontrolujte LDAP položku „{search_item}“" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Zkontrolujte konfiguraci nslcd \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Zkontrolujte konfiguraci nsswitch \" {database}\"" @@ -10437,13 +10439,7 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the sledovači chyb, abychom ji mohli opravit. K hlášení " -"chyby prosím připojte také status log." +"chyby prosím připojte také protokoly." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "Instalace" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Toto je posledních %(num_lines)s řádků stavového protokolu pro toto webové " -"rozhraní. Pokud chcete nahlásit chybu, použijte prosím prohlížeč chyb a " -"připojte tento stavový protokol k hlášení chyby." +"Toto jsou poslední řádky protokolů služeb souvisejících s touto aplikací. " +"Pokud chcete nahlásit chybu, použijte prosím bug tracker a " +"připojte tento protokol k hlášení o chybě." #: plinth/templates/app-logs.html:26 msgid "" @@ -10736,10 +10726,8 @@ msgid "Clear all tags" msgstr "Vymazat všechny štítky" #: plinth/templates/toolbar.html:39 plinth/templates/toolbar.html:40 -#, fuzzy -#| msgid "Logs" msgid "View Logs" -msgstr "Protokoly" +msgstr "Zobrazit Protokoly" #: plinth/templates/toolbar.html:46 plinth/templates/toolbar.html:47 msgid "Backup" diff --git a/plinth/locale/da/LC_MESSAGES/django.po b/plinth/locale/da/LC_MESSAGES/django.po index e8aa26647..4474fff90 100644 --- a/plinth/locale/da/LC_MESSAGES/django.po +++ b/plinth/locale/da/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2022-09-14 17:19+0000\n" "Last-Translator: ikmaak \n" "Language-Team: Danish \n" "Language-Team: German \n" @@ -35,27 +35,27 @@ msgstr "Container {container_name} wird ausgeführt" msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "Der Dienst {service_name} wird ausgeführt" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "Gebunden auf {kind} Port {listen_address}:{port}" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "Gebunden an {kind} Port {port}" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "Verbinden mit {host}:{port}" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "Verbindung mit {host}:{port} fehlgeschlagen" @@ -221,12 +221,12 @@ msgstr "mDNS" msgid "Backups allows creating and managing backup archives." msgstr "Erstellen und Verwalten von Sicherungs-Archiven." -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "Sicherungen" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." @@ -235,19 +235,19 @@ msgstr "" "Bevorzugen Sie einen verschlüsselten Remote-Backup-Speicherort oder einen " "zusätzlich angeschlossenen Datenträger." -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "Aktivieren eines Sicherungszeitplans" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "Gehe zu {app_name}" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " @@ -257,7 +257,7 @@ msgstr "" "Versuche zur Sicherung waren nicht erfolgreich. Der letzte Fehler ist: " "{error_message}" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "Fehler beim Sichern" @@ -1680,56 +1680,58 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Diese App zeigt auch die Protokolle für " +"{box_name} Dienste an." #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Diagnose" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "übersprungen" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "bestanden" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "gescheitert" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "Fehler" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "Warnung" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "MiB" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "GiB" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" "Sie sollten einige Anwendungen deaktivieren, um den Speicherverbrauch zu " "reduzieren." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "Sie sollten auf diesem System keine neuen Anwendungen installieren." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1738,24 +1740,24 @@ msgstr "" "Das System hat wenig Speicherplatz: {percent_used}% verwendet, " "{memory_available}·{memory_available_unit}frei. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Wenig Speicher" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "Laufende Diagnosen" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "Bei Routinetests wurden {issue_count} Probleme gefunden." -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "Diagnose-Ergebnisse" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "Zu Diagnose-Ergebnisse gehen" @@ -9263,7 +9265,7 @@ msgstr "" msgid "Distribution Update" msgstr "Distributionsaktualisierung" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "Auf Paket-Sperren überprüfen" @@ -9693,7 +9695,7 @@ msgstr "Starten der Aktualisierung fehlgeschlagen." msgid "Frequent feature updates activated." msgstr "Häufige Funktions-Updates aktiviert." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9704,7 +9706,7 @@ msgstr "" "muss ein Benutzerkonto Teil einer Gruppe sein, damit ein Benutzer auf die " "App zugreifen kann." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9716,25 +9718,25 @@ msgstr "" "dürfen nur Mitglieder der Gruppe admin Apps oder " "Systemeinstellungen ändern." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Benutzer und Gruppen" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Zugriff auf alle Anwendungen und Systemeinstellungen" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "LDAP-Eintrag „{search_item}“ prüfen" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Prüfen Sie die nslcd-Konfiguration \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Prüfen Sie die nsswitch-Konfiguration \"{database}\"" @@ -10687,46 +10689,35 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the bug tracker so we can fix it. Also, please attach " "the logs to the bug report." msgstr "" -"Dies ist ein interner Fehler und nicht etwas, das Sie verursacht haben oder " -"beheben können. Bitte melden Sie den Fehler im online Fehlermelder, so dass wir ihn beheben können. Fügen Sie auch das Statusprotokoll dem Fehlerbericht bei." +"Dies ist ein interner Fehler, den Sie weder verursacht haben noch beheben " +"können. Bitte melden Sie den Fehler im Bug-Tracker, damit wir ihn beheben " +"können. Fügen Sie dem Fehlerbericht bitte auch die Protokolle bei." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "Installation" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Dies sind die letzten %(num_lines)s Zeilen des Statusprotokolls der " -"Weboberfläche. Bitte melden Sie den Fehler im online Fehlerverfolger und fügen diese Ausgabe hinzu." +"Dies sind die letzten Zeilen der Protokolle für Dienste, die an dieser App " +"beteiligt sind. Wenn Sie einen Fehler melden möchten, verwenden Sie bitte " +"den Bug-Tracker und fügen Sie dieses Protokoll dem Fehlerbericht " +"bei." #: plinth/templates/app-logs.html:26 msgid "" diff --git a/plinth/locale/django.pot b/plinth/locale/django.pot index d9aa4c44a..b48e31f3f 100644 --- a/plinth/locale/django.pot +++ b/plinth/locale/django.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -31,27 +31,27 @@ msgstr "" msgid "FreedomBox" msgstr "" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "" @@ -206,37 +206,37 @@ msgstr "" msgid "Backups allows creating and managing backup archives." msgstr "" -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." msgstr "" -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " "succeed. The latest error is: {error_message}" msgstr "" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "" @@ -1519,76 +1519,76 @@ msgid "" msgstr "" #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "" -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " "{memory_available_unit} free. {advice_message}" msgstr "" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "" -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "" @@ -7987,7 +7987,7 @@ msgstr "" msgid "Distribution Update" msgstr "" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "" @@ -8330,14 +8330,14 @@ msgstr "" msgid "Frequent feature updates activated." msgstr "" -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " "account to be part of a group to authorize the user to access the app." msgstr "" -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -8345,25 +8345,25 @@ msgid "" "group may alter apps or system settings." msgstr "" -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "" diff --git a/plinth/locale/el/LC_MESSAGES/django.po b/plinth/locale/el/LC_MESSAGES/django.po index 2671518ea..e13d077d8 100644 --- a/plinth/locale/el/LC_MESSAGES/django.po +++ b/plinth/locale/el/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2022-09-14 17:20+0000\n" "Last-Translator: ikmaak \n" "Language-Team: Greek admin μπορούν να " "τροποποιήσουν τις εφαρμογές ή τις ρυθμίσεις του συστήματος." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Χρήστες και ομάδες" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Πρόσβαση σε όλες τις υπηρεσίες και τις ρυθμίσεις συστήματος" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Ελέγξτε την καταχώρηση LDAP \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "" diff --git a/plinth/locale/es/LC_MESSAGES/django.po b/plinth/locale/es/LC_MESSAGES/django.po index c4cdf994d..b881c1d50 100644 --- a/plinth/locale/es/LC_MESSAGES/django.po +++ b/plinth/locale/es/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2024-11-01 17:00+0000\n" "Last-Translator: gallegonovato \n" "Language-Team: Spanish admin pueden cambiar configuraciones de " "apps o del sistema." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Usuarias/os y grupos" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Acceso a todos los servicios y configuraciones del sistema" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Comprobar la entrada LDAP \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Comprobar la configuración de nslcd \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Comprueba la configuración del nsswitch \"{database}\"" diff --git a/plinth/locale/et/LC_MESSAGES/django.po b/plinth/locale/et/LC_MESSAGES/django.po index 21fd15e98..dfcee8482 100644 --- a/plinth/locale/et/LC_MESSAGES/django.po +++ b/plinth/locale/et/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-07-20 18:01+0000\n" "Last-Translator: Priit Jõerüüt \n" "Language-Team: Estonian \n" "Language-Team: Persian \n" "Language-Team: Plinth Developers \n" "Language-Team: French admin peuvent modifier les applications ou changer les paramètres système." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Utilisateurs et groupes" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Accès à tous les services et à la configuration du système" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Vérification de l’entrée LDAP « {search_item} »" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Vérifier la configuration nslcd \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Vérifier la configuration nsswitch \"{database}\"" diff --git a/plinth/locale/gl/LC_MESSAGES/django.po b/plinth/locale/gl/LC_MESSAGES/django.po index 2d97c0568..29697a651 100644 --- a/plinth/locale/gl/LC_MESSAGES/django.po +++ b/plinth/locale/gl/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2022-12-30 10:51+0000\n" "Last-Translator: gallegonovato \n" "Language-Team: Galician \n" "Language-Team: Gujarati \n" "Language-Team: Hindi \n" @@ -35,27 +35,27 @@ msgstr "A szolgáltatás fut: {service_name}" msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "A szolgáltatás fut: {service_name}" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "Figyelés a {kind} porton: {listen_address}:{port}" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "Figyelés {kind} porton: {port}" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "Csatlakozás ide: {host}:{port}" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "Nem lehet ide csatlakozni: {host}:{port}" @@ -225,12 +225,12 @@ msgstr "mDNS" msgid "Backups allows creating and managing backup archives." msgstr "Lehetővé teszi a biztonsági mentés létrehozását és kezelését." -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "Biztonsági mentések" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." @@ -238,19 +238,19 @@ msgstr "" "Automatikus biztonsági mentések ütemezésének engedélyezése. Ha lehet, " "használj egy titkosított távoli helyet, vagy egy extra külső lemezt." -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "Ütemezett biztonsági mentés engedélyezése" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "Ugrás ide: {app_name}" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " @@ -260,7 +260,7 @@ msgstr "" "mentésre tett próbálkozás nem sikerült. A legutóbbi hibaüzenet: " "{error_message}" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "Hiba történt a biztonsági mentés közben" @@ -1724,52 +1724,52 @@ msgid "" msgstr "" #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Hibaellenőrzés" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "sikerült" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "sikertelen" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "hiba" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "figyelmeztetés" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "MiB" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "GiB" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "A memóriahasználat csökkentése érdekében tilts le néhány alkalmazást." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "Ne telepíts további alkalmazásokat erre a rendszerre." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1778,28 +1778,28 @@ msgstr "" "A rendszerben kevés a memória: {percent_used}% használt, " "{memory_available} {memory_available_unit} szabad. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Kevés a memória" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 #, fuzzy #| msgid "Run Diagnostics" msgid "Running diagnostics" msgstr "Ellenőrzés indítása" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "" -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 #, fuzzy #| msgid "Diagnostic Results" msgid "Diagnostics results" msgstr "Az ellenőrzés eredménye" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 #, fuzzy #| msgid "Diagnostic Results" msgid "Go to diagnostics results" @@ -9442,7 +9442,7 @@ msgstr "" msgid "Distribution Update" msgstr "A disztribúció frissítése elindult" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "" @@ -9851,7 +9851,7 @@ msgstr "A frissítést nem sikerült elindítani." msgid "Frequent feature updates activated." msgstr "Gyakori funkciófrissítések aktiválva." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9862,7 +9862,7 @@ msgstr "" "alkalmazások megkövetelik továbbá, hogy a felhasználói fiók egy csoport " "tagja legyen, hogy a felhasználó hozzáférhessen az alkalmazáshoz." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9874,25 +9874,25 @@ msgstr "" "az admin csoport felhasználói módosíthatják az alkalmazásokat vagy " "a rendszerbeállításokat." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Felhasználók és csoportok" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Hozzáférés az összes szolgáltatáshoz és rendszerbeállításhoz" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "LDAP-bejegyzés ellenőrzése: \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "" diff --git a/plinth/locale/id/LC_MESSAGES/django.po b/plinth/locale/id/LC_MESSAGES/django.po index eee9211b5..6c8c8f3ca 100644 --- a/plinth/locale/id/LC_MESSAGES/django.po +++ b/plinth/locale/id/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Indonesian (FreedomBox)\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2022-09-14 17:19+0000\n" "Last-Translator: ikmaak \n" "Language-Team: Indonesian \n" "Language-Team: Italian \n" @@ -33,27 +33,27 @@ msgstr "Il contenitore {container_name} è in esecuzione" msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "Il servizio {service_name} è in esecuzione" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "In ascolto sulla porta {kind}{listen_address}:{port}" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "In ascolto sulla porta{port}:{kind}" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "Connessione a {host}:{port}" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "Impossibile connettersi a {host}:{port}" @@ -217,37 +217,37 @@ msgstr "" msgid "Backups allows creating and managing backup archives." msgstr "Backups consente di creare e gestire archivi di backup." -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "Backup" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." msgstr "" -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "Abilita una schedulazione del Backup" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "Vai a {app_name}" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " "succeed. The latest error is: {error_message}" msgstr "" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "Errore durante il backup" @@ -1617,78 +1617,80 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Questa app mostra anche i log per i servizi " +"{box_name}." #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Diagnostica" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "superato" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "fallito" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "GiB" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "" -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " "{memory_available_unit} free. {advice_message}" msgstr "" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "Esecuzione Diagnostica" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "" -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "Risultati Diagnostica" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "Vai ai risultati diagnostici" @@ -8521,7 +8523,7 @@ msgstr "" msgid "Distribution Update" msgstr "Aggiornamento della distribuzione" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "" @@ -8872,14 +8874,14 @@ msgstr "" msgid "Frequent feature updates activated." msgstr "" -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " "account to be part of a group to authorize the user to access the app." msgstr "" -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -8887,25 +8889,25 @@ msgid "" "group may alter apps or system settings." msgstr "" -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "" @@ -9739,13 +9741,7 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the bug tracker in modo " -"da poterlo correggere. Inoltre, si prega di allegare il status log alla segnalazione del bug." +"da poterlo correggere. Inoltre, si prega di allegare i status log alla segnalazione del bug." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Queste sono le ultime %(num_lines)s del status log di questa interfaccia " -"web. Se vuoi riportare un bug, prego usa il bug tracker e " -"allega questo status log report del bug." +"Queste sono le ultime righe dei status log di questa app. Se vuoi riportare " +"un bug, prego usa il bug tracker e allega questo status log report del " +"bug." #: plinth/templates/app-logs.html:26 msgid "" diff --git a/plinth/locale/ja/LC_MESSAGES/django.po b/plinth/locale/ja/LC_MESSAGES/django.po index 2d5e3c34c..864118bed 100644 --- a/plinth/locale/ja/LC_MESSAGES/django.po +++ b/plinth/locale/ja/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2023-05-07 23:50+0000\n" "Last-Translator: Nobuhiro Iwamatsu \n" "Language-Team: Japanese \n" "Language-Team: Kannada \n" "Language-Team: Lithuanian \n" "Language-Team: Latvian \n" "Language-Team: Norwegian Bokmål admin-gruppen endre programmer eller systeminnstillinger." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Brukere og grupper" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Tilgang til alle tjenester og systeminnstillinger" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Sjekk LDAP-oppføring «{search_item}»" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "" diff --git a/plinth/locale/nl/LC_MESSAGES/django.po b/plinth/locale/nl/LC_MESSAGES/django.po index 67b282a75..e84914cba 100644 --- a/plinth/locale/nl/LC_MESSAGES/django.po +++ b/plinth/locale/nl/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-09-17 09:01+0000\n" "Last-Translator: ikmaak \n" "Language-Team: Dutch admin -groep mogen toepassings- of " "systeeminstellingen wijzigen." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Gebruikers en Groepen" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Toegang tot alle diensten en systeeminstellingen" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Zoek LDAP item \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Controleer nslcd configuratie \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Controleer nsswitch config \"{database}\"" diff --git a/plinth/locale/pl/LC_MESSAGES/django.po b/plinth/locale/pl/LC_MESSAGES/django.po index f937557df..58dc870cb 100644 --- a/plinth/locale/pl/LC_MESSAGES/django.po +++ b/plinth/locale/pl/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2024-07-13 12:09+0000\n" "Last-Translator: Monika \n" "Language-Team: Polish \n" "Language-Team: Portuguese \n" "Language-Team: Russian \n" @@ -34,27 +34,27 @@ msgstr "Контейнер {container_name} запущен" msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "Выполняется служба {service_name}" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "Слушать на {kind} порт {listen_address}:{port}" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "Слушать порт {port} на {kind}" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "Подключение к {host}:{port}" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "Невозможно подключиться к {host}:{port}" @@ -213,19 +213,19 @@ msgstr "Местный" #: plinth/modules/avahi/manifest.py:14 msgid "mDNS" -msgstr "mDNS" +msgstr "" #: plinth/modules/backups/__init__.py:24 msgid "Backups allows creating and managing backup archives." msgstr "" "Резервное копирование позволяет создавать и управлять резервными архивами." -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "Резервные копии" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." @@ -234,19 +234,19 @@ msgstr "" "данных. Предпочтительно зашифрованное удаленное место резервного копирования " "или дополнительный подключенный диск." -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "Включить расписание резервного копирования" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "Перейти в {app_name}" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " @@ -256,7 +256,7 @@ msgstr "" "попытки резервного копирования не увенчались успехом. Последняя ошибка: " "{error_message}" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "Ошибка во время резервного копирования" @@ -503,7 +503,7 @@ msgstr "Конфигурация" #: plinth/modules/backups/manifest.py:21 msgid "Borg" -msgstr "Borg" +msgstr "" #: plinth/modules/backups/privileged.py:34 msgid "" @@ -935,7 +935,7 @@ msgstr "Список и чтение всех файлов" #: plinth/modules/bepasty/__init__.py:57 plinth/modules/bepasty/manifest.py:6 msgid "bepasty" -msgstr "bepasty" +msgstr "" #: plinth/modules/bepasty/forms.py:17 msgid "Public Access (default permissions)" @@ -977,7 +977,7 @@ msgstr "Обмен файлами" #: plinth/modules/bepasty/manifest.py:23 msgid "Pastebin" -msgstr "Pastebin" +msgstr "" #: plinth/modules/bepasty/templates/bepasty.html:12 msgid "Manage Passwords" @@ -1658,56 +1658,58 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Это приложение также показывает журналы для " +"{box_name} ." #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Диагностика" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "пропустить" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "успешно" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "сбой" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "ошибка" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "предупреждение" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "МБ" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "ГБ" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" "Вы должны отключить некоторые приложения, чтобы уменьшить использование " "памяти." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "Вы не должны устанавливать никаких новых приложений в этой системе." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1716,24 +1718,24 @@ msgstr "" "В системе мало памяти: использовано {percent_used}%, свободно " "{memory_available} {memory_available_unit}. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Мало памяти" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "Выполнение диагностики" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "Найдены {issue_count} проблем во время обычных проверок." -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "Результаты диагностики" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "Перейдите к результатам диагностики" @@ -2455,7 +2457,7 @@ msgstr "Thunderbird" #: plinth/modules/email/manifest.py:37 msgid "Thunderbird Mobile" -msgstr "Thunderbird Mobile" +msgstr "" #: plinth/modules/email/manifest.py:52 msgid "FairEmail" @@ -2531,8 +2533,6 @@ msgid "Host/Target/Value" msgstr "Хост/Цель/Значение" #: plinth/modules/email/templates/email-dns.html:50 -#, fuzzy -#| msgid "Server hostname or IP address" msgid "Reverse DNS Records for IP Addresses" msgstr "Обратные записи DNS для IP-адресов" @@ -2566,8 +2566,6 @@ msgstr "" "разделе. Это можно настроить в приложении конфиденциальности." #: plinth/modules/email/templates/email-dns.html:76 -#, fuzzy -#| msgid "Hostname" msgid "Host" msgstr "Хост" @@ -2584,8 +2582,7 @@ msgstr "" "записей DNS домена." #: plinth/modules/email/templates/email.html:35 -#, fuzzy, python-format -#| msgid "Resolve domain name: {domain}" +#, python-format msgid "View domain: %(domain)s" msgstr "Просмотр домена: %(domain)s" @@ -2705,9 +2702,8 @@ msgstr "Веб-сайт" #: plinth/modules/featherwiki/manifest.py:18 #: plinth/modules/tiddlywiki/manifest.py:25 -#, fuzzy msgid "Quine" -msgstr "Quine" +msgstr "" #: plinth/modules/featherwiki/manifest.py:18 #: plinth/modules/nextcloud/manifest.py:56 @@ -3247,8 +3243,6 @@ msgstr "" "силу." #: plinth/modules/gnome/__init__.py:48 -#, fuzzy -#| msgid "GNOME Files" msgid "GNOME" msgstr "GNOME" @@ -3257,8 +3251,6 @@ msgid "Desktop" msgstr "Десктоп" #: plinth/modules/gnome/manifest.py:10 -#, fuzzy -#| msgid "Tor Browser" msgid "Browser" msgstr "Браузер" @@ -3267,8 +3259,6 @@ msgid "Office suite" msgstr "Офисный пакет" #: plinth/modules/gnome/manifest.py:12 -#, fuzzy -#| msgid "Software Update" msgid "Software store" msgstr "Магазин программного обеспечения" @@ -3658,12 +3648,6 @@ msgid "{box_name} Manual" msgstr "Руководство {box_name}" #: plinth/modules/homeassistant/__init__.py:31 -#, fuzzy -#| msgid "" -#| "Home Assistant is a home automation hub with emphasis on local control " -#| "and privacy. It integrates with thousands of devices including smart " -#| "bulbs, alarms, presense sensors, door bells, thermostats, irrigation " -#| "timers, energy monitors, etc." msgid "" "Home Assistant is a home automation hub with emphasis on local control and " "privacy. It integrates with thousands of devices including smart bulbs, " @@ -3727,9 +3711,8 @@ msgid "Home Automation" msgstr "Домашняя автоматизация" #: plinth/modules/homeassistant/manifest.py:63 -#, fuzzy msgid "IoT" -msgstr "IoT" +msgstr "" #: plinth/modules/homeassistant/manifest.py:64 #: plinth/modules/networks/manifest.py:8 @@ -3742,11 +3725,11 @@ msgstr "Wi-Fi" #: plinth/modules/homeassistant/manifest.py:65 msgid "ZigBee" -msgstr "ZigBee" +msgstr "" #: plinth/modules/homeassistant/manifest.py:66 msgid "Z-Wave" -msgstr "Z-Wave" +msgstr "" #: plinth/modules/homeassistant/manifest.py:67 msgid "Thread" @@ -3955,7 +3938,7 @@ msgstr "Видеокомната Janus" #: plinth/modules/janus/manifest.py:16 msgid "WebRTC" -msgstr "WebRTC" +msgstr "" #: plinth/modules/janus/manifest.py:16 msgid "Web conference" @@ -4038,7 +4021,7 @@ msgstr "Управление сервером контента Kiwix" #: plinth/modules/kiwix/__init__.py:56 plinth/modules/kiwix/manifest.py:8 msgid "Kiwix" -msgstr "Kiwix" +msgstr "" #: plinth/modules/kiwix/forms.py:23 msgid "Content packages have to be in .zim format" @@ -4302,6 +4285,10 @@ msgid "" "only be installed if frequent feature updates is enabled in the Software Update app." msgstr "" +"Примечание: Это приложение часто получает обновления. Оно " +"может быть установлено только в том случае, если в системе включено частое " +"обновление функций. Обновление программного " +"обеспечения ." #: plinth/modules/matrixsynapse/__init__.py:59 msgid "Matrix Synapse" @@ -4775,8 +4762,6 @@ msgid "UPnP" msgstr "UPnP" #: plinth/modules/minidlna/manifest.py:116 -#, fuzzy -#| msgid "MiniDLNA" msgid "DLNA" msgstr "DLNA" @@ -4814,9 +4799,8 @@ msgstr "" #: plinth/modules/miniflux/__init__.py:42 #: plinth/modules/miniflux/manifest.py:10 -#, fuzzy msgid "Miniflux" -msgstr "Miniflux" +msgstr "" #: plinth/modules/miniflux/forms.py:12 msgid "Enter a username for the user." @@ -4839,28 +4823,24 @@ msgid "Passwords do not match." msgstr "Пароли не совпадают." #: plinth/modules/miniflux/manifest.py:18 -#, fuzzy msgid "Fluent Reader Lite" -msgstr "Fluent Reader Lite" +msgstr "" #: plinth/modules/miniflux/manifest.py:33 msgid "Fluent Reader" msgstr "Беглое чтение" #: plinth/modules/miniflux/manifest.py:46 -#, fuzzy msgid "FluxNews" -msgstr "FluxNews" +msgstr "" #: plinth/modules/miniflux/manifest.py:61 -#, fuzzy msgid "MiniFlutt" -msgstr "MiniFlutt" +msgstr "" #: plinth/modules/miniflux/manifest.py:71 -#, fuzzy msgid "NetNewsWire" -msgstr "NetNewsWire" +msgstr "" #: plinth/modules/miniflux/manifest.py:86 msgid "Newsflash" @@ -4884,14 +4864,11 @@ msgstr "Агрегация новостей" #: plinth/modules/miniflux/manifest.py:138 #: plinth/modules/rssbridge/manifest.py:16 plinth/modules/ttrss/manifest.py:55 -#, fuzzy -#| msgid "SSH" msgid "RSS" msgstr "RSS" #: plinth/modules/miniflux/manifest.py:138 #: plinth/modules/rssbridge/manifest.py:16 plinth/modules/ttrss/manifest.py:55 -#, fuzzy msgid "ATOM" msgstr "ATOM" @@ -5201,7 +5178,7 @@ msgstr "Ссылка" #: plinth/modules/names/templates/names.html:104 #: plinth/modules/networks/templates/connection_show.html:268 msgid "DNS-over-TLS" -msgstr "DNS-over-TLS" +msgstr "" #: plinth/modules/names/templates/names.html:108 msgid "DNSSEC" @@ -6249,7 +6226,7 @@ msgstr "локальная ссылка" #: plinth/modules/networks/views.py:32 msgid "dhcp" -msgstr "dhcp" +msgstr "" #: plinth/modules/networks/views.py:33 msgid "ignore" @@ -6509,7 +6486,7 @@ msgstr "" #: plinth/modules/nextcloud/manifest.py:11 #: plinth/modules/nextcloud/manifest.py:18 msgid "Nextcloud" -msgstr "Nextcloud" +msgstr "" #: plinth/modules/nextcloud/forms.py:19 msgid "Not set" @@ -7233,11 +7210,11 @@ msgstr "Контакты" #: plinth/modules/radicale/manifest.py:91 plinth/modules/sogo/manifest.py:75 msgid "CalDAV" -msgstr "CalDAV" +msgstr "" #: plinth/modules/radicale/manifest.py:91 plinth/modules/sogo/manifest.py:76 msgid "CardDAV" -msgstr "CardDAV" +msgstr "" #: plinth/modules/radicale/views.py:32 msgid "Access rights configuration updated" @@ -7833,7 +7810,7 @@ msgstr "Точка входа" #: plinth/modules/shadowsocks/manifest.py:22 #: plinth/modules/shadowsocksserver/manifest.py:21 msgid "Shadowsocks" -msgstr "Shadowsocks" +msgstr "" #: plinth/modules/shadowsocksserver/__init__.py:26 #, python-brace-format @@ -8102,7 +8079,7 @@ msgstr "Заведомо исправное состояние" #: plinth/modules/snapshot/manifest.py:14 msgid "Btrfs" -msgstr "Btrfs" +msgstr "" #: plinth/modules/snapshot/templates/snapshot_delete_selected.html:12 msgid "Delete the following snapshots permanently?" @@ -8730,7 +8707,7 @@ msgstr "" #: plinth/modules/tiddlywiki/__init__.py:64 #: plinth/modules/tiddlywiki/manifest.py:9 msgid "TiddlyWiki" -msgstr "TiddlyWiki" +msgstr "" #: plinth/modules/tiddlywiki/forms.py:39 msgid "A TiddlyWiki file with .html file extension" @@ -9103,7 +9080,7 @@ msgstr "TTRSS-читатель" #: plinth/modules/ttrss/manifest.py:25 msgid "Geekttrss" -msgstr "Geekttrss" +msgstr "" #: plinth/modules/upgrades/__init__.py:34 msgid "Check for and apply the latest software and security updates." @@ -9153,7 +9130,7 @@ msgstr "" msgid "Distribution Update" msgstr "Обновление дистрибутива" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "Проверьте, не задерживается ли посылка" @@ -9367,7 +9344,7 @@ msgstr "Автоматическое обновление отключено." #: plinth/modules/upgrades/templates/upgrades-dist-upgrade.html:54 msgid "Distribution upgrades are disabled." -msgstr "Обновления дистрибутива отключены" +msgstr "Обновления дистрибутива отключены." #: plinth/modules/upgrades/templates/upgrades-dist-upgrade.html:58 msgid "" @@ -9573,7 +9550,7 @@ msgstr "Не удалось запустить обновление." msgid "Frequent feature updates activated." msgstr "Активированы частые обновления функций." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9585,7 +9562,7 @@ msgstr "" "запись пользователя была частью группы, чтобы разрешить пользователю доступ " "к приложению." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9597,25 +9574,25 @@ msgstr "" "пользователи группы admin могут изменять приложения или системные " "настройки." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Пользователи и группы" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Доступ ко всем сервисам и настройкам системы" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Проверьте запись LDAP \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Проверьте конфигурацию nslcd \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Проверьте конфигурацию nsswitch \"{database}\"" @@ -10543,13 +10520,7 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the logs to the bug report." msgstr "" "Это внутренняя ошибка, а не то, что вы вызвали или можете исправить. " -"Сообщите об ошибке в трекере ошибок, чтобы мы могли ее исправить. Также " -"приложите журнал состояния к отчету об " -"ошибке." +"Пожалуйста, сообщите об ошибке на трекере ошибок чтобы мы могли это " +"исправить. Кроме того, пожалуйста, приложите файлы " +"журналов к сообщению об ошибке." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "Установка" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Это последние %(num_lines)s строк журнала состояния для этого веб-" -"интерфейса. Если вы хотите сообщить об ошибке, используйте трекер ошибок и " -"прикрепите этот журнал состояния к отчету об ошибке." +"Это последние строки журналов для служб, задействованных в этом приложении. " +"Если вы хотите сообщить об ошибке, пожалуйста, воспользуйтесь отслеживаним ошибок и приложите этот журнал к отчету об ошибке." #: plinth/templates/app-logs.html:26 msgid "" @@ -10844,10 +10809,8 @@ msgid "Clear all tags" msgstr "Очистить все теги" #: plinth/templates/toolbar.html:39 plinth/templates/toolbar.html:40 -#, fuzzy -#| msgid "Logs" msgid "View Logs" -msgstr "Журналы" +msgstr "Просмотр журналов" #: plinth/templates/toolbar.html:46 plinth/templates/toolbar.html:47 msgid "Backup" diff --git a/plinth/locale/si/LC_MESSAGES/django.po b/plinth/locale/si/LC_MESSAGES/django.po index bdc6deaef..5ad95f989 100644 --- a/plinth/locale/si/LC_MESSAGES/django.po +++ b/plinth/locale/si/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2021-04-27 13:32+0000\n" "Last-Translator: HelaBasa \n" "Language-Team: Sinhala \n" "Language-Team: Slovenian \n" "Language-Team: Albanian \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.13-dev\n" +"X-Generator: Weblate 5.14-dev\n" #: plinth/config.py:103 #, python-brace-format @@ -25,36 +25,35 @@ msgid "Static configuration {etc_path} is setup properly" msgstr "Formësimi statik {etc_path} është ujdisur si duhet" #: plinth/container.py:140 -#, fuzzy, python-brace-format -#| msgid "Service {service_name} is running" +#, python-brace-format msgid "Container {container_name} is running" -msgstr "Shërbimi {service_name} po xhiron" +msgstr "Kontejneri {container_name} po xhiron" #: plinth/context_processors.py:21 plinth/views.py:175 msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "Shërbimi {service_name} po xhiron" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "Po përgjohet në portë {kind} {listen_address}:{port}" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "Po përgjohet në portë {kind} {port}" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "Lidhu me {host}:{port}" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "S’lidhet dot me {host}:{port}" @@ -72,10 +71,8 @@ msgid "Repository to backup to" msgstr "Depo për kopjeruajtje" #: plinth/forms.py:62 -#, fuzzy -#| msgid "None" msgid "(None)" -msgstr "Asnjë" +msgstr "(Asnjë)" #: plinth/forms.py:68 msgid "Select a domain name to be used with this application" @@ -220,12 +217,12 @@ msgstr "mDNS" msgid "Backups allows creating and managing backup archives." msgstr "Kopjeruajtjet lejojnë krijim dhe administrim arkivash kopjeruajtjeje." -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "Kopjeruajtje" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." @@ -234,19 +231,19 @@ msgstr "" "Parapëlqeni një vendndodhje të largët kopjeruajtjesh të fshehtëzuara ose një " "disk ekstra bashkëngjitur." -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "Aktivizoni një Plan Kopjeruajtjesh" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "Kalo te {app_name}" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " @@ -255,7 +252,7 @@ msgstr "" "Dështoi një kopjeruajtje e planifikuar. {error_count} përpjekjet e mëparshme " "s’patën sukses. Gabimi i fundit qe: {error_message}" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "Gabim Gjatë Kopjeruajtjes" @@ -707,27 +704,15 @@ msgid "Restore data from" msgstr "Riktheje të dhëna prej" #: plinth/modules/backups/templates/backups_upload.html:17 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Upload a backup file downloaded from another %(box_name)s to " -#| "restore its\n" -#| " contents. You can choose the apps you wish to restore after " -#| "uploading a\n" -#| " backup file.\n" -#| " " +#, python-format msgid "" "Upload a backup file downloaded from another %(box_name)s to restore its " "contents. You can choose the apps you wish to restore after uploading a " "backup file." msgstr "" -"\n" -" Ngarkoni një kartel kopjeruajtje të shkarkuar prej një tjetër " -"%(box_name)s për\n" -" të rikthyer lëndën e tij. Pas ngarkimit të kartelës kopjeruajtje, mund " -"të zgjidhni\n" -" aplikacionet që doni të rikthehen.\n" -" " +"Ngarkoni një kartelë kopjeruajtje të shkarkuar prej një tjetër %(box_name)s " +"për të rikthyer lëndën e tij. Pas ngarkimit të kartelës kopjeruajtje, mund " +"të zgjidhni aplikacionet që doni të rikthehen." #: plinth/modules/backups/templates/backups_upload.html:31 #, python-format @@ -1673,55 +1658,57 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Ky aplikacion shfaq gjithashtu regjistrat për " +"shërbimet {box_name}." #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Diagnostikime" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "anashkaluar" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "kaloi" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "dështoi" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "gabim" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "kujdes" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "MiB" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "GiB" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" "Duhet të çaktivizoni disa aplikacione, për të zvogëluar përdorim kujtese." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "S’duhet të instaloni çfarëdo aplikacioni të ri në këtë sistem." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1730,24 +1717,24 @@ msgstr "" "Ka pak kujtesë për sistemin: {percent_used}% të përdorur, {memory_available} " "{memory_available_unit} të lirë. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Kujtesë e Pakët" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "Xhirim diagnostikimesh" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "U gjetën {issue_count} probleme gjatë testimeve rutinë." -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "Përfundime diagnostikimesh" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "Kaloni te përfundime diagnostikimi" @@ -1828,31 +1815,31 @@ msgstr "Përfundime" #: plinth/modules/diagnostics/templates/diagnostics_full.html:53 #, python-format msgid "%(number)s passed" -msgstr "" +msgstr "%(number)s të kaluar" #: plinth/modules/diagnostics/templates/diagnostics_full.html:57 #, python-format msgid "%(number)s failed" -msgstr "" +msgstr "%(number)s me dështim" #: plinth/modules/diagnostics/templates/diagnostics_full.html:61 #, python-format msgid "%(number)s warnings" -msgstr "" +msgstr "%(number)s sinjalizime" #: plinth/modules/diagnostics/templates/diagnostics_full.html:65 #, python-format msgid "%(number)s errors" -msgstr "" +msgstr "%(number)s gabime" #: plinth/modules/diagnostics/templates/diagnostics_full.html:69 #, python-format msgid "%(number)s skipped" -msgstr "" +msgstr "%(number)s të anashkaluar" #: plinth/modules/diagnostics/templates/diagnostics_full.html:111 msgid "Running..." -msgstr "" +msgstr "Po xhirohet…" #: plinth/modules/diagnostics/templates/diagnostics_results.html:11 msgid "Test" @@ -1903,13 +1890,6 @@ msgstr "" "tuaj DNS, do të marrë një përgjigje me adresën tuaj aktuale IP." #: plinth/modules/dynamicdns/__init__.py:41 -#, fuzzy -#| msgid "" -#| "If you are looking for a free dynamic DNS account, you may find a free " -#| "GnuDIP service at ddns.freedombox.org or you may find free update URL " -#| "based services at freedns.afraid.org." msgid "" "If you are looking for a free dynamic DNS account, you may find a free " "GnuDIP service at ddns.freedombox.org, ose mund të gjeni shërbime të " -"lira me bazë përditësim URL-je, te freedns.afraid.org." +"target=\"_blank\">ddns.freedombox.org. Me këtë shërbim, përfitoni " +"nënpërkatësi të pakufizuara (me mundësinë e shenjës së gjithëpushtetshme të " +"aktivizuar te rregullimet e llogarisë). Që të përdorni një nënpërkatësi, " +"shtojeni si një përkatësi statike te aplikacioni Emra." #: plinth/modules/dynamicdns/__init__.py:47 -#, fuzzy -#| msgid "" -#| "If you are looking for a free dynamic DNS account, you may find a free " -#| "GnuDIP service at ddns.freedombox.org or you may find free update URL " -#| "based services at freedns.afraid.org." msgid "" "Alternatively, you may find a free update URL based service at freedns.afraid.org." msgstr "" -"Nëse po kërkoni për një llogari falas DNS-je dinamike, mund të gjeni një " -"shërbim GnuDIP të lirë, te ddns.freedombox.org, ose mund të gjeni shërbime të " -"lira me bazë përditësim URL-je, te freedns.afraid.org." +"Ndryshe, mund të gjeni një shërbim falas të bazuar në përditësim URL-sh, te " +"freedns.afraid.org." #: plinth/modules/dynamicdns/__init__.py:50 msgid "" @@ -2088,7 +2060,7 @@ msgstr "Përkatësi" #: plinth/modules/dynamicdns/manifest.py:17 msgid "Free" -msgstr "" +msgstr "I lirë" #: plinth/modules/dynamicdns/manifest.py:17 msgid "Needs public IP" @@ -2680,7 +2652,7 @@ msgstr "Feather Wiki" #: plinth/modules/featherwiki/forms.py:13 plinth/modules/tiddlywiki/forms.py:13 msgid "Wiki files cannot be named \"index.html\"." -msgstr "" +msgstr "Kartelat Wiki s’mund të emërtohen “index.html”." #: plinth/modules/featherwiki/forms.py:20 plinth/modules/tiddlywiki/forms.py:20 msgid "Name of the wiki file, with file extension \".html\"" @@ -9172,7 +9144,7 @@ msgstr "" msgid "Distribution Update" msgstr "Përditësim Shpërndarjeje" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "Kontrolloni për mbajtje paketash" @@ -9603,7 +9575,7 @@ msgstr "Nisja e përmirësimi dështoi." msgid "Frequent feature updates activated." msgstr "Përditësime të shpeshta veçorish të aktivizuara." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9614,7 +9586,7 @@ msgstr "" "aplikacione kërkojnë doemos një llogari përdoruesi, për të qenë pjesë e një " "grupi që autorizon përdoruesin të përdorë aplikacionin." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9626,25 +9598,25 @@ msgstr "" "vetëm përdoruesit e grupit përgjegjës mund të ndryshojnë " "aplikacionet, apo rregullimet e sistemit." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Përdorues dhe Grupe" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Hyrje te krejt shërbimet dhe rregullime të sistemit" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Kontrolloni zërin LDAP \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Kontrolloni “{key} {value}” formësimi nslcd-je" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Kontrolloni “{database}” formësimi nsswitch" diff --git a/plinth/locale/sr/LC_MESSAGES/django.po b/plinth/locale/sr/LC_MESSAGES/django.po index 2e71ad406..f53580344 100644 --- a/plinth/locale/sr/LC_MESSAGES/django.po +++ b/plinth/locale/sr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2022-09-14 17:20+0000\n" "Last-Translator: ikmaak \n" "Language-Team: Serbian \n" "Language-Team: Swedish admin kan dock ändra appar eller Systeminställningar." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Användare och grupper" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Tillgång till alla tjänster och systeminställningar" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Kontrollera LDAP-posten \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Kontrollera nslcd-konfigurationen \"{key}{value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Kontrollera nsswitch-konfigurationen \"{database}\"" diff --git a/plinth/locale/ta/LC_MESSAGES/django.po b/plinth/locale/ta/LC_MESSAGES/django.po index 28942989c..6a135084f 100644 --- a/plinth/locale/ta/LC_MESSAGES/django.po +++ b/plinth/locale/ta/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-08-17 08:02+0000\n" "Last-Translator: தமிழ்நேரம் \n" "Language-Team: Tamil நிர்வாகம் குழுவின் பயனர்கள் " "மட்டுமே பயன்பாடுகள் அல்லது கணினி அமைப்புகளை மாற்றலாம்." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "பயனர்கள் மற்றும் குழுக்கள்" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "அனைத்து சேவைகள் மற்றும் கணினி அமைப்புகளுக்கான அணுகல்" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "LDAP உள்ளீட்டை சரிபார்க்கவும் \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "NSLCD கட்டமைப்பைச் சரிபார்க்கவும் \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "NSSWITCH கட்டமைப்பு \"{database}\"" diff --git a/plinth/locale/te/LC_MESSAGES/django.po b/plinth/locale/te/LC_MESSAGES/django.po index e5e0fcf9f..ea7ded507 100644 --- a/plinth/locale/te/LC_MESSAGES/django.po +++ b/plinth/locale/te/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-05-14 17:03+0000\n" "Last-Translator: Sripath Roy Koganti \n" "Language-Team: Telugu అడ్మిన్ సమూహం యొక్క వినియోగదారులు మాత్రమే యాప్‌లు లేదా సిస్టమ్ " "సెట్టింగ్‌లను మార్చవచ్చు." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "వినియోగదారులు మరియు సమూహాలు" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "అన్ని సేవలకు మరియు సిస్టమ్ అమరికలకు ప్రాప్యత" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "LDAP నమోదు \"{search_item}\" తనిఖీ" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "nslcd config \"{key} {value}\"ని తనిఖీ చేయండి" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "nsswitch config \"{database}\"ని తనిఖీ చేయండి" diff --git a/plinth/locale/tr/LC_MESSAGES/django.po b/plinth/locale/tr/LC_MESSAGES/django.po index 12d91f24c..2d2f636e2 100644 --- a/plinth/locale/tr/LC_MESSAGES/django.po +++ b/plinth/locale/tr/LC_MESSAGES/django.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-09-10 04:01+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" +"PO-Revision-Date: 2025-09-24 03:01+0000\n" "Last-Translator: Burak Yavuz \n" "Language-Team: Turkish \n" @@ -32,28 +32,28 @@ msgstr "{container_name} kapsayıcısı çalışıyor" msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "{service_name} hizmeti çalışıyor" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "" "{kind} üzerinde {listen_address}:{port} nolu bağlantı noktasını dinleme" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "{kind} üzerinde {port} nolu bağlantı noktasını dinleme" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "{host}:{port} adresine bağlı" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "{host}:{port} adresine bağlanamıyor" @@ -216,12 +216,12 @@ msgstr "mDNS" msgid "Backups allows creating and managing backup archives." msgstr "Yedeklemeler, yedekleme arşivleri oluşturmayı ve yönetmeyi sağlar." -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "Yedeklemeler" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." @@ -229,19 +229,19 @@ msgstr "" "Veri güvenliği için otomatik bir yedekleme planı etkinleştirin. Şifrelenmiş " "bir uzak yedekleme konumu veya fazladan eklenmiş bir disk tercih edin." -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "Bir Yedekleme Planı etkinleştirin" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "{app_name} için git" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " @@ -250,7 +250,7 @@ msgstr "" "Planlanmış bir yedekleme başarısız oldu. Geçen {error_count} yedekleme " "denemesi başarılı olmadı. En son hata: {error_message}" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "Yedekleme Sırasında Hata" @@ -1642,55 +1642,57 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Bu uygulama ayrıca {box_name} hizmetleri için günlükleri gösterir." #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Tanılama" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "atlandı" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "geçti" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "başarısız" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "hata" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "uyarı" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "MiB" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "GiB" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" "Bellek kullanımını azaltmak için bazı uygulamaları etkisizleştirmelisiniz." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "Bu sisteme herhangi bir yeni uygulama yüklememelisiniz." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1699,24 +1701,24 @@ msgstr "" "Sistem belleği düşük: %{percent_used} kullanılıyor, {memory_available}." "{memory_available_unit} boş. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Düşük Bellek" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "Tanılama çalıştırılıyor" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "Sıradan denemeler sırasında {issue_count} sorun bulundu." -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "Tanı sonuçları" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "Tanı sonuçlarına git" @@ -9112,7 +9114,7 @@ msgstr "" msgid "Distribution Update" msgstr "Dağıtım Güncellemesi" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "Paket bekletmelerini denetle" @@ -9527,7 +9529,7 @@ msgstr "Yükseltmeyi başlatma başarısız oldu." msgid "Frequent feature updates activated." msgstr "Sık yapılan özellik güncellemeleri etkinleştirildi." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9538,7 +9540,7 @@ msgstr "" "kullanıcıya, uygulamaya erişme yetkisi vermek için bir kullanıcı hesabının " "bir grubun parçası olmasını gerektirir." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9550,25 +9552,25 @@ msgstr "" "sadece admin grubunun kullanıcıları uygulamaları veya sistem " "ayarlarını değiştirebilir." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Kullanıcılar ve Gruplar" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Tüm hizmetlere ve sistem ayarlarına erişim" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "LDAP \"{search_item}\" girişini denetleme" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "\"{key} {value}\" nslcd yapılandırmasını denetleme" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "\"{database}\" nsswitch yapılandırmasını denetleme" @@ -10492,13 +10494,7 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the hata izleyicide hatayı bildirin. " -"Ayrıca, lütfen hata raporuna durum günlüğünü ekleyin." +"Ayrıca, lütfen hata raporuna günlükleri ekleyin." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "Kurulum" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Bunlar, bu web arayüzü için durum günlüğünün son %(num_lines)s satırıdır. " +"Bunlar, bu uygulamada yer alan hizmetler için günlüklerin son satırlarıdır. " "Eğer bir hata bildirmek istiyorsanız, lütfen hata izleyiciyi " -"kullanın ve bu durum günlüğünü hata raporuna ekleyin." +"kullanın ve bu günlüğü hata raporuna ekleyin." #: plinth/templates/app-logs.html:26 msgid "" @@ -10792,10 +10781,8 @@ msgid "Clear all tags" msgstr "Tüm etiketleri temizle" #: plinth/templates/toolbar.html:39 plinth/templates/toolbar.html:40 -#, fuzzy -#| msgid "Logs" msgid "View Logs" -msgstr "Günlükler" +msgstr "Günlükleri görüntüle" #: plinth/templates/toolbar.html:46 plinth/templates/toolbar.html:47 msgid "Backup" diff --git a/plinth/locale/uk/LC_MESSAGES/django.po b/plinth/locale/uk/LC_MESSAGES/django.po index 9ec68ec56..5bd154c7f 100644 --- a/plinth/locale/uk/LC_MESSAGES/django.po +++ b/plinth/locale/uk/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-09-10 04:01+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" +"PO-Revision-Date: 2025-09-24 03:02+0000\n" "Last-Translator: Максим Горпиніч \n" "Language-Team: Ukrainian \n" @@ -34,27 +34,27 @@ msgstr "Контейнер {container_name} працює" msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "Сервіс {service_name} запущено" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "Слухати на {kind} порт {listen_address}:{port}" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "Слухати на {kind} порт {port}" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "Під'єднання до {host}:{port}" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "Неможливо підключитись до {host}:{port}" @@ -219,12 +219,12 @@ msgid "Backups allows creating and managing backup archives." msgstr "" "Резервне копіювання дозволяє створювати та керувати резервними архівами." -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "Резервні копії" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." @@ -233,19 +233,19 @@ msgstr "" "Для резервних копій надається перевага віддаленому зашифрованому " "розташуванню або зовнішньому знімному диску." -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "Дозволити планування резервних копій" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "Перейти в {app_name}" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " @@ -254,7 +254,7 @@ msgstr "" "Невдале заплановане резервне копіювання. Минулі {error_count} спроб не були " "успішними. Остання помилка: {error_message}" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "Помилка під час резервного копіювання" @@ -1651,55 +1651,57 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Ця програма також показує журнали для сервісів " +"{box_name}." #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Діагностика" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "пропущено" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "пройдено" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "невдало" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "помилка" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "попередження" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "МіБ" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "ҐіБ" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" "Ви повинні вимкнути деякі застосунки, щоб зменшити використання памʼяті." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "Не слід встановлювати нові застосунки в цій системі." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1708,24 +1710,24 @@ msgstr "" "У системі бракує памʼяті: {percent_used}% використано, {memory_available} " "{memory_available_unit} вільно. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Мало памʼяті" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "Виконується діагностика" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "Знайдено {issue_count} проблеми під час звичайних перевірок." -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "Результати діагностики" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "Перейти до результатів діагностики" @@ -9107,7 +9109,7 @@ msgstr "" msgid "Distribution Update" msgstr "Оновлення розповсюдження" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "Перевірте наявність пакетів" @@ -9522,7 +9524,7 @@ msgstr "Не вдалося розпочати оновлення." msgid "Frequent feature updates activated." msgstr "Оновлення частих можливостей активовано." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9533,7 +9535,7 @@ msgstr "" "застосунки також вимагають, щоб обліковий запис був частиною групи, щоб " "отримати авторизований доступ до застосунку." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9545,25 +9547,25 @@ msgstr "" "користувачі з групи admin можуть змінювати застосунки або системні " "налаштування." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Користувачі і групи" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Доступ до всіх сервісів і налаштувань системи" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Перевірка запису LDAP \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Перевірте nslcd конфігурацію \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Перевірте nsswitch конфігурацію \"{database}\"" @@ -10483,47 +10485,34 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the bug tracker so we can fix it. Also, please attach " "the logs to the bug report." msgstr "" -"Це внутрішня помилка і не те, що Ви спричинили чи можете виправити. Будь " -"ласка, повідомте про помилку на сторінці відстеження недоліків, щоб ми могли " -"виправити її. Також, будь ласка, прикріпіть до звіту про недолік журнал стану." +"Це внутрішня помилка, яку ви не спричинили і не можете виправити. Будь " +"ласка, повідомте про помилку на bug tracker, щоб ми могли її " +"виправити. Також, будь ласка, додайте журнали " +"до звіту про помилку." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "Встановлення" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Це останні %(num_lines)s рядків із журналу стану для цього вебінтерфейсу. " -"Якщо Ви бажаєте повідомити про недолік, будь ласка використовуйте сторінку " -"відстеження недоліків і прикріпіть цей журнал стану до звіту " -"про недолік." +"Це останні рядки журналів для служб, пов'язаних із цією програмою. Якщо ви " +"хочете повідомити про помилку, скористайтеся системою відстеження " +"помилок і додайте цей журнал до звіту про помилку." #: plinth/templates/app-logs.html:26 msgid "" @@ -10783,10 +10772,8 @@ msgid "Clear all tags" msgstr "Очистити всі теги" #: plinth/templates/toolbar.html:39 plinth/templates/toolbar.html:40 -#, fuzzy -#| msgid "Logs" msgid "View Logs" -msgstr "Журнали" +msgstr "Переглянути журнали" #: plinth/templates/toolbar.html:46 plinth/templates/toolbar.html:47 msgid "Backup" diff --git a/plinth/locale/vi/LC_MESSAGES/django.po b/plinth/locale/vi/LC_MESSAGES/django.po index 77014a3af..d26da84c7 100644 --- a/plinth/locale/vi/LC_MESSAGES/django.po +++ b/plinth/locale/vi/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2021-07-28 08:34+0000\n" "Last-Translator: bruh \n" "Language-Team: Vietnamese \n" "Language-Team: Chinese (Simplified Han script) bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the bug tracker so we can fix it. Also, please attach " "the logs to the bug report." msgstr "" -"这是一个内部错误,不是你造成的或可以修复。 请报告到 bug 追踪器 上这样我" -"们就可以修复该错误。同时请附加状态日志到 " -"Bug 报告里。" +"这是一个内部错误,不是你造成的或可以修复的。 请报告到 bug 追踪器 上,让我" +"们可以修复该错误。同时请附加 日志到 Bug 报告里。" #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "安装" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"这些是此 Web 界面状态日志的最后 %(num_lines)s 行。如果想报 Bug,请通过 bug 追踪器 " -"并附上此状态日志。" +"并附上此日志。" #: plinth/templates/app-logs.html:26 msgid "" diff --git a/plinth/locale/zh_Hant/LC_MESSAGES/django.po b/plinth/locale/zh_Hant/LC_MESSAGES/django.po index c962ac322..0f7bfd144 100644 --- a/plinth/locale/zh_Hant/LC_MESSAGES/django.po +++ b/plinth/locale/zh_Hant/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-02-07 12:01+0000\n" "Last-Translator: pesder \n" "Language-Team: Chinese (Traditional Han script) None: """Create components for the app.""" super().__init__() diff --git a/plinth/modules/backups/privileged.py b/plinth/modules/backups/privileged.py index ced04bc8c..a2276ead4 100644 --- a/plinth/modules/backups/privileged.py +++ b/plinth/modules/backups/privileged.py @@ -118,8 +118,8 @@ def reraise_known_errors(privileged_func): def _reraise_known_errors(err): """Look whether the caught error is known and reraise it accordingly""" - stdout = (getattr(err, 'stdout') or b'').decode() - stderr = (getattr(err, 'stderr') or b'').decode() + stdout = (getattr(err, 'stdout', b'') or b'').decode() + stderr = (getattr(err, 'stderr', b'') or b'').decode() caught_error = str((err, err.args, stdout, stderr)) for known_error in KNOWN_ERRORS: for error in known_error['errors']: @@ -194,7 +194,7 @@ def _is_mounted(mountpoint): cmd = ['mountpoint', '-q', mountpoint] # mountpoint exits with status non-zero if it didn't find a mountpoint try: - subprocess.run(cmd, check=True) + action_utils.run(cmd, check=True) return True except subprocess.CalledProcessError: return False @@ -244,8 +244,7 @@ def init(path: str, encryption: str, @privileged def info(path: str, encryption_passphrase: secret_str | None = None) -> dict: """Show repository information.""" - process = _run(['borg', 'info', '--json', path], encryption_passphrase, - stdout=subprocess.PIPE) + process = _run(['borg', 'info', '--json', path], encryption_passphrase) return json.loads(process.stdout.decode()) @@ -255,7 +254,7 @@ def list_repo(path: str, encryption_passphrase: secret_str | None = None) -> dict: """List repository contents.""" process = _run(['borg', 'list', '--json', '--format="{comment}"', path], - encryption_passphrase, stdout=subprocess.PIPE) + encryption_passphrase) return json.loads(process.stdout.decode()) @@ -281,7 +280,7 @@ def remove_uploaded_archive(file_path: str): def _get_borg_version(): """Return the version of borgbackup.""" - process = _run(['borg', '--version'], stdout=subprocess.PIPE) + process = _run(['borg', '--version']) return process.stdout.decode().split()[1] # Example: "borg 1.1.9" @@ -322,10 +321,7 @@ def _extract(archive_path, destination, encryption_passphrase, locations=None): try: os.chdir(os.path.expanduser(destination)) - # TODO: with python 3.7 use subprocess.run with the 'capture_output' - # argument - process = _run(borg_call, encryption_passphrase, check=False, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + process = _run(borg_call, encryption_passphrase, check=False) if process.returncode != 0: error = process.stderr.decode() # Don't fail on the borg error when no files were matched @@ -350,8 +346,7 @@ def export_tar(path: str, encryption_passphrase: secret_str | None = None): def _read_archive_file(archive, filepath, encryption_passphrase): """Read the content of a file inside an archive.""" borg_call = ['borg', 'extract', archive, filepath, '--stdout'] - return _run(borg_call, encryption_passphrase, - stdout=subprocess.PIPE).stdout.decode() + return _run(borg_call, encryption_passphrase).stdout.decode() @reraise_known_errors @@ -365,8 +360,7 @@ def get_archive_apps( 'borg', 'list', path, manifest_folder, '--format', '{path}{NEWLINE}' ] try: - borg_process = _run(borg_call, encryption_passphrase, - stdout=subprocess.PIPE) + borg_process = _run(borg_call, encryption_passphrase) manifest_path = borg_process.stdout.decode().strip() except subprocess.CalledProcessError: raise RuntimeError('Borg exited unsuccessfully') diff --git a/plinth/modules/backups/repository.py b/plinth/modules/backups/repository.py index ec1c642cb..ec40a3ed1 100644 --- a/plinth/modules/backups/repository.py +++ b/plinth/modules/backups/repository.py @@ -237,7 +237,7 @@ class RootBorgRepository(BaseBorgRepository): UUID = 'root' PATH = '/var/lib/freedombox/borgbackup' - storage_type = 'root' + storage_type = 'root' # type: ignore name = format_lazy(_('{box_name} storage'), box_name=_(cfg.box_name)) borg_path = PATH sort_order = 10 @@ -252,7 +252,7 @@ class RootBorgRepository(BaseBorgRepository): class BorgRepository(BaseBorgRepository): """General Borg repository implementation.""" known_credentials = ['encryption_passphrase'] - storage_type = 'disk' + storage_type = 'disk' # type: ignore sort_order = 20 flags = {'removable': True} @@ -273,7 +273,7 @@ class SshBorgRepository(BaseBorgRepository): known_credentials = [ 'ssh_keyfile', 'ssh_password', 'encryption_passphrase' ] - storage_type = 'ssh' + storage_type = 'ssh' # type: ignore sort_order = 30 flags = {'removable': True, 'mountable': True} diff --git a/plinth/modules/bepasty/privileged.py b/plinth/modules/bepasty/privileged.py index 7e6a186b2..11a09c5e6 100644 --- a/plinth/modules/bepasty/privileged.py +++ b/plinth/modules/bepasty/privileged.py @@ -10,7 +10,6 @@ import pwd import secrets import shutil import string -import subprocess import augeas @@ -71,13 +70,13 @@ def setup(domain_name: str): try: grp.getgrnam('bepasty') except KeyError: - subprocess.run(['addgroup', '--system', 'bepasty'], check=True) + action_utils.run(['addgroup', '--system', 'bepasty'], check=True) # Create bepasty user if needed. try: pwd.getpwnam('bepasty') except KeyError: - subprocess.run([ + action_utils.run([ 'adduser', '--system', '--ingroup', 'bepasty', '--home', '/var/lib/bepasty', '--gecos', 'bepasty file sharing', 'bepasty' ], check=True) @@ -174,5 +173,5 @@ def uninstall(): """Remove bepasty user, group and data.""" shutil.rmtree(DATA_DIR, ignore_errors=True) CONF_FILE.unlink(missing_ok=True) - subprocess.run(['deluser', 'bepasty'], check=False) - subprocess.run(['delgroup', 'bepasty'], check=False) + action_utils.run(['deluser', 'bepasty'], check=False) + action_utils.run(['delgroup', 'bepasty'], check=False) diff --git a/plinth/modules/calibre/privileged.py b/plinth/modules/calibre/privileged.py index 962f2f575..75829b569 100644 --- a/plinth/modules/calibre/privileged.py +++ b/plinth/modules/calibre/privileged.py @@ -3,7 +3,6 @@ import pathlib import shutil -import subprocess from plinth import action_utils from plinth.actions import privileged @@ -28,9 +27,9 @@ def create_library(name: str): calibre.validate_library_name(name) library = LIBRARIES_PATH / name library.mkdir(mode=0o755) # Raise exception if already exists - subprocess.call( + action_utils.run( ['calibredb', '--with-library', library, 'list_categories'], - stdout=subprocess.DEVNULL) + check=False) # Force systemd StateDirectory= logic to assign proper ownership to the # DynamicUser= diff --git a/plinth/modules/calibre/tests/test_privileged.py b/plinth/modules/calibre/tests/test_privileged.py index 6b41173f8..346897e25 100644 --- a/plinth/modules/calibre/tests/test_privileged.py +++ b/plinth/modules/calibre/tests/test_privileged.py @@ -29,9 +29,8 @@ def fixture_patch(): path = pathlib.Path(args[0][2]) / 'metadata.db' path.touch() - with patch('subprocess.call') as subprocess_call, \ - patch('subprocess.run'), patch('shutil.chown'): - subprocess_call.side_effect = side_effect + with patch('subprocess.run') as subprocess_run, patch('shutil.chown'): + subprocess_run.side_effect = side_effect yield diff --git a/plinth/modules/config/__init__.py b/plinth/modules/config/__init__.py index 75e1c6b1b..c0b82f459 100644 --- a/plinth/modules/config/__init__.py +++ b/plinth/modules/config/__init__.py @@ -109,23 +109,23 @@ def home_page_url2scid(url: str | None): def _home_page_scid2url(shortcut_id: str) -> str | None: """Return the url for the given home page shortcut ID.""" + url: str | None = '/plinth/' if shortcut_id == 'plinth': - url = '/plinth/' + pass elif shortcut_id == 'apache-default': url = None elif shortcut_id.startswith('uws-'): user = shortcut_id[4:] if user in get_users_with_website(): url = uws_url_of_user(user) - else: - url = None else: shortcuts = frontpage.Shortcut.list() aux = [ shortcut.url for shortcut in shortcuts if shortcut_id == shortcut.component_id ] - url = aux[0] if 1 == len(aux) else None + if 1 == len(aux): + url = aux[0] return url diff --git a/plinth/modules/config/tests/test_config.py b/plinth/modules/config/tests/test_config.py index dd7d192d1..5453fed1a 100644 --- a/plinth/modules/config/tests/test_config.py +++ b/plinth/modules/config/tests/test_config.py @@ -4,11 +4,13 @@ Tests for config module. """ import os -from unittest.mock import MagicMock, patch +import pathlib +from unittest.mock import Mock, patch import pytest from plinth import __main__ as plinth_main +from plinth import utils from plinth.modules.apache import uws_directory_of_user, uws_url_of_user from plinth.modules.config import (_home_page_scid2url, change_home_page, get_home_page, home_page_url2scid) @@ -61,23 +63,14 @@ def test_homepage_mapping_skip_ci(): # AC: Return None if it doesn't: os.rmdir(uws_directory) - assert _home_page_scid2url(uws_scid) is None + assert _home_page_scid2url(uws_scid) == '/plinth/' -class Dict2Obj: - """Mock object made out of any dict.""" - - def __init__(self, a_dict): - self.__dict__ = a_dict - - -@patch('plinth.frontpage.Shortcut.list', - MagicMock(return_value=[ - Dict2Obj({ - 'url': 'url/for/' + id, - 'component_id': id - }) for id in ('a', 'b') - ])) +@patch( + 'plinth.frontpage.Shortcut.list', + Mock(return_value=[ + Mock(url='url/for/' + id, component_id=id) for id in ('a', 'b') + ])) @pytest.mark.usefixtures('needs_root') def test_homepage_field(): """Test homepage changes. @@ -104,23 +97,14 @@ def test_homepage_field(): Currently they share the same test case. - Search for another valid user apart from fbx. """ - user = 'fbx' + user = 'test_' + utils.random_string(size=12) uws_directory = uws_directory_of_user(user) uws_url = uws_url_of_user(user) uws_scid = home_page_url2scid(uws_url) default_home_page = 'plinth' original_home_page = get_home_page() or default_home_page - - # Check test's preconditions: - if original_home_page not in (default_home_page, None): - reason = "Unexpected home page {}.".format(original_home_page) - pytest.skip(reason) - - if os.path.exists(uws_directory): - # Don't blindly remove a pre-existing directory. Just skip the test. - reason = "UWS directory {} exists already.".format(uws_directory) - pytest.skip(reason) + change_home_page(default_home_page) # Set to known value explicitly # AC: invalid changes fall back to default: for scid in ('uws-unexisting', uws_scid, 'missing_app'): @@ -128,12 +112,7 @@ def test_homepage_field(): assert get_home_page() == default_home_page # AC: valid changes actually happen: - try: - os.mkdir(uws_directory) - except Exception: - reason = "Needs access to ~/ directory. " \ - + "CI sandboxed workspace doesn't provide it." - pytest.skip(reason) + pathlib.Path(uws_directory).mkdir(parents=True) for scid in ('b', 'a', uws_scid, 'apache-default', 'plinth'): change_home_page(scid) assert get_home_page() == scid diff --git a/plinth/modules/datetime/privileged.py b/plinth/modules/datetime/privileged.py index 45790405e..666f79245 100644 --- a/plinth/modules/datetime/privileged.py +++ b/plinth/modules/datetime/privileged.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Set time zone with timedatectl.""" -import subprocess - +from plinth import action_utils from plinth.actions import privileged @@ -10,4 +9,4 @@ from plinth.actions import privileged def set_timezone(timezone: str): """Set time zone with timedatectl.""" command = ['timedatectl', 'set-timezone', timezone] - subprocess.run(command, stdout=subprocess.DEVNULL, check=True) + action_utils.run(command, check=True) diff --git a/plinth/modules/diagnostics/__init__.py b/plinth/modules/diagnostics/__init__.py index 42b5652b6..0b3d21d65 100644 --- a/plinth/modules/diagnostics/__init__.py +++ b/plinth/modules/diagnostics/__init__.py @@ -86,8 +86,9 @@ class DiagnosticsApp(app_module.App): # Check periodically for low RAM space glib.schedule(3600, _warn_about_low_ram_space) - # Run diagnostics once a day - glib.schedule(24 * 3600, _daily_diagnostics_run, in_thread=False) + # Run diagnostics once a day or every 30 minutes in development mode. + glib.schedule(24 * 3600, _daily_diagnostics_run, in_thread=False, + develop_interval=1800) def setup(self, old_version): """Install and configure the app.""" diff --git a/plinth/modules/ejabberd/privileged.py b/plinth/modules/ejabberd/privileged.py index a682a073e..85810d4cf 100644 --- a/plinth/modules/ejabberd/privileged.py +++ b/plinth/modules/ejabberd/privileged.py @@ -89,7 +89,7 @@ def setup(domain_name: str): _upgrade_config(domain_name) try: - subprocess.check_output(['ejabberdctl', 'restart']) + action_utils.run(['ejabberdctl', 'restart'], check=True) except subprocess.CalledProcessError as err: logger.warn('Failed to restart ejabberd with new configuration: %s', err) @@ -145,11 +145,11 @@ def pre_change_hostname(old_hostname: str, new_hostname: str): logger.info('ejabberdctl not found') return - subprocess.call(['ejabberdctl', 'backup', EJABBERD_BACKUP]) - subprocess.check_output([ + action_utils.run(['ejabberdctl', 'backup', EJABBERD_BACKUP], check=False) + action_utils.run([ 'ejabberdctl', 'mnesia-change-nodename', 'ejabberd@' + old_hostname, 'ejabberd@' + new_hostname, EJABBERD_BACKUP, EJABBERD_BACKUP_NEW - ]) + ], check=True) os.remove(EJABBERD_BACKUP) @@ -160,20 +160,20 @@ def change_hostname(): return action_utils.service_stop('ejabberd') - subprocess.call(['pkill', '-u', 'ejabberd']) + action_utils.run(['pkill', '-u', 'ejabberd'], check=False) # Make sure there aren't files in the Mnesia spool dir os.makedirs('/var/lib/ejabberd/oldfiles', exist_ok=True) - subprocess.call('mv /var/lib/ejabberd/*.* /var/lib/ejabberd/oldfiles/', - shell=True) + action_utils.run('mv /var/lib/ejabberd/*.* /var/lib/ejabberd/oldfiles/', + shell=True, check=False) action_utils.service_start('ejabberd') # restore backup database if os.path.exists(EJABBERD_BACKUP_NEW): try: - subprocess.check_output( - ['ejabberdctl', 'restore', EJABBERD_BACKUP_NEW]) + action_utils.run(['ejabberdctl', 'restore', EJABBERD_BACKUP_NEW], + check=True) os.remove(EJABBERD_BACKUP_NEW) except subprocess.CalledProcessError as err: logger.error('Failed to restore ejabberd backup database: %s', err) @@ -278,7 +278,7 @@ def mam(command: str) -> bool | None: yaml.dump(conf, file_handle) if action_utils.service_is_running('ejabberd'): - subprocess.call(['ejabberdctl', 'reload_config']) + action_utils.run(['ejabberdctl', 'reload_config'], check=False) return None @@ -359,14 +359,14 @@ def configure_turn(turn_server_config: dict[str, Any], managed: bool): Path(EJABBERD_MANAGED_COTURN).unlink(missing_ok=True) if action_utils.service_is_running('ejabberd'): - subprocess.call(['ejabberdctl', 'reload_config']) + action_utils.run(['ejabberdctl', 'reload_config'], check=False) def _get_version(): """Get the current ejabberd version.""" try: - output = subprocess.check_output(['ejabberdctl', - 'status']).decode('utf-8') + output = action_utils.run(['ejabberdctl', 'status'], + check=True).stdout.decode('utf-8') except subprocess.CalledProcessError: return None diff --git a/plinth/modules/email/postfix.py b/plinth/modules/email/postfix.py index 1a04731cb..f0700546c 100644 --- a/plinth/modules/email/postfix.py +++ b/plinth/modules/email/postfix.py @@ -11,6 +11,8 @@ import re import subprocess from dataclasses import dataclass +from plinth import action_utils + @dataclass class Service: # NOQA, pylint: disable=too-many-instance-attributes @@ -109,7 +111,7 @@ def _run(args): Raise a RuntimeError on non-zero exit codes. """ try: - result = subprocess.run(args, check=True, capture_output=True) + result = action_utils.run(args, check=True) return result.stdout.decode() except subprocess.SubprocessError as subprocess_error: raise RuntimeError('Subprocess failed') from subprocess_error diff --git a/plinth/modules/email/privileged/dkim.py b/plinth/modules/email/privileged/dkim.py index d3b9273c6..3ffec7aba 100644 --- a/plinth/modules/email/privileged/dkim.py +++ b/plinth/modules/email/privileged/dkim.py @@ -7,8 +7,8 @@ See: https://rspamd.com/doc/modules/dkim_signing.html import pathlib import re import shutil -import subprocess +from plinth import action_utils from plinth.actions import privileged from plinth.privileged import service as service_privileged @@ -30,9 +30,9 @@ def get_dkim_public_key(domain: str) -> str: """Privileged action to get the public key from DKIM key.""" _validate_domain_name(domain) key_file = _keys_dir / f'{domain}.dkim.key' - output = subprocess.check_output( + output = action_utils.run( ['openssl', 'rsa', '-in', - str(key_file), '-pubout'], stderr=subprocess.DEVNULL) + str(key_file), '-pubout'], check=True).stdout return ''.join(output.decode().splitlines()[1:-1]) @@ -54,7 +54,7 @@ def setup_dkim(domain: str): # Ed25519 is widely *not* accepted as of 2022-01. See: # https://serverfault.com/questions/1023674 - subprocess.run([ + action_utils.run([ 'rspamadm', 'dkim_keygen', '-t', 'rsa', '-b', '2048', '-s', 'dkim', '-d', domain, '-k', (str(key_file)) ], check=True) diff --git a/plinth/modules/email/privileged/home.py b/plinth/modules/email/privileged/home.py index 544643dc3..05a9f974e 100644 --- a/plinth/modules/email/privileged/home.py +++ b/plinth/modules/email/privileged/home.py @@ -6,8 +6,7 @@ See: https://doc.dovecot.org/configuration_manual/authentication/user_databases_userdb/ """ -import subprocess - +from plinth import action_utils from plinth.actions import privileged @@ -19,4 +18,4 @@ def setup_home(): Dovecot creates new directories with the same permissions as the parent directory. Ensure that 'others' can't access /var/mail/. """ - subprocess.run(['chmod', 'o-rwx', '/var/mail'], check=True) + action_utils.run(['chmod', 'o-rwx', '/var/mail'], check=True) diff --git a/plinth/modules/email/privileged/spam.py b/plinth/modules/email/privileged/spam.py index c42331393..6205f2d7f 100644 --- a/plinth/modules/email/privileged/spam.py +++ b/plinth/modules/email/privileged/spam.py @@ -10,8 +10,8 @@ For testing DKIM signatures: https://www.mail-tester.com/ import pathlib import re -import subprocess +from plinth import action_utils from plinth.actions import privileged from plinth.modules.email import postfix @@ -31,10 +31,11 @@ def setup_spam(): def _compile_sieve(): """Compile all .sieve script to binary format for performance.""" - sieve_dirs = ['/etc/dovecot/freedombox-sieve-after/', - '/etc/dovecot/freedombox-sieve'] + sieve_dirs = [ + '/etc/dovecot/freedombox-sieve-after/', '/etc/dovecot/freedombox-sieve' + ] for sieve_dir in sieve_dirs: - subprocess.run(['sievec', sieve_dir], check=True) + action_utils.run(['sievec', sieve_dir], check=True) def _setup_rspamd(): diff --git a/plinth/modules/firewall/privileged.py b/plinth/modules/firewall/privileged.py index 605c7e948..54ccb5e93 100644 --- a/plinth/modules/firewall/privileged.py +++ b/plinth/modules/firewall/privileged.py @@ -36,10 +36,10 @@ def _flush_iptables_rules(): iptables_rules += rule_template.format(table=table) ip6tables_rules += rule_template.format(table=table) - subprocess.run(['iptables-restore'], input=iptables_rules.encode(), - check=True) - subprocess.run(['ip6tables-restore'], input=iptables_rules.encode(), - check=True) + action_utils.run(['iptables-restore'], input=iptables_rules.encode(), + check=True) + action_utils.run(['ip6tables-restore'], input=iptables_rules.encode(), + check=True) def set_firewall_backend(backend): @@ -67,8 +67,7 @@ def set_firewall_backend(backend): def _run_firewall_cmd(args): """Run firewall-cmd command, discard output and check return value.""" - subprocess.run(['firewall-cmd'] + args, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) + action_utils.run(['firewall-cmd'] + args, check=True) def _setup_local_service_protection(): @@ -160,9 +159,8 @@ def _setup_inter_zone_forwarding(): def setup(): """Perform basic firewalld setup.""" action_utils.service_enable('firewalld') - subprocess.run(['firewall-cmd', '--set-default-zone=external'], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - check=True) + action_utils.run(['firewall-cmd', '--set-default-zone=external'], + check=True) set_firewall_backend('nftables') _setup_local_service_protection() @@ -176,7 +174,8 @@ def get_config() -> FirewallConfig: config: FirewallConfig = {} # Get the default zone. - output = subprocess.check_output(['firewall-cmd', '--get-default-zone']) + output = action_utils.run(['firewall-cmd', '--get-default-zone'], + check=True).stdout config['default_zone'] = output.decode().strip() # Load Augeas lens. @@ -191,8 +190,9 @@ def get_config() -> FirewallConfig: config['backend'] = aug.get('FirewallBackend') # Get the list of direct passthroughs. - output = subprocess.check_output( - ['firewall-cmd', '--direct', '--get-all-passthroughs']) + output = action_utils.run( + ['firewall-cmd', '--direct', '--get-all-passthroughs'], + check=True).stdout config['passthroughs'] = output.decode().strip().split('\n') return config diff --git a/plinth/modules/gitweb/__init__.py b/plinth/modules/gitweb/__init__.py index 3dcd79829..00c751e7a 100644 --- a/plinth/modules/gitweb/__init__.py +++ b/plinth/modules/gitweb/__init__.py @@ -34,7 +34,7 @@ class GitwebApp(app_module.App): app_id = 'gitweb' - _version = 3 + _version = 4 def __init__(self) -> None: """Create components for the app.""" diff --git a/plinth/modules/gitweb/privileged.py b/plinth/modules/gitweb/privileged.py index 25e50d73b..332d42af5 100644 --- a/plinth/modules/gitweb/privileged.py +++ b/plinth/modules/gitweb/privileged.py @@ -4,14 +4,16 @@ import configparser import logging import os +import pathlib import re import shutil import subprocess import time from typing import Any +from urllib import parse from plinth import action_utils -from plinth.actions import privileged +from plinth.actions import privileged, secret_str from plinth.modules.gitweb.forms import RepositoryValidator, get_name_from_url from plinth.modules.gitweb.manifest import GIT_REPO_PATH, REPO_DIR_OWNER @@ -27,7 +29,7 @@ def validate_repo_name(name: str) -> str: return name -def validate_repo_url(url: str) -> str: +def validate_repo_url(url: secret_str) -> secret_str: """Validate a repository URL.""" RepositoryValidator(input_should_be='url')(url) return url @@ -35,33 +37,58 @@ def validate_repo_url(url: str) -> str: @privileged def setup(): - """Disable default Apache2 Gitweb configuration.""" + """Configure Gitweb module.""" + # Disable default Apache2 Gitweb configuration. action_utils.webserver_disable('gitweb') - if not _get_global_default_branch(): - _set_global_default_branch('main') + + # Configure Git client. + if not _get_git_global_config('init.defaultBranch'): + _set_git_global_config('init.defaultBranch', 'main') + if not _get_git_global_config('credential.helper'): + _set_git_global_config('credential.helper', 'cache') -def _get_global_default_branch(): - """Get globally configured default branch name.""" +def _get_git_global_config(key: str) -> str | None: + """Return a value from Git global configuration.""" try: - default_branch = subprocess.check_output( - ['git', 'config', '--global', '--get', - 'init.defaultBranch']).decode().strip() + value = action_utils.run(['git', 'config', '--global', '--get', key], + check=True).stdout.decode().strip() except subprocess.CalledProcessError as exception: - if exception.returncode == 1: # Default branch not configured + if exception.returncode == 1: # Configuration option doesn't exist return None raise - return default_branch + return value -def _set_global_default_branch(name): - """Configure default branch name globally.""" - subprocess.check_call( - ['git', 'config', '--global', 'init.defaultBranch', name]) +def _set_git_global_config(key: str, value: str) -> None: + """Set a Git global configuration value.""" + action_utils.run(['git', 'config', '--global', key, value], check=True) -def _clone_with_progress_report(url, repo_dir): +def _setup_git_credentials(url: secret_str) -> str: + """Set up git credential helper and return URL without credentials.""" + url_parts = parse.urlsplit(url) + safe_netloc = url_parts.netloc.split('@')[-1] + safe_url = url_parts._replace(netloc=safe_netloc).geturl() + username = url_parts.username or '' + password = url_parts.password or '' + + if username or password: + # Feed credentials to Git credential helper + input = (f'protocol={url_parts.scheme}\n' + f'host={safe_netloc}\n' + f'username={username}\n' + f'password={password}\n\n') + env = dict(os.environ, GIT_TERMINAL_PROMPT='0') + action_utils.run(['git', 'credential', 'approve'], + input=input.encode(), stdout=subprocess.DEVNULL, + check=True, env=env) + + return safe_url + + +def _clone_with_progress_report(url: secret_str, repo_dir: pathlib.Path): """Clone a repository and write progress info to the file.""" starttime = time.time() status_file = repo_dir / 'clone_progress' @@ -70,9 +97,12 @@ def _clone_with_progress_report(url, repo_dir): env = dict(os.environ, GIT_TERMINAL_PROMPT='0', LC_ALL='C', GIT_HTTP_LOW_SPEED_LIMIT='100', GIT_HTTP_LOW_SPEED_TIME='60') + safe_url = _setup_git_credentials(url) + logger.info(f'Cloning Git repository {safe_url} ...') proc = subprocess.Popen( - ['git', 'clone', '--bare', '--progress', url, + ['git', 'clone', '--bare', '--progress', safe_url, str(repo_temp_dir)], stderr=subprocess.PIPE, text=True, env=env) + assert proc.stderr is not None # write clone progress to the file errors = [] @@ -108,7 +138,7 @@ def _clone_with_progress_report(url, repo_dir): raise RuntimeError('Git repository cloning failed.', errors) -def _prepare_clone_repo(url: str, is_private: bool): +def _prepare_clone_repo(url: secret_str, is_private: bool): """Prepare cloning a repository.""" repo_name = get_name_from_url(url) if not repo_name.endswith('.git'): @@ -150,7 +180,8 @@ def _clone_status_line_to_percent(line): return None -def _clone_repo(url: str, description: str, owner: str, keep_ownership: bool): +def _clone_repo(url: secret_str, description: str, owner: str, + keep_ownership: bool): """Clone a repository.""" repo = get_name_from_url(url) if not repo.endswith('.git'): @@ -166,9 +197,9 @@ def _clone_repo(url: str, description: str, owner: str, keep_ownership: bool): shutil.rmtree(repo_temp_path) if not keep_ownership: - subprocess.check_call( + action_utils.run( ['chown', '-R', f'{REPO_DIR_OWNER}:{REPO_DIR_OWNER}', repo], - cwd=GIT_REPO_PATH) + cwd=GIT_REPO_PATH, check=True) _set_repo_description(repo, description) _set_repo_owner(repo, owner) @@ -178,12 +209,12 @@ def _create_repo(repo: str, description: str, owner: str, is_private: bool, keep_ownership: bool): """Create an empty repository.""" try: - subprocess.check_call(['git', 'init', '-q', '--bare', repo], - cwd=GIT_REPO_PATH) + action_utils.run(['git', 'init', '-q', '--bare', repo], + cwd=GIT_REPO_PATH, check=True) if not keep_ownership: - subprocess.check_call( + action_utils.run( ['chown', '-R', f'{REPO_DIR_OWNER}:{REPO_DIR_OWNER}', repo], - cwd=GIT_REPO_PATH) + cwd=GIT_REPO_PATH, check=True) _set_repo_description(repo, description) _set_repo_owner(repo, owner) if is_private: @@ -202,8 +233,7 @@ def _get_default_branch(repo): return action_utils.run_as_user( ['git', '-C', str(repo_path), 'symbolic-ref', '--short', 'HEAD'], - username=REPO_DIR_OWNER, check=True, - stdout=subprocess.PIPE).stdout.decode().strip() + username=REPO_DIR_OWNER, check=True).stdout.decode().strip() def _get_repo_description(repo): @@ -271,8 +301,7 @@ def _get_branches(repo): """Return list of the branches in the repository.""" process = action_utils.run_as_user( ['git', '-C', repo, 'branch', '--format=%(refname:short)'], - cwd=GIT_REPO_PATH, username=REPO_DIR_OWNER, check=True, - stdout=subprocess.PIPE) + cwd=GIT_REPO_PATH, username=REPO_DIR_OWNER, check=True) return process.stdout.decode().strip().split() @@ -345,7 +374,7 @@ def repo_info(name: str) -> dict[str, str]: @privileged -def create_repo(url: str | None = None, name: str | None = None, +def create_repo(url: secret_str | None = None, name: str | None = None, description: str = '', owner: str = '', keep_ownership: bool = False, is_private: bool = False, skip_prepare: bool = False, prepare_only: bool = False): @@ -367,13 +396,14 @@ def create_repo(url: str | None = None, name: str | None = None, @privileged -def repo_exists(url: str) -> bool: +def repo_exists(url: secret_str) -> bool: """Return whether remote repository exists.""" url = validate_repo_url(url) + safe_url = _setup_git_credentials(url) env = dict(os.environ, GIT_TERMINAL_PROMPT='0') try: - subprocess.check_call(['git', 'ls-remote', url, 'HEAD'], timeout=10, - env=env) + action_utils.run(['git', 'ls-remote', safe_url, 'HEAD'], timeout=10, + env=env, check=True) return True except subprocess.CalledProcessError: return False diff --git a/plinth/modules/gitweb/tests/test_privileged.py b/plinth/modules/gitweb/tests/test_privileged.py index f9ddf6d12..cc884c501 100644 --- a/plinth/modules/gitweb/tests/test_privileged.py +++ b/plinth/modules/gitweb/tests/test_privileged.py @@ -2,6 +2,8 @@ """Test module for gitweb module operations.""" import pathlib +import subprocess +from unittest.mock import call, patch import pytest from django.forms import ValidationError @@ -121,3 +123,26 @@ def test_action_create_repo_with_invalid_urls(url): with pytest.raises(ValidationError): privileged.create_repo(url=url, description='', owner='', keep_ownership=True) + + +@patch('plinth.action_utils.run') +def test_setup_git_creentials(run): + """Test that setting up git credentials works.""" + url = 'https://user:pass@host.example/path?key=value' + safe_url = privileged._setup_git_credentials(url) + assert safe_url == 'https://host.example/path?key=value' + + input_ = b'protocol=https\nhost=host.example\nusername=user\n' \ + b'password=pass\n\n' + env = run.mock_calls[0].kwargs.pop('env') + assert env['GIT_TERMINAL_PROMPT'] == '0' + assert run.mock_calls == [ + call(['git', 'credential', 'approve'], input=input_, + stdout=subprocess.DEVNULL, check=True) + ] + + run.reset_mock() + url = 'https://host2.example/path?key=value' + safe_url = privileged._setup_git_credentials(url) + assert safe_url == 'https://host2.example/path?key=value' + run.assert_not_called() diff --git a/plinth/modules/ikiwiki/privileged.py b/plinth/modules/ikiwiki/privileged.py index fda1eca9b..92b3bb086 100644 --- a/plinth/modules/ikiwiki/privileged.py +++ b/plinth/modules/ikiwiki/privileged.py @@ -5,8 +5,8 @@ import os import pathlib import re import shutil -import subprocess +from plinth import action_utils from plinth.actions import privileged, secret_str SETUP_WIKI = '/etc/ikiwiki/plinth-wiki.setup' @@ -61,10 +61,9 @@ def create_wiki(wiki_name: str, admin_name: str, admin_password: secret_str): """Create a wiki.""" pw_bytes = admin_password.encode() input_ = pw_bytes + b'\n' + pw_bytes - subprocess.run(['ikiwiki', '-setup', SETUP_WIKI, wiki_name, admin_name], - stdout=subprocess.PIPE, input=input_, - stderr=subprocess.PIPE, - env=dict(os.environ, PERL_UNICODE='AS'), check=True) + action_utils.run(['ikiwiki', '-setup', SETUP_WIKI, wiki_name, admin_name], + input=input_, env=dict(os.environ, + PERL_UNICODE='AS'), check=True) @privileged @@ -72,17 +71,15 @@ def create_blog(blog_name: str, admin_name: str, admin_password: secret_str): """Create a blog.""" pw_bytes = admin_password.encode() input_ = pw_bytes + b'\n' + pw_bytes - subprocess.run(['ikiwiki', '-setup', SETUP_BLOG, blog_name, admin_name], - stdout=subprocess.PIPE, input=input_, - stderr=subprocess.PIPE, env=dict(os.environ, - PERL_UNICODE='AS')) + action_utils.run(['ikiwiki', '-setup', SETUP_BLOG, blog_name, admin_name], + input=input_, env=dict(os.environ, PERL_UNICODE='AS')) @privileged def setup_site(site_name: str): """Run setup for a site.""" setup_path = os.path.join(WIKI_PATH, site_name + '.setup') - subprocess.run(['ikiwiki', '-setup', setup_path], check=True) + action_utils.run(['ikiwiki', '-setup', setup_path], check=True) @privileged diff --git a/plinth/modules/infinoted/privileged.py b/plinth/modules/infinoted/privileged.py index c37f4680e..80bf1e275 100644 --- a/plinth/modules/infinoted/privileged.py +++ b/plinth/modules/infinoted/privileged.py @@ -9,6 +9,7 @@ import shutil import subprocess import time +from plinth import action_utils from plinth.actions import privileged DATA_DIR = '/var/lib/infinoted' @@ -105,7 +106,7 @@ def _kill_daemon(): end_time = time.time() + 300 while time.time() < end_time: try: - subprocess.run(['infinoted', '--kill-daemon'], check=True) + action_utils.run(['infinoted', '--kill-daemon'], check=True) break except subprocess.CalledProcessError: pass @@ -123,19 +124,19 @@ def setup(): with open(SYSTEMD_SERVICE_PATH, 'w', encoding='utf-8') as file_handle: file_handle.write(SYSTEMD_SERVICE) - subprocess.check_call(['systemctl', 'daemon-reload']) + action_utils.service_daemon_reload() # Create infinoted group if needed. try: grp.getgrnam('infinoted') except KeyError: - subprocess.run(['addgroup', '--system', 'infinoted'], check=True) + action_utils.run(['addgroup', '--system', 'infinoted'], check=True) # Create infinoted user if needed. try: pwd.getpwnam('infinoted') except KeyError: - subprocess.run([ + action_utils.run([ 'adduser', '--system', '--ingroup', 'infinoted', '--home', DATA_DIR, '--gecos', 'Infinoted collaborative editing server', 'infinoted' @@ -151,7 +152,7 @@ def setup(): try: # infinoted doesn't have a "create key and exit" mode. Run as # daemon so we can stop after. - subprocess.run([ + action_utils.run([ 'infinoted', '--create-key', '--create-certificate', '--daemonize' ], check=True) diff --git a/plinth/modules/kiwix/privileged.py b/plinth/modules/kiwix/privileged.py index bcaad19e1..8dd5ae750 100644 --- a/plinth/modules/kiwix/privileged.py +++ b/plinth/modules/kiwix/privileged.py @@ -53,7 +53,8 @@ def add_package(file_name: str, temporary_file_path: str): def _kiwix_manage_add(zim_file: str): - subprocess.check_call(['kiwix-manage', LIBRARY_FILE, 'add', zim_file]) + action_utils.run(['kiwix-manage', LIBRARY_FILE, 'add', zim_file], + check=True) # kiwix-serve doesn't read the library file unless it is restarted. action_utils.service_try_restart('kiwix-server-freedombox') @@ -97,8 +98,8 @@ def delete_package(zim_id: str): if book.attrib['id'] != zim_id: continue - subprocess.check_call( - ['kiwix-manage', LIBRARY_FILE, 'remove', zim_id]) + action_utils.run(['kiwix-manage', LIBRARY_FILE, 'remove', zim_id], + check=True) (KIWIX_HOME / book.attrib['path']).unlink() action_utils.service_try_restart('kiwix-server-freedombox') return diff --git a/plinth/modules/letsencrypt/privileged.py b/plinth/modules/letsencrypt/privileged.py index 0993c02d4..f96184cf2 100644 --- a/plinth/modules/letsencrypt/privileged.py +++ b/plinth/modules/letsencrypt/privileged.py @@ -8,7 +8,6 @@ import os import pathlib import re import shutil -import subprocess import sys from typing import Any @@ -28,8 +27,9 @@ WEB_ROOT_PATH = '/var/www/html' def _get_certificate_expiry(domain: str) -> str: """Return the expiry date of a certificate.""" certificate_file = os.path.join(le.LIVE_DIRECTORY, domain, 'cert.pem') - output = subprocess.check_output( - ['openssl', 'x509', '-enddate', '-noout', '-in', certificate_file]) + output = action_utils.run( + ['openssl', 'x509', '-enddate', '-noout', '-in', certificate_file], + check=True).stdout return output.decode().strip().split('=')[1] @@ -41,7 +41,8 @@ def _get_modified_time(domain: str) -> int: def _get_validity_status(domain: str) -> str: """Return validity status of a certificate; valid, revoked, expired.""" - output = subprocess.check_output(['certbot', 'certificates', '-d', domain]) + output = action_utils.run(['certbot', 'certificates', '-d', domain], + check=True).stdout line = output.decode(sys.stdout.encoding) match = re.search(r'INVALID: (.*)\)', line) @@ -115,7 +116,7 @@ def revoke(domain: str): if TEST_MODE: command.append('--staging') - subprocess.run(command, check=True) + action_utils.run(command, check=True) action_utils.webserver_disable(domain, kind='site') @@ -132,7 +133,7 @@ def obtain(domain: str): if TEST_MODE: command.append('--staging') - subprocess.run(command, check=True) + action_utils.run(command, check=True) @privileged @@ -249,5 +250,5 @@ def _assert_managed_path(module, path): def delete(domain: str): """Disable a domain and delete the certificate.""" command = ['certbot', 'delete', '--non-interactive', '--cert-name', domain] - subprocess.run(command, check=True) + action_utils.run(command, check=True) action_utils.webserver_disable(domain, kind='site') diff --git a/plinth/modules/mediawiki/privileged.py b/plinth/modules/mediawiki/privileged.py index fa01c2b58..06004922a 100644 --- a/plinth/modules/mediawiki/privileged.py +++ b/plinth/modules/mediawiki/privileged.py @@ -7,6 +7,7 @@ import shutil import subprocess import tempfile +from plinth import action_utils from plinth.actions import privileged, secret_str from plinth.utils import generate_password @@ -26,8 +27,7 @@ def get_php_command(): version = '' try: - process = subprocess.run(['dpkg', '-s', 'php'], stdout=subprocess.PIPE, - check=True) + process = action_utils.run(['dpkg', '-s', 'php'], check=True) for line in process.stdout.decode().splitlines(): if line.startswith('Version:'): version = line.split(':')[-1].split('+')[0].strip() @@ -51,14 +51,15 @@ def setup(): with tempfile.NamedTemporaryFile() as password_file_handle: password_file_handle.write(password.encode()) password_file_handle.flush() - subprocess.check_call([ + action_utils.run([ get_php_command(), install_script, '--confpath=/etc/mediawiki', '--dbtype=sqlite', '--dbpath=' + data_dir, '--scriptpath=/mediawiki', '--passfile', password_file_handle.name, 'Wiki', 'admin' - ]) - subprocess.run(['chmod', '-R', 'o-rwx', data_dir], check=True) - subprocess.run(['chown', '-R', 'www-data:www-data', data_dir], check=True) + ], check=True) + action_utils.run(['chmod', '-R', 'o-rwx', data_dir], check=True) + action_utils.run(['chown', '-R', 'www-data:www-data', data_dir], + check=True) conf_file = pathlib.Path(CONF_FILE) if not conf_file.exists(): @@ -100,17 +101,17 @@ def change_password(username: str, password: secret_str): change_password_script = os.path.join(MAINTENANCE_SCRIPTS_DIR, 'changePassword.php') - subprocess.check_call([ + action_utils.run([ get_php_command(), change_password_script, '--user', username, '--password', password - ]) + ], check=True) @privileged def update(): """Run update.php maintenance script when version upgrades happen.""" update_script = os.path.join(MAINTENANCE_SCRIPTS_DIR, 'update.php') - subprocess.check_call([get_php_command(), update_script, '--quick']) + action_utils.run([get_php_command(), update_script, '--quick'], check=True) def _update_setting(setting_name, setting_line): @@ -178,7 +179,7 @@ def set_default_language(language: str): # languages. rebuild_messages_script = os.path.join(MAINTENANCE_SCRIPTS_DIR, 'rebuildmessages.php') - subprocess.check_call([get_php_command(), rebuild_messages_script]) + action_utils.run([get_php_command(), rebuild_messages_script], check=True) @privileged diff --git a/plinth/modules/minidlna/privileged.py b/plinth/modules/minidlna/privileged.py index 2fba83493..e57058770 100644 --- a/plinth/modules/minidlna/privileged.py +++ b/plinth/modules/minidlna/privileged.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Configure minidlna server.""" -import subprocess from os import chmod, fdopen, remove, stat from shutil import move from tempfile import mkstemp @@ -51,7 +50,7 @@ def setup(): encoding='utf-8') as conf: conf.write(SYSCTL_CONF) - subprocess.run(['systemctl', 'restart', 'systemd-sysctl'], check=True) + action_utils.run(['systemctl', 'restart', 'systemd-sysctl'], check=True) @privileged diff --git a/plinth/modules/miniflux/__init__.py b/plinth/modules/miniflux/__init__.py index ab1461d53..1a4083379 100644 --- a/plinth/modules/miniflux/__init__.py +++ b/plinth/modules/miniflux/__init__.py @@ -58,7 +58,12 @@ class MinifluxApp(app_module.App): login_required=True) self.add(shortcut) - packages = Packages('packages-miniflux', ['miniflux', 'postgresql']) + # miniflux package does not depend on postgres. Install postgresql, + # start it and only then install miniflux. + packages = Packages('packages-miniflux-postgresql', ['postgresql']) + self.add(packages) + + packages = Packages('packages-miniflux', ['miniflux']) self.add(packages) drop_in_configs = DropinConfigs( @@ -74,7 +79,7 @@ class MinifluxApp(app_module.App): urls=['https://{host}/miniflux/']) self.add(webserver) - daemon = SharedDaemon('shared-daemon-miniflus-postgresql', + daemon = SharedDaemon('shared-daemon-miniflux-postgresql', 'postgresql') self.add(daemon) @@ -89,7 +94,17 @@ class MinifluxApp(app_module.App): def setup(self, old_version=None): """Install and configure the app.""" privileged.pre_setup() - super().setup(old_version) + + # Database needs to be running for successful initialization or upgrade + # of miniflux database. 1. The app and database are being freshly + # installed. 2. The app is being freshly installed, but database server + # is already installed but disabled. 3. The app is being updated to new + # version but is currently disabled (DB is installed but disabled or + # enabled). + with self.get_component( + 'shared-daemon-miniflux-postgresql').ensure_running(): + super().setup(old_version) + privileged.setup(old_version) if not old_version: self.enable() @@ -97,7 +112,11 @@ class MinifluxApp(app_module.App): def uninstall(self): """De-configure and uninstall the app.""" privileged.uninstall() - super().uninstall() + with self.get_component( + 'shared-daemon-miniflux-postgresql').ensure_running(): + # Database needs to be running for successful removal miniflux + # database. + super().uninstall() class MinifluxBackupRestore(BackupRestore): diff --git a/plinth/modules/miniflux/privileged.py b/plinth/modules/miniflux/privileged.py index 1018e53f4..e30b09170 100644 --- a/plinth/modules/miniflux/privileged.py +++ b/plinth/modules/miniflux/privileged.py @@ -123,8 +123,13 @@ def reset_user_password(username: str, password: secret_str): @privileged def uninstall(): """Ensure that the database is removed.""" - action_utils.debconf_set_selections( - ['miniflux miniflux/purge boolean true']) + action_utils.debconf_set_selections([ + 'miniflux miniflux/purge boolean true', + 'miniflux miniflux/dbconfig-install boolean true', + 'miniflux miniflux/dbconfig-reinstall boolean true' + 'miniflux miniflux/dbconfig-upgrade boolean true', + 'miniflux miniflux/dbconfig-remove boolean true', + ]) def _get_database_config(): diff --git a/plinth/modules/mumble/privileged.py b/plinth/modules/mumble/privileged.py index 759c0eab3..f602087e1 100644 --- a/plinth/modules/mumble/privileged.py +++ b/plinth/modules/mumble/privileged.py @@ -4,7 +4,6 @@ Configure Mumble server. """ import pathlib -import subprocess import augeas @@ -37,8 +36,8 @@ def check_setup() -> bool: @privileged def set_super_user_password(password: secret_str): """Set the superuser password with murmurd command.""" - subprocess.run(['murmurd', '-readsupw'], input=password.encode(), - stdout=subprocess.DEVNULL, check=False) + action_utils.run(['murmurd', '-readsupw'], input=password.encode(), + check=False) @privileged diff --git a/plinth/modules/names/privileged.py b/plinth/modules/names/privileged.py index 37804eebc..f9f7fa248 100644 --- a/plinth/modules/names/privileged.py +++ b/plinth/modules/names/privileged.py @@ -2,7 +2,6 @@ """Configure Names App.""" import pathlib -import subprocess import augeas @@ -22,7 +21,7 @@ HOSTS_LOCAL_IP = '127.0.1.1' @privileged def set_hostname(hostname: str): """Set system hostname using hostnamectl.""" - subprocess.run( + action_utils.run( ['hostnamectl', 'set-hostname', '--transient', '--static', hostname], check=True) action_utils.service_restart('avahi-daemon') @@ -83,7 +82,7 @@ def domain_delete_all(): def install_resolved(): """Install systemd-resolved related packages.""" packages = ['systemd-resolved', 'libnss-resolve'] - subprocess.run(['dpkg', '--configure', '-a'], check=False) + action_utils.run(['dpkg', '--configure', '-a'], check=False) with action_utils.apt_hold_freedombox(): action_utils.run_apt_command(['--fix-broken', 'install']) returncode = action_utils.run_apt_command(['install'] + packages) diff --git a/plinth/modules/networks/privileged.py b/plinth/modules/networks/privileged.py index 2ea77518e..05eaf2be6 100644 --- a/plinth/modules/networks/privileged.py +++ b/plinth/modules/networks/privileged.py @@ -29,8 +29,9 @@ def _sort_interfaces(interfaces: list[str]) -> list[str]: def _get_interfaces() -> dict[str, list[str]]: """Return all network interfaces by their type.""" - output = subprocess.check_output( - ['nmcli', '--terse', '--fields', 'type,device', 'device']) + output = action_utils.run( + ['nmcli', '--terse', '--fields', 'type,device', 'device'], + check=True).stdout interfaces = collections.defaultdict(list) for line in output.decode().splitlines(): type_, _, interface = line.partition(':') @@ -45,14 +46,15 @@ def _get_interfaces() -> dict[str, list[str]]: def _add_connection(connection_name: str, interface: str, remaining_arguments: list[str]): """Add an Ethernet/Wi-Fi connection of type regular or shared.""" - output = subprocess.check_output( - ['nmcli', '--terse', '--fields', 'name,device', 'con', 'show']) + output = action_utils.run( + ['nmcli', '--terse', '--fields', 'name,device', 'con', 'show'], + check=True).stdout lines = output.decode().splitlines() if f'{connection_name}:{interface}' in lines: logging.info('Connection %s already exists for device %s, not adding.', connection_name, interface) else: - subprocess.run([ + action_utils.run([ 'nmcli', 'con', 'add', 'con-name', connection_name, 'ifname', interface ] + remaining_arguments, check=True) @@ -108,8 +110,9 @@ def _set_connection_properties(connection_name: str, properties: dict[str, str]): """Configure property key/values on a connection.""" for key, value in properties.items(): - subprocess.run(['nmcli', 'con', 'modify', connection_name, key, value], - check=True) + action_utils.run( + ['nmcli', 'con', 'modify', connection_name, key, value], + check=True) def _configure_wireless_interface(interface: str): diff --git a/plinth/modules/networks/tests/test_privileged.py b/plinth/modules/networks/tests/test_privileged.py index b18de501e..9bcefa2bf 100644 --- a/plinth/modules/networks/tests/test_privileged.py +++ b/plinth/modules/networks/tests/test_privileged.py @@ -6,10 +6,10 @@ from unittest.mock import patch from .. import privileged -@patch('subprocess.check_output') -def test_get_interfaces(check_output): +@patch('subprocess.run') +def test_get_interfaces(run): """Test returning list of network interfaces in sorted order.""" - check_output.return_value = '\n'.join([ + run.return_value.stdout = '\n'.join([ 'ethernet:ve-fbx-testing', 'ethernet:enp39s0', 'ethernet:enp32s1', diff --git a/plinth/modules/nextcloud/privileged.py b/plinth/modules/nextcloud/privileged.py index f7da36d57..4a373f203 100644 --- a/plinth/modules/nextcloud/privileged.py +++ b/plinth/modules/nextcloud/privileged.py @@ -73,13 +73,13 @@ def setup(): def _run_in_container( - *args, capture_output: bool = False, check: bool = True, + *args, check: bool = True, env: dict[str, str] | None = None) -> subprocess.CompletedProcess: """Run a command inside the container.""" env_args = [f'--env={key}={value}' for key, value in (env or {}).items()] command = ['podman', 'exec', '--user', WWW_DATA_UID ] + env_args + [CONTAINER_NAME] + list(args) - return subprocess.run(command, capture_output=capture_output, check=check) + return action_utils.run(command, check=check) def _run_occ(*args, **kwargs) -> subprocess.CompletedProcess: @@ -109,8 +109,7 @@ def disable(): def get_override_domain(): """Return the domain name that Nextcloud is configured to override with.""" try: - domain = _run_occ('config:system:get', 'overwritehost', - capture_output=True) + domain = _run_occ('config:system:get', 'overwritehost') return domain.stdout.decode().strip() except subprocess.CalledProcessError: return None @@ -159,8 +158,7 @@ def get_default_phone_region(): """"Get the value of default_phone_region.""" try: default_phone_region = _run_occ('config:system:get', - 'default_phone_region', - capture_output=True) + 'default_phone_region') return default_phone_region.stdout.decode().strip() except subprocess.CalledProcessError: return None @@ -174,7 +172,7 @@ def set_default_phone_region(region: str): def _database_query(query: str): """Run a database query.""" - subprocess.run(['mysql'], input=query.encode(), check=True) + action_utils.run(['mysql'], input=query.encode(), check=True) def _create_database(): @@ -239,14 +237,14 @@ def _nextcloud_wait_until_ready(): # obtaining. We are unable to obtain the lock for 5 minutes, fail and stop # the setup process. lock_file = _data_path / 'nextcloud-init-sync.lock' - subprocess.run( + action_utils.run( ['flock', '--exclusive', '--wait', '300', lock_file, 'echo'], check=True) def _nextcloud_get_status(): """Return Nextcloud status such installed, in maintenance, etc.""" - output = _run_occ('status', '--output=json', capture_output=True) + output = _run_occ('status', '--output=json') return json.loads(output.stdout) @@ -281,8 +279,7 @@ def _configure_ldap(): # Check if LDAP has already been configured. This is necessary because # if the setup proccess is rerun when updating the FredomBox app another # redundant LDAP config would be created. - output = _run_occ('ldap:test-config', 's01', capture_output=True, - check=False) + output = _run_occ('ldap:test-config', 's01', check=False) if 'Invalid configID' in output.stdout.decode(): _run_occ('ldap:create-empty-config') @@ -362,7 +359,7 @@ def dump_database(): with _maintenance_mode(): with DB_BACKUP_FILE.open('w', encoding='utf-8') as file_handle: - subprocess.run([ + action_utils.run([ 'mysqldump', '--add-drop-database', '--add-drop-table', '--add-drop-trigger', '--single-transaction', '--default-character-set=utf8mb4', '--user', 'root', @@ -374,11 +371,11 @@ def dump_database(): def restore_database(): """Restore database from file.""" with DB_BACKUP_FILE.open('r', encoding='utf-8') as file_handle: - subprocess.run(['mysql', '--user', 'root'], stdin=file_handle, - check=True) + action_utils.run(['mysql', '--user', 'root'], stdin=file_handle, + check=True) - subprocess.run(['redis-cli', '-n', - str(REDIS_DB), 'FLUSHDB', 'SYNC'], check=False) + action_utils.run(['redis-cli', '-n', + str(REDIS_DB), 'FLUSHDB', 'SYNC'], check=False) _set_database_privileges(_get_database_password()) @@ -405,8 +402,7 @@ def _get_database_password(): code = 'if (file_exists("/var/www/html/config/config.php")) {' \ 'include_once("/var/www/html/config/config.php");' \ 'print($CONFIG["dbpassword"] ?? ""); }' - return _run_in_container('php', '-r', code, - capture_output=True).stdout.decode().strip() + return _run_in_container('php', '-r', code).stdout.decode().strip() def _create_redis_config(): diff --git a/plinth/modules/openvpn/privileged.py b/plinth/modules/openvpn/privileged.py index 7ec0d98b1..9fb620a85 100644 --- a/plinth/modules/openvpn/privileged.py +++ b/plinth/modules/openvpn/privileged.py @@ -104,15 +104,15 @@ def _setup_firewall(): 'firewall-cmd', '--zone', 'internal', '--{}-interface'.format(operation), interface ] - subprocess.call(command) - subprocess.call(command + ['--permanent']) + action_utils.run(command, check=False) + action_utils.run(command + ['--permanent'], check=False) def _is_tunplus_enabled(): """Return whether tun+ interface is already added.""" try: - process = subprocess.run( + process = action_utils.run( ['firewall-cmd', '--zone', 'internal', '--list-interfaces'], - stdout=subprocess.PIPE, check=True) + check=True) return 'tun+' in process.stdout.decode().strip().split() except subprocess.CalledProcessError: return True # Safer @@ -135,8 +135,8 @@ def _setup_firewall(): def _run_easy_rsa(args): """Execute easy-rsa command with some default arguments.""" - return subprocess.run(['/usr/share/easy-rsa/easyrsa'] + args, - cwd=KEYS_DIRECTORY, check=True) + return action_utils.run(['/usr/share/easy-rsa/easyrsa'] + args, + cwd=KEYS_DIRECTORY, check=True) def _write_easy_rsa_config(): @@ -162,9 +162,9 @@ def _is_renewable(cert_name): if not cert_path.exists(): return False - process = subprocess.run( + process = action_utils.run( ['openssl', 'x509', '-noout', '-enddate', '-in', - str(cert_path)], check=True, stdout=subprocess.PIPE) + str(cert_path)], check=True) date_string = process.stdout.decode().strip().partition('=')[2] cert_expiry_time = datetime.datetime.strptime(date_string, '%b %d %H:%M:%S %Y GMT') diff --git a/plinth/modules/power/privileged.py b/plinth/modules/power/privileged.py index 06e22967c..4f468d9c9 100644 --- a/plinth/modules/power/privileged.py +++ b/plinth/modules/power/privileged.py @@ -1,18 +1,17 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Shutdown/restart the system.""" -import subprocess - +from plinth import action_utils from plinth.actions import privileged @privileged def restart(): """Restart the system.""" - subprocess.call('reboot') + action_utils.run('reboot', check=False) @privileged def shutdown(): """Shut down the system.""" - subprocess.call(['shutdown', 'now']) + action_utils.run(['shutdown', 'now'], check=False) diff --git a/plinth/modules/samba/privileged.py b/plinth/modules/samba/privileged.py index c8f0078f1..3e2e1d6e1 100644 --- a/plinth/modules/samba/privileged.py +++ b/plinth/modules/samba/privileged.py @@ -7,6 +7,7 @@ import pathlib import shutil import subprocess +from plinth import action_utils from plinth.actions import privileged DEFAULT_FILE = '/etc/default/samba' @@ -51,12 +52,13 @@ CONF = r''' def _close_share(share_name): """Disconnect all samba users who are connected to the share.""" - subprocess.check_call(['smbcontrol', 'smbd', 'close-share', share_name]) + action_utils.run(['smbcontrol', 'smbd', 'close-share', share_name], + check=True) def _conf_command(parameters, **kwargs): """Run samba configuration registry command.""" - subprocess.check_call(['net', 'conf'] + parameters, **kwargs) + action_utils.run(['net', 'conf'] + parameters, check=True, **kwargs) def _create_share(mount_point, share_type, windows_filesystem=False): @@ -103,7 +105,7 @@ def _create_share_name(mount_point): def _define_open_share(name, path, windows_filesystem=False): """Define an open samba share.""" try: - _conf_command(['delshare', name], stderr=subprocess.DEVNULL) + _conf_command(['delshare', name]) except subprocess.CalledProcessError: pass _conf_command(['addshare', name, path, 'writeable=y', 'guest_ok=y']) @@ -115,7 +117,7 @@ def _define_open_share(name, path, windows_filesystem=False): def _define_group_share(name, path, windows_filesystem=False): """Define a group samba share.""" try: - _conf_command(['delshare', name], stderr=subprocess.DEVNULL) + _conf_command(['delshare', name]) except subprocess.CalledProcessError: pass _conf_command(['addshare', name, path, 'writeable=y', 'guest_ok=n']) @@ -128,7 +130,7 @@ def _define_group_share(name, path, windows_filesystem=False): def _define_homes_share(name, path): """Define a samba share for private homes.""" try: - _conf_command(['delshare', name], stderr=subprocess.DEVNULL) + _conf_command(['delshare', name]) except subprocess.CalledProcessError: pass userpath = os.path.join(path, '%u') @@ -153,7 +155,7 @@ def _get_mount_point(path): def _get_shares() -> list[dict[str, str]]: """Get shares.""" shares = [] - output = subprocess.check_output(['net', 'conf', 'list']) + output = action_utils.run(['net', 'conf', 'list'], check=True).stdout config = configparser.RawConfigParser() config.read_string(output.decode()) for name in config.sections(): @@ -198,8 +200,8 @@ def _set_open_share_permissions(directory): file_path = os.path.join(root, file) shutil.chown(file_path, group='freedombox-share') os.chmod(file_path, 0o0664) - subprocess.check_call(['setfacl', '-Rm', 'g::rwX', directory]) - subprocess.check_call(['setfacl', '-Rdm', 'g::rwX', directory]) + action_utils.run(['setfacl', '-Rm', 'g::rwX', directory], check=True) + action_utils.run(['setfacl', '-Rdm', 'g::rwX', directory], check=True) def _use_config_file(conf_file): @@ -229,8 +231,8 @@ def _set_share_permissions(directory): file_path = os.path.join(root, file) shutil.chown(file_path, group='freedombox-share') os.chmod(file_path, 0o0664) - subprocess.check_call(['setfacl', '-Rm', 'g::rwX', directory]) - subprocess.check_call(['setfacl', '-Rdm', 'g::rwX', directory]) + action_utils.run(['setfacl', '-Rm', 'g::rwX', directory], check=True) + action_utils.run(['setfacl', '-Rdm', 'g::rwX', directory], check=True) @privileged @@ -270,7 +272,7 @@ def get_shares() -> list[dict[str, str]]: @privileged def get_users() -> list[str]: """Get users from Samba database.""" - output = subprocess.check_output(['pdbedit', '-L']).decode() + output = action_utils.run(['pdbedit', '-L'], check=True).stdout.decode() samba_users = [line.split(':')[0] for line in output.split()] return samba_users diff --git a/plinth/modules/snapshot/privileged.py b/plinth/modules/snapshot/privileged.py index 4691003c4..f899b86ee 100644 --- a/plinth/modules/snapshot/privileged.py +++ b/plinth/modules/snapshot/privileged.py @@ -4,7 +4,6 @@ import os import pathlib import signal -import subprocess import augeas import dbus @@ -21,13 +20,13 @@ def setup(old_version: int): """Configure snapper.""" # Check if root config exists. command = ['snapper', 'list-configs'] - process = subprocess.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, check=True) output = process.stdout.decode() # Create root config if needed. if 'root' not in output: command = ['snapper', 'create-config', '/'] - subprocess.run(command, check=True) + action_utils.run(command, check=True) if old_version and old_version <= 4: _remove_fstab_entry('/') @@ -76,7 +75,7 @@ def _migrate_config_from_version_3(): 'EMPTY_PRE_POST_MIN_AGE=0', 'FREE_LIMIT=0.3', ] - subprocess.run(command, check=True) + action_utils.run(command, check=True) def _set_default_config(): @@ -98,7 +97,7 @@ def _set_default_config(): 'EMPTY_PRE_POST_MIN_AGE=0', 'FREE_LIMIT=0.3', ] - subprocess.run(command, check=True) + action_utils.run(command, check=True) def _remove_fstab_entry(mount_point): @@ -137,16 +136,15 @@ def _remove_fstab_entry(mount_point): def _systemd_path_escape(path): """Escape a string using systemd path rules.""" - process = subprocess.run(['systemd-escape', '--path', path], - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['systemd-escape', '--path', path], check=True) return process.stdout.decode().strip() def _get_subvolume_path(mount_point): """Return the subvolume path for .snapshots in a filesystem.""" # -o causes the list of subvolumes directly under the given mount point - process = subprocess.run(['btrfs', 'subvolume', 'list', '-o', mount_point], - stdout=subprocess.PIPE, check=True) + process = action_utils.run( + ['btrfs', 'subvolume', 'list', '-o', mount_point], check=True) for line in process.stdout.decode().splitlines(): entry = line.split() @@ -223,8 +221,7 @@ def _parse_number(number): @privileged def list_() -> list[dict[str, str]]: """List snapshots.""" - process = subprocess.run(['snapper', 'list'], stdout=subprocess.PIPE, - check=True) + process = action_utils.run(['snapper', 'list'], check=True) lines = process.stdout.decode().splitlines() keys = ('number', 'is_default', 'is_active', 'type', 'pre_number', 'date', @@ -246,7 +243,7 @@ def list_() -> list[dict[str, str]]: def _get_default_snapshot(): """Return the default snapshot by looking at default subvolume.""" command = ['btrfs', 'subvolume', 'get-default', '/'] - process = subprocess.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, check=True) output = process.stdout.decode() output_parts = output.split() @@ -277,26 +274,26 @@ def disable_apt_snapshot(state: str): def create(): """Create snapshot.""" command = ['snapper', 'create', '--description', 'manually created'] - subprocess.run(command, check=True) + action_utils.run(command, check=True) @privileged def delete(number: str): """Delete a snapshot by number.""" command = ['snapper', 'delete', number] - subprocess.run(command, check=True) + action_utils.run(command, check=True) @privileged def set_config(config: list[str]): """Set snapper configuration.""" command = ['snapper', 'set-config'] + config - subprocess.run(command, check=True) + action_utils.run(command, check=True) def _get_config(): command = ['snapper', 'get-config'] - process = subprocess.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, check=True) lines = process.stdout.decode().splitlines() config = {} for line in lines[2:]: @@ -345,4 +342,4 @@ def rollback(number: str): # behavior when a snapshot number to rollback to is provided is the # behavior that we desire. command = ['snapper', '--ambit', 'classic', 'rollback', number] - subprocess.run(command, check=True) + action_utils.run(command, check=True) diff --git a/plinth/modules/sogo/privileged.py b/plinth/modules/sogo/privileged.py index 39b6a198c..5e22cee61 100644 --- a/plinth/modules/sogo/privileged.py +++ b/plinth/modules/sogo/privileged.py @@ -4,10 +4,9 @@ import pathlib import re import shutil -import subprocess import tempfile -from plinth import utils +from plinth import action_utils, utils from plinth.actions import privileged from plinth.db import postgres from plinth.modules.email.privileged.domain import \ @@ -144,8 +143,8 @@ def set_domain(domain: str): def _get_config_value(key: str) -> str: """Return the value of a property from the configuration file.""" - process = subprocess.run(['plget', key], input=CONFIG_FILE.read_bytes(), - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['plget', key], input=CONFIG_FILE.read_bytes(), + check=True) return process.stdout.decode().strip() @@ -154,7 +153,7 @@ def _set_config_value(key: str, value: str): with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file.write(f'{{\n{key} = "{value}";\n}}'.encode('utf-8')) temp_file.close() - subprocess.run(['plmerge', CONFIG_FILE, temp_file.name], check=True) + action_utils.run(['plmerge', CONFIG_FILE, temp_file.name], check=True) pathlib.Path(temp_file.name).unlink() diff --git a/plinth/modules/ssh/privileged.py b/plinth/modules/ssh/privileged.py index f1a744fe5..7cb0a07f5 100644 --- a/plinth/modules/ssh/privileged.py +++ b/plinth/modules/ssh/privileged.py @@ -7,7 +7,6 @@ import pathlib import pwd import shutil import stat -import subprocess import augeas @@ -101,7 +100,7 @@ def set_keys(user: str, keys: str, auth_user: str, auth_password: secret_str): ssh_folder = os.path.join(get_user_homedir(user), '.ssh') key_file_path = os.path.join(ssh_folder, 'authorized_keys') - subprocess.check_call(['mkhomedir_helper', user]) + action_utils.run(['mkhomedir_helper', user], check=True) if not os.path.exists(ssh_folder): os.makedirs(ssh_folder) diff --git a/plinth/modules/storage/privileged.py b/plinth/modules/storage/privileged.py index e5422eced..04f4e10d1 100644 --- a/plinth/modules/storage/privileged.py +++ b/plinth/modules/storage/privileged.py @@ -46,7 +46,7 @@ def _move_gpt_second_header(device): """ command = ['sgdisk', '--move-second-header', device] try: - subprocess.run(command, check=True) + action_utils.run(command, check=True) except subprocess.CalledProcessError: raise RuntimeError('Error moving GPT second header to the end') @@ -65,12 +65,12 @@ def _resize_partition(device, requested_partition, free_space): 'B', 'resizepart', requested_partition['number'] ] try: - subprocess.run(command, check=True) + action_utils.run(command, check=True) except subprocess.CalledProcessError: try: input_text = 'yes\n' + str(free_space['end']) - subprocess.run(fallback_command, check=True, - input=input_text.encode()) + action_utils.run(fallback_command, check=True, + input=input_text.encode()) except subprocess.CalledProcessError as exception: raise RuntimeError(f'Error expanding partition: {exception}') @@ -90,8 +90,7 @@ def _resize_ext4(device, requested_partition, _free_space, _mount_point): requested_partition['number']) try: command = ['resize2fs', partition_device] - subprocess.run(command, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) + action_utils.run(command, check=True) except subprocess.CalledProcessError as exception: raise RuntimeError(f'Error expanding filesystem: {exception}') @@ -100,7 +99,7 @@ def _resize_btrfs(_device, _requested_partition, _free_space, mount_point='/'): """Resize a btrfs file system inside a partition.""" try: command = ['btrfs', 'filesystem', 'resize', 'max', mount_point] - subprocess.run(command, stdout=subprocess.DEVNULL, check=True) + action_utils.run(command, check=True) except subprocess.CalledProcessError as exception: raise RuntimeError(f'Error expanding filesystem: {exception}') @@ -167,7 +166,7 @@ def _get_partitions_and_free_spaces(device, partition_number): command = [ 'parted', '--machine', '--script', device, 'unit', 'B', 'print', 'free' ] - process = subprocess.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, check=True) requested_partition = None free_spaces = [] @@ -215,7 +214,7 @@ def mount(block_device: str): UDISKS_FILESYSTEM_SHARED=1 by writing a udev rule. """ - subprocess.run([ + action_utils.run([ 'udisksctl', 'mount', '--block-device', block_device, '--no-user-interaction' ], check=True) @@ -325,7 +324,7 @@ def usage_info() -> str: 'df', '--exclude-type=tmpfs', '--exclude-type=devtmpfs', '--block-size=1', '--output=source,fstype,size,used,avail,pcent,target' ] - return subprocess.check_output(command).decode() + return action_utils.run(command, check=False).stdout.decode() @privileged diff --git a/plinth/modules/syncthing/privileged.py b/plinth/modules/syncthing/privileged.py index 6180c353d..5359a09d6 100644 --- a/plinth/modules/syncthing/privileged.py +++ b/plinth/modules/syncthing/privileged.py @@ -5,7 +5,6 @@ import grp import os import pwd import shutil -import subprocess import time import augeas @@ -37,13 +36,13 @@ def setup(): try: grp.getgrnam('syncthing') except KeyError: - subprocess.run(['addgroup', '--system', 'syncthing'], check=True) + action_utils.run(['addgroup', '--system', 'syncthing'], check=True) # Create syncthing user if needed. try: pwd.getpwnam('syncthing') except KeyError: - subprocess.run([ + action_utils.run([ 'adduser', '--system', '--ingroup', 'syncthing', '--home', DATA_DIR, '--gecos', 'Syncthing file synchronization server', 'syncthing' diff --git a/plinth/modules/tor/privileged.py b/plinth/modules/tor/privileged.py index b2b4847f2..17ca0bebe 100644 --- a/plinth/modules/tor/privileged.py +++ b/plinth/modules/tor/privileged.py @@ -8,7 +8,6 @@ import pathlib import re import shutil import socket -import subprocess import time from typing import Any @@ -54,7 +53,7 @@ def _first_time_setup(): """Setup Tor configuration for the first time setting defaults.""" logger.info('Performing first time setup for Tor') - subprocess.run(['tor-instance-create', INSTANCE_NAME], check=True) + action_utils.run(['tor-instance-create', INSTANCE_NAME], check=True) # Remove line starting with +SocksPort, since our augeas lens # doesn't handle it correctly. diff --git a/plinth/modules/torproxy/privileged.py b/plinth/modules/torproxy/privileged.py index 8ce68cc2a..883df4a79 100644 --- a/plinth/modules/torproxy/privileged.py +++ b/plinth/modules/torproxy/privileged.py @@ -4,7 +4,6 @@ import logging import os import shutil -import subprocess from typing import Any import augeas @@ -31,7 +30,7 @@ def setup(): # Mask the service to prevent re-enabling it by the Tor master service. action_utils.service_mask('tor@default') - subprocess.run(['tor-instance-create', INSTANCE_NAME], check=True) + action_utils.run(['tor-instance-create', INSTANCE_NAME], check=True) # Remove line starting with +SocksPort, since our augeas lens # doesn't handle it correctly. diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py index f573ecdcd..a412e022e 100644 --- a/plinth/modules/upgrades/__init__.py +++ b/plinth/modules/upgrades/__init__.py @@ -351,6 +351,15 @@ def is_backports_enabled(): return os.path.exists(privileged.BACKPORTS_SOURCES_LIST) +def get_current_release(): + """Return current release and codename as a tuple.""" + output = action_utils.run( + ['lsb_release', '--release', '--codename', '--short'], + check=True).stdout.decode().strip() + lines = output.split('\n') + return lines[0], lines[1] + + def is_backports_current(): """Return whether backports are enabled for the current release.""" if not is_backports_enabled(): diff --git a/plinth/modules/upgrades/distupgrade.py b/plinth/modules/upgrades/distupgrade.py index ce95eff6f..e9024727e 100644 --- a/plinth/modules/upgrades/distupgrade.py +++ b/plinth/modules/upgrades/distupgrade.py @@ -5,7 +5,6 @@ import contextlib import datetime import logging import pathlib -import subprocess from datetime import timezone from typing import Generator @@ -73,7 +72,7 @@ distribution_info: dict = { def _apt_run(arguments: list[str]): """Run an apt command and ensure that output is written to stdout.""" - returncode = action_utils.run_apt_command(arguments, stdout=None) + returncode = action_utils.run_apt_command(arguments) if returncode: raise RuntimeError( f'Apt command failed with return code: {returncode}') @@ -218,11 +217,11 @@ def _snapshot_run_and_disable() -> Generator[None, None, None]: try: logger.info('Taking a snapshot before dist upgrade...') command = ['snapper', 'create', '--description', 'before dist-upgrade'] - subprocess.run(command, check=True) + action_utils.run(command, check=True) aug = snapshot_module.load_augeas() if snapshot_module.is_apt_snapshots_enabled(aug): logger.info('Disabling apt snapshots during dist upgrade...') - subprocess.run([ + action_utils.run([ '/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot', @@ -235,7 +234,7 @@ def _snapshot_run_and_disable() -> Generator[None, None, None]: finally: if reenable: logger.info('Re-enabling apt snapshots...') - subprocess.run([ + action_utils.run([ '/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot' ], input='{"args": ["no"], "kwargs": {}}'.encode(), check=True) else: @@ -303,7 +302,7 @@ def _apt_update(): def _apt_fix(): """Try to fix any problems with apt/dpkg before the upgrade.""" logger.info('Fixing any broken apt/dpkg states...') - subprocess.run(['dpkg', '--configure', '-a'], check=False) + action_utils.run(['dpkg', '--configure', '-a'], check=False) _apt_run(['--fix-broken', 'install']) @@ -341,7 +340,7 @@ def _unattended_upgrades_run(): To handle upgrading the freedombox package. """ logger.info('Running unattended-upgrade...') - subprocess.run(['unattended-upgrade', '--verbose'], check=False) + action_utils.run(['unattended-upgrade', '--verbose'], check=False) def _freedombox_restart(): @@ -360,7 +359,7 @@ def _trigger_on_complete(): # file will not be possible. For that, we need to launch a new process with # a different systemd service (which does not have the bind mounts). logger.info('Triggering on-complete to commit sources.lists') - subprocess.run([ + action_utils.run([ 'systemd-run', '--unit=freedombox-dist-upgrade-on-complete', '--description=Finish up upgrade to new stable Debian release', '/usr/bin/freedombox-cmd', 'upgrades', 'dist_upgrade_on_complete', @@ -417,7 +416,7 @@ def start_service(): '--property=KillMode=process', '--property=TimeoutSec=72hr', f'--property=BindPaths={temp_sources_list}:{sources_list}' ] - subprocess.run(['systemd-run'] + args + [ + action_utils.run(['systemd-run'] + args + [ 'systemd-inhibit', '/usr/bin/freedombox-cmd', 'upgrades', 'dist_upgrade', '--no-args' ], check=True) diff --git a/plinth/modules/upgrades/privileged.py b/plinth/modules/upgrades/privileged.py index ad107cdfa..bec033389 100644 --- a/plinth/modules/upgrades/privileged.py +++ b/plinth/modules/upgrades/privileged.py @@ -7,6 +7,7 @@ import pathlib import re import subprocess +from plinth import action_utils from plinth.action_utils import (apt_hold_flag, apt_unhold_freedombox, is_package_manager_busy, run_apt_command, service_is_running) @@ -127,17 +128,17 @@ def release_held_packages(): 'holds.') return - output = subprocess.check_output(['apt-mark', 'showhold']).decode().strip() + output = action_utils.run(['apt-mark', 'showhold'], + check=True).stdout.decode().strip() holds = output.split('\n') logger.info('Releasing package holds: %s', holds) - subprocess.run(['apt-mark', 'unhold', *holds], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) + action_utils.run(['apt-mark', 'unhold', *holds], check=True) @privileged def run(): """Run unattended-upgrades.""" - subprocess.run(['dpkg', '--configure', '-a'], check=False) + action_utils.run(['dpkg', '--configure', '-a'], check=False) run_apt_command(['--fix-broken', 'install']) _release_held_freedombox() diff --git a/plinth/modules/upgrades/tests/test_distupgrade.py b/plinth/modules/upgrades/tests/test_distupgrade.py index 8852de9f8..84efd6a8c 100644 --- a/plinth/modules/upgrades/tests/test_distupgrade.py +++ b/plinth/modules/upgrades/tests/test_distupgrade.py @@ -7,7 +7,7 @@ import re import subprocess from datetime import datetime as datetime_original from datetime import timezone -from unittest.mock import call, patch +from unittest.mock import Mock, call, patch import pytest @@ -24,7 +24,7 @@ def test_apt_run(run): distupgrade._apt_run(args) assert run.call_args.args == \ (['apt-get', '--assume-yes', '--quiet=2'] + args,) - assert not run.call_args.kwargs['stdout'] + assert run.call_args.kwargs['stdout'] == subprocess.PIPE run.return_value.returncode = 10 with pytest.raises(RuntimeError): @@ -219,7 +219,7 @@ def test_snapshot_run_and_disable(is_supported, is_apt_snapshots_enabled, run): with distupgrade._snapshot_run_and_disable(): assert run.call_args_list == [ call(['snapper', 'create', '--description', 'before dist-upgrade'], - check=True) + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) ] run.reset_mock() @@ -230,16 +230,18 @@ def test_snapshot_run_and_disable(is_supported, is_apt_snapshots_enabled, run): with distupgrade._snapshot_run_and_disable(): assert run.call_args_list == [ call(['snapper', 'create', '--description', 'before dist-upgrade'], - check=True), + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True), call([ '/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot' - ], input=b'{"args": ["yes"], "kwargs": {}}', check=True) + ], input=b'{"args": ["yes"], "kwargs": {}}', + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) ] run.reset_mock() assert run.call_args_list == [ call(['/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot'], - input=b'{"args": ["no"], "kwargs": {}}', check=True) + input=b'{"args": ["no"], "kwargs": {}}', stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True) ] @@ -262,40 +264,51 @@ def test_services_disable(service_is_running, service_disable, service_enable): @patch('subprocess.run') -@patch('subprocess.check_call') -@patch('subprocess.check_output') -def test_apt_hold_packages(check_output, check_call, run, tmp_path): +def test_apt_hold_packages(run, tmp_path): """Test that holding apt packages works.""" + + def _run(command, **kwargs): + if 'showhold' in command: + return Mock(stdout=False) + + return Mock(returncode=0) + hold_flag = tmp_path / 'flag' - run.return_value.returncode = 0 + run.side_effect = _run with patch('plinth.action_utils.apt_hold_flag', hold_flag), \ patch('plinth.modules.upgrades.distupgrade.PACKAGES_WITH_PROMPTS', ['package1', 'package2']): - check_output.return_value = False with distupgrade._apt_hold_packages(): assert hold_flag.exists() assert hold_flag.stat().st_mode & 0o117 == 0 - expected_call = [call(['apt-mark', 'hold', 'freedombox'])] - assert check_call.call_args_list == expected_call expected_calls = [ - call(['apt-mark', 'hold', 'package1'], check=False), - call(['apt-mark', 'hold', 'package2'], check=False) + call(['apt-mark', 'showhold', 'freedombox'], check=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE), + call(['apt-mark', 'hold', 'freedombox'], check=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE), + call(['apt-mark', 'showhold', 'package1'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True), + call(['apt-mark', 'hold', 'package1'], check=False, + stdout=subprocess.PIPE, stderr=subprocess.PIPE), + call(['apt-mark', 'showhold', 'package2'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True), + call(['apt-mark', 'hold', 'package2'], check=False, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) ] - assert run.call_args_list == expected_calls - check_call.reset_mock() + assert run.mock_calls == expected_calls run.reset_mock() expected_call = [ - call(['apt-mark', 'unhold', 'freedombox'], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - check=False) + call(['apt-mark', 'unhold', 'package1'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True), + call(['apt-mark', 'unhold', 'package2'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True), + call(['apt-mark', 'unhold', 'freedombox'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False), ] assert run.call_args_list == expected_call - expected_calls = [ - call(['apt-mark', 'unhold', 'package1']), - call(['apt-mark', 'unhold', 'package2']) - ] - assert check_call.call_args_list == expected_calls @patch('plinth.action_utils.debconf_set_selections') @@ -340,7 +353,8 @@ def test_apt_fix(run, apt_run): """Test that apt fixes work.""" distupgrade._apt_fix() assert run.call_args_list == [ - call(['dpkg', '--configure', '-a'], check=False) + call(['dpkg', '--configure', '-a'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False) ] assert apt_run.call_args_list == [call(['--fix-broken', 'install'])] @@ -365,7 +379,9 @@ def test_apt_full_upgrade(apt_run): def test_unatteneded_upgrades_run(run): """Test that running unattended upgrades works.""" distupgrade._unattended_upgrades_run() - run.assert_called_with(['unattended-upgrade', '--verbose'], check=False) + run.assert_called_with(['unattended-upgrade', '--verbose'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) @patch('plinth.action_utils.service_restart') @@ -384,7 +400,7 @@ def test_trigger_on_complete(run): '--description=Finish up upgrade to new stable Debian release', '/usr/bin/freedombox-cmd', 'upgrades', 'dist_upgrade_on_complete', '--no-args' - ], check=True) + ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) def test_on_complete(tmp_path): diff --git a/plinth/modules/upgrades/tests/test_utils.py b/plinth/modules/upgrades/tests/test_utils.py index 8336c9731..abd98bc84 100644 --- a/plinth/modules/upgrades/tests/test_utils.py +++ b/plinth/modules/upgrades/tests/test_utils.py @@ -64,10 +64,10 @@ deb http://deb.debian.org/debian trixie main assert utils.get_sources_list_codename() == 'testing' -@patch('subprocess.check_output') -def test_get_current_release(check_output): +@patch('subprocess.run') +def test_get_current_release(run): """Test that getting current release works.""" - check_output.return_value = b'test-release\ntest-codename\n\n' + run.return_value.stdout = b'test-release\ntest-codename\n\n' assert utils.get_current_release() == ('test-release', 'test-codename') diff --git a/plinth/modules/upgrades/utils.py b/plinth/modules/upgrades/utils.py index 5904554fc..bc48cfeab 100644 --- a/plinth/modules/upgrades/utils.py +++ b/plinth/modules/upgrades/utils.py @@ -3,10 +3,10 @@ import pathlib import re -import subprocess import augeas +from plinth import action_utils from plinth.modules.apache.components import check_url RELEASE_FILE_URL = \ @@ -23,7 +23,7 @@ def check_auto() -> bool: 'apt-config', 'shell', 'UpdateInterval', 'APT::Periodic::Update-Package-Lists' ] - output = subprocess.check_output(arguments).decode() + output = action_utils.run(arguments, check=True).stdout.decode() update_interval = 0 match = re.match(r"UpdateInterval='(.*)'", output) if match: @@ -62,7 +62,7 @@ def is_release_file_available(protocol: str, dist: str, def is_sufficient_free_space() -> bool: """Return whether there is sufficient free space for dist upgrade.""" - output = subprocess.check_output(['df', '--output=avail', '/']) + output = action_utils.run(['df', '--output=avail', '/'], check=True).stdout free_space = int(output.decode().split('\n')[1]) return free_space >= DIST_UPGRADE_REQUIRED_FREE_SPACE @@ -100,9 +100,9 @@ def get_sources_list_codename() -> str | None: def get_current_release(): """Return current release and codename as a tuple.""" - output = subprocess.check_output( - ['lsb_release', '--release', '--codename', - '--short']).decode().strip() + output = action_utils.run( + ['lsb_release', '--release', '--codename', '--short'], + check=True).stdout.decode().strip() lines = output.split('\n') return lines[0], lines[1] diff --git a/plinth/modules/users/__init__.py b/plinth/modules/users/__init__.py index 71e6418cb..67543f3cd 100644 --- a/plinth/modules/users/__init__.py +++ b/plinth/modules/users/__init__.py @@ -9,6 +9,7 @@ from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_noop +from plinth import action_utils from plinth import app as app_module from plinth import cfg, menu from plinth.config import DropinConfigs @@ -127,8 +128,9 @@ def _diagnose_ldap_entry(search_item: str) -> DiagnosticCheck: result = Result.FAILED try: - output = subprocess.check_output( - ['ldapsearch', '-LLL', '-x', '-b', 'dc=thisbox', search_item]) + output = action_utils.run( + ['ldapsearch', '-LLL', '-x', '-b', 'dc=thisbox', search_item], + check=True).stdout if search_item in output.decode(): result = Result.PASSED except subprocess.CalledProcessError: diff --git a/plinth/modules/users/privileged.py b/plinth/modules/users/privileged.py index 3d75043ca..a676f2c65 100644 --- a/plinth/modules/users/privileged.py +++ b/plinth/modules/users/privileged.py @@ -68,7 +68,7 @@ def first_setup(): def setup(): """Setup LDAP.""" # Update pam config for mkhomedir. - subprocess.run(['pam-auth-update', '--package'], check=True) + action_utils.run(['pam-auth-update', '--package'], check=True) _configure_ldapscripts() @@ -145,10 +145,10 @@ def _create_organizational_unit(unit): """Create an organizational unit in LDAP.""" distinguished_name = 'ou={unit},dc=thisbox'.format(unit=unit) try: - subprocess.run([ + action_utils.run([ 'ldapsearch', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-s', 'base', '-b', distinguished_name, '(objectclass=*)' - ], stdout=subprocess.DEVNULL, check=True) + ], check=True) return # Already exists except subprocess.CalledProcessError: input = ''' @@ -156,18 +156,18 @@ dn: ou={unit},dc=thisbox objectClass: top objectClass: organizationalUnit ou: {unit}'''.format(unit=unit) - subprocess.run(['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - input=input.encode(), stdout=subprocess.DEVNULL, - check=True) + action_utils.run( + ['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], + input=input.encode(), check=True) def _setup_admin(): """Remove LDAP admin password and Allow root to modify the users.""" - process = subprocess.run([ + process = action_utils.run([ 'ldapsearch', '-Q', '-L', '-L', '-L', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-s', 'base', '-b', 'olcDatabase={1}mdb,cn=config', '(objectclass=*)', 'olcRootDN', 'olcRootPW' - ], check=True, stdout=subprocess.PIPE) + ], check=True) ldap_object = {} for line in process.stdout.decode().splitlines(): if line: @@ -175,18 +175,18 @@ def _setup_admin(): ldap_object[line[0]] = line[1] if 'olcRootPW' in ldap_object: - subprocess.run( + action_utils.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + check=True, input=b''' dn: olcDatabase={1}mdb,cn=config changetype: modify delete: olcRootPW''') root_dn = 'gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth' if ldap_object['olcRootDN'] != root_dn: - subprocess.run( + action_utils.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + check=True, input=b''' dn: olcDatabase={1}mdb,cn=config changetype: modify replace: olcRootDN @@ -205,9 +205,9 @@ def _setup_ldap_ppolicy() -> bool: """ # Load ppolicy module try: - subprocess.run( + action_utils.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + check=True, input=b''' dn: cn=module{0},cn=config changetype: modify add: olcModuleLoad @@ -218,18 +218,19 @@ olcModuleLoad: ppolicy''') # Add namedobject schema needed for 'objectClass: namedPolicy'. try: - subprocess.run([ + action_utils.run([ 'ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-f', '/etc/ldap/schema/namedobject.ldif' - ], check=True, stdout=subprocess.DEVNULL) + ], check=True) except subprocess.CalledProcessError as error: if error.returncode != 80: # Schema already added raise # Set up default password policy try: - subprocess.run(['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + action_utils.run( + ['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], check=True, + input=b''' dn: cn=DefaultPPolicy,ou=policies,dc=thisbox cn: DefaultPPolicy objectClass: pwdPolicy @@ -243,8 +244,9 @@ pwdLockout: TRUE''') # Make DefaultPPolicy as a default ppolicy overlay try: - subprocess.run(['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + action_utils.run( + ['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], check=True, + input=b''' dn: olcOverlay={0}ppolicy,olcDatabase={1}mdb,cn=config objectClass: olcOverlayConfig objectClass: olcPPolicyConfig @@ -329,22 +331,22 @@ def get_nslcd_config() -> dict[str, str]: def _get_samba_users(): """Get users from the Samba user database.""" # 'pdbedit -L' is better for listing users but is installed only with samba - stdout = subprocess.check_output( - ['tdbdump', '/var/lib/samba/private/passdb.tdb']).decode() + stdout = action_utils.run(['tdbdump', '/var/lib/samba/private/passdb.tdb'], + check=True).stdout.decode() return re.findall(r'USER_(.*)\\0', stdout) def _delete_samba_user(username): """Delete a Samba user.""" if username in _get_samba_users(): - subprocess.check_call(['smbpasswd', '-x', username]) + action_utils.run(['smbpasswd', '-x', username], check=True) _disconnect_samba_user(username) def _disconnect_samba_user(username): """Disconnect a Samba user.""" try: - subprocess.check_call(['pkill', '-U', username, 'smbd']) + action_utils.run(['pkill', '-U', username, 'smbd'], check=True) except subprocess.CalledProcessError as error: if error.returncode != 1: raise @@ -352,7 +354,8 @@ def _disconnect_samba_user(username): def _get_user_home(username): """Return the user home directory.""" - output = subprocess.check_output(['getent', 'passwd', username], text=True) + output = action_utils.run(['getent', 'passwd', username], + check=True).stdout.decode() return pathlib.Path(output.split(':')[5]) @@ -453,7 +456,7 @@ def rename_user(old_username: str, new_username: str): def _set_user_password(username, password): """Set a user's password.""" - process = _run(['slappasswd', '-s', password], stdout=subprocess.PIPE) + process = _run(['slappasswd', '-s', password]) password = process.stdout.decode().strip() _run(['ldapsetpasswd', username, password]) @@ -463,9 +466,9 @@ def _set_samba_user(username, password): If a user already exists, update password. """ - proc = subprocess.run(['smbpasswd', '-a', '-s', username], - input='{0}\n{0}\n'.format(password).encode(), - stderr=subprocess.PIPE, check=False) + proc = action_utils.run(['smbpasswd', '-a', '-s', username], + input='{0}\n{0}\n'.format(password).encode(), + check=False) if proc.returncode != 0: raise RuntimeError('Unable to add Samba user: ', proc.stderr) @@ -489,11 +492,11 @@ def _get_admin_users(): admin_users = [] try: - output = subprocess.check_output([ + output = action_utils.run([ 'ldapsearch', '-LLL', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-o', 'ldif-wrap=no', '-s', 'base', '-b', 'cn=admin,ou=groups,dc=thisbox', 'memberUid' - ]).decode() + ], check=True).stdout.decode() except subprocess.CalledProcessError as error: if error.returncode == 32: # no entries found @@ -511,7 +514,7 @@ def _get_admin_users(): def _get_user_ids(username: str) -> str | None: """Get user information in format like `id` command.""" try: - process = _run(['ldapid', username], stdout=subprocess.PIPE) + process = _run(['ldapid', username]) except subprocess.CalledProcessError as error: if error.returncode == 1: # User doesn't exist @@ -530,7 +533,7 @@ def _user_exists(username): def _get_group_users(groupname): """Return list of members in the group.""" try: - process = _run(['ldapgid', '-P', groupname], stdout=subprocess.PIPE) + process = _run(['ldapgid', '-P', groupname]) except subprocess.CalledProcessError: return [] # Group does not exist @@ -677,14 +680,14 @@ def set_user_status(username: str, status: str, auth_user: str, # Set user status in Samba password database if username in _get_samba_users(): - subprocess.check_call(['smbpasswd', smbpasswd_flag, username]) + action_utils.run(['smbpasswd', smbpasswd_flag, username], check=True) _flush_cache() if status == 'inactive': # Kill all user processes. This includes disconnectiong ssh, samba and # cockpit sessions. - subprocess.run(['pkill', '--signal', 'KILL', '--uid', username]) + action_utils.run(['pkill', '--signal', 'KILL', '--uid', username]) def _upgrade_inactivate_users(usernames: list[str]): @@ -695,7 +698,7 @@ def _upgrade_inactivate_users(usernames: list[str]): _flush_cache() for username in usernames: - subprocess.run(['pkill', '--signal', 'KILL', '--uid', username]) + action_utils.run(['pkill', '--signal', 'KILL', '--uid', username]) def _flush_cache(): @@ -706,6 +709,4 @@ def _flush_cache(): def _run(arguments, check=True, **kwargs): """Run a command. Check return code and suppress output by default.""" env = dict(os.environ, LDAPSCRIPTS_CONF=LDAPSCRIPTS_CONF) - kwargs['stdout'] = kwargs.get('stdout', subprocess.DEVNULL) - kwargs['stderr'] = kwargs.get('stderr', subprocess.DEVNULL) - return subprocess.run(arguments, env=env, check=check, **kwargs) + return action_utils.run(arguments, env=env, check=check, **kwargs) diff --git a/plinth/modules/wireguard/privileged.py b/plinth/modules/wireguard/privileged.py index 50dd0cb26..fc37ca833 100644 --- a/plinth/modules/wireguard/privileged.py +++ b/plinth/modules/wireguard/privileged.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Configuration helper for WireGuard.""" -import subprocess - +from plinth import action_utils from plinth.actions import privileged SERVER_INTERFACE = 'wg0' @@ -11,8 +10,8 @@ SERVER_INTERFACE = 'wg0' @privileged def get_info() -> dict[str, dict]: """Return info for each configured interface.""" - output = subprocess.check_output(['wg', 'show', 'all', - 'dump']).decode().strip() + output = action_utils.run(['wg', 'show', 'all', 'dump'], + check=True).stdout.decode().strip() lines = output.split('\n') interfaces: dict[str, dict] = {} for line in lines: diff --git a/plinth/modules/wordpress/privileged.py b/plinth/modules/wordpress/privileged.py index 0b5c92f72..23fd7b994 100644 --- a/plinth/modules/wordpress/privileged.py +++ b/plinth/modules/wordpress/privileged.py @@ -6,7 +6,6 @@ import pathlib import random import shutil import string -import subprocess import augeas @@ -90,8 +89,8 @@ def _create_database(db_name): # Wordpress' install.php creates the tables. # SQL injection is avoided due to known input. query = f'''CREATE DATABASE {db_name};''' - subprocess.run(['mysql', '--user', 'root'], input=query.encode(), - check=True) + action_utils.run(['mysql', '--user', 'root'], input=query.encode(), + check=True) def _set_privileges(db_host, db_name, db_user, db_password): @@ -103,8 +102,8 @@ def _set_privileges(db_host, db_name, db_user, db_password): IDENTIFIED BY '{db_password}'; FLUSH PRIVILEGES; ''' - subprocess.run(['mysql', '--user', 'root'], input=query.encode(), - check=True) + action_utils.run(['mysql', '--user', 'root'], input=query.encode(), + check=True) def _generate_secret_key(length=64, chars=None): @@ -146,7 +145,7 @@ def dump_database(): _db_backup_file.parent.mkdir(parents=True, exist_ok=True) with action_utils.service_ensure_running('mysql'): with _db_backup_file.open('w', encoding='utf-8') as file_handle: - subprocess.run([ + action_utils.run([ 'mysqldump', '--add-drop-database', '--add-drop-table', '--add-drop-trigger', '--user', 'root', '--databases', DB_NAME ], stdout=file_handle, check=True) @@ -157,8 +156,8 @@ def restore_database(): """Restore database from file.""" with action_utils.service_ensure_running('mysql'): with _db_backup_file.open('r', encoding='utf-8') as file_handle: - subprocess.run(['mysql', '--user', 'root'], stdin=file_handle, - check=True) + action_utils.run(['mysql', '--user', 'root'], stdin=file_handle, + check=True) _set_privileges(DB_HOST, DB_NAME, DB_USER, _read_db_password()) @@ -192,9 +191,9 @@ def _drop_database(db_host, db_name, db_user): """Drop the mysql database that was created during install.""" with action_utils.service_ensure_running('mysql'): query = f"DROP DATABASE {db_name};" - subprocess.run(['mysql', '--user', 'root'], input=query.encode(), - check=False) + action_utils.run(['mysql', '--user', 'root'], input=query.encode(), + check=False) query = f"DROP USER IF EXISTS {db_user}@{db_host};" - subprocess.run(['mysql', '--user', 'root'], input=query.encode(), - check=False) + action_utils.run(['mysql', '--user', 'root'], input=query.encode(), + check=False) diff --git a/plinth/modules/zoph/privileged.py b/plinth/modules/zoph/privileged.py index 6ed2eefe8..b63e7f960 100644 --- a/plinth/modules/zoph/privileged.py +++ b/plinth/modules/zoph/privileged.py @@ -23,6 +23,7 @@ def pre_install(): 'zoph zoph/dbconfig-install boolean true', 'zoph zoph/dbconfig-upgrade boolean true', 'zoph zoph/dbconfig-remove boolean true', + 'zoph zoph/dbconfig-reinstall boolean true' 'zoph zoph/mysql/admin-user string root', 'zoph zoph/rm_images select yes', ]) @@ -33,15 +34,13 @@ def get_configuration() -> dict[str, str]: """Return the current configuration.""" configuration = {} try: - process = subprocess.run(['zoph', '--dump-config'], - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['zoph', '--dump-config'], check=True) except subprocess.CalledProcessError as exception: if exception.returncode != 96: raise _zoph_setup_cli_user() - process = subprocess.run(['zoph', '--dump-config'], - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['zoph', '--dump-config'], check=True) for line in process.stdout.decode().splitlines(): name, value = line.partition(':')[::2] @@ -75,13 +74,13 @@ WHERE def _zoph_configure(key, value): """Set a configure value in Zoph.""" try: - subprocess.run(['zoph', '--config', key, value], check=True) + action_utils.run(['zoph', '--config', key, value], check=True) except subprocess.CalledProcessError as exception: if exception.returncode != 96: raise _zoph_setup_cli_user() - subprocess.run(['zoph', '--config', key, value], check=True) + action_utils.run(['zoph', '--config', key, value], check=True) @privileged @@ -137,17 +136,16 @@ def set_configuration(enable_osm: bool | None = None, query = f"UPDATE zoph_users SET user_name='{admin_user}' \ WHERE user_name='admin';" - subprocess.run(['mysql', _get_db_config()['db_name']], - input=query.encode(), check=True) + action_utils.run(['mysql', _get_db_config()['db_name']], + input=query.encode(), check=True) @privileged def is_configured() -> bool | None: """Return whether zoph app is configured.""" try: - process = subprocess.run( - ['zoph', '--get-config', 'interface.user.remote'], - stdout=subprocess.PIPE, check=True) + process = action_utils.run( + ['zoph', '--get-config', 'interface.user.remote'], check=True) return process.stdout.decode().strip() == 'true' except (FileNotFoundError, subprocess.CalledProcessError): return None @@ -163,8 +161,8 @@ def dump_database(): db_name = _get_db_config()['db_name'] os.makedirs(os.path.dirname(DB_BACKUP_FILE), exist_ok=True) with open(DB_BACKUP_FILE, 'w', encoding='utf-8') as db_backup_file: - subprocess.run(['mysqldump', db_name], stdout=db_backup_file, - check=True) + action_utils.run(['mysqldump', db_name], stdout=db_backup_file, + check=True) @privileged @@ -178,15 +176,16 @@ def restore_database(): db_user = _get_db_config()['db_user'] db_host = _get_db_config()['db_host'] db_pass = _get_db_config()['db_pass'] - subprocess.run(['mysqladmin', '--force', 'drop', db_name], check=False) - subprocess.run(['mysqladmin', 'create', db_name], check=True) + action_utils.run(['mysqladmin', '--force', 'drop', db_name], + check=False) + action_utils.run(['mysqladmin', 'create', db_name], check=True) with open(DB_BACKUP_FILE, 'r', encoding='utf-8') as db_restore_file: - subprocess.run(['mysql', db_name], stdin=db_restore_file, - check=True) + action_utils.run(['mysql', db_name], stdin=db_restore_file, + check=True) # Set the password for user from restored configuration query = f'ALTER USER {db_user}@{db_host} IDENTIFIED BY "{db_pass}";' - subprocess.run(['mysql'], input=query.encode(), check=True) + action_utils.run(['mysql'], input=query.encode(), check=True) @privileged @@ -198,12 +197,12 @@ def uninstall(): with action_utils.service_ensure_running('mysql'): try: config = _get_db_config() - subprocess.run( + action_utils.run( ['mysqladmin', '--force', 'drop', config['db_name']], check=False) query = f'DROP USER IF EXISTS {config["db_user"]}@localhost;' - subprocess.run(['mysql'], input=query.encode(), check=False) + action_utils.run(['mysql'], input=query.encode(), check=False) except FileNotFoundError: # Database configuration not found pass diff --git a/plinth/privileged/packages.py b/plinth/privileged/packages.py index 90d40ccd1..68928fd04 100644 --- a/plinth/privileged/packages.py +++ b/plinth/privileged/packages.py @@ -3,7 +3,6 @@ import logging import os -import subprocess from collections import defaultdict from typing import Any @@ -14,7 +13,7 @@ import apt_pkg from plinth import action_utils from plinth import app as app_module from plinth import module_loader -from plinth.action_utils import run_apt_command +from plinth.action_utils import run, run_apt_command from plinth.actions import privileged logger = logging.getLogger(__name__) @@ -61,7 +60,7 @@ def install(app_id: str, packages: list[str], skip_recommends: bool = False, if force_missing_configuration: extra_arguments += ['-o', 'Dpkg::Options::=--force-confmiss'] - subprocess.run(['dpkg', '--configure', '-a'], check=False) + run(['dpkg', '--configure', '-a'], check=False) with action_utils.apt_hold_freedombox(): run_apt_command(['--fix-broken', 'install']) returncode = run_apt_command(['install'] + extra_arguments + packages) @@ -79,7 +78,7 @@ def remove(app_id: str, packages: list[str], purge: bool): except Exception: raise PermissionError(f'Packages are not managed: {packages}') - subprocess.run(['dpkg', '--configure', '-a'], check=False) + run(['dpkg', '--configure', '-a'], check=False) with action_utils.apt_hold_freedombox(): run_apt_command(['--fix-broken', 'install']) options = [] if not purge else ['--purge'] diff --git a/plinth/privileged_daemon.py b/plinth/privileged_daemon.py index 7cfd256a5..3b31ec900 100644 --- a/plinth/privileged_daemon.py +++ b/plinth/privileged_daemon.py @@ -8,10 +8,12 @@ import logging import os import pathlib import pwd +import signal import socket import socketserver import struct import sys +import threading import time import systemd.daemon @@ -28,6 +30,8 @@ FREEDOMBOX_PROCESS_USER = 'plinth' MAX_REQUEST_LENGTH = 1_000_000 +_server = None + idle_shutdown_time: int | None = 5 * 60 # 5 minutes freedombox_develop = False @@ -207,7 +211,7 @@ class Server(socketserver.ThreadingUnixStreamServer): def client_main() -> None: """Parse arguments for the client for privileged daemon.""" - log.action_init() + log.action_init(console=True) parser = argparse.ArgumentParser() parser.add_argument('module', help='Module to trigger action in') @@ -245,6 +249,25 @@ def client_main() -> None: sys.exit(1) +def _on_sigterm(signal_number: int, frame) -> None: + """Handle SIGTERM signal. Issue server shutdown.""" + threading.Thread(target=_shutdown_server).start() + + +def _shutdown_server() -> None: + """Issue a shutdown request to the server. + + This must be run in a thread separate from the server.serve_forever() + otherwise it will deadlock waiting for the shutdown to complete. + """ + global _server + logger.info('SIGTERM received, shutting down the server.') + if _server: + _server.shutdown() + + logger.info('Shutdown complete, some requests may be running.') + + def main() -> None: """Start the server, listen on socket, and serve forever.""" global freedombox_develop, idle_shutdown_time @@ -263,10 +286,15 @@ def main() -> None: if not systemd.daemon.listen_fds(unset_environment=False): idle_shutdown_time = None + signal.signal(signal.SIGTERM, _on_sigterm) + module_loader.load_modules() app_module.apps_init() with Server(str(address), RequestHandler) as server: + global _server + _server = server # Reference needed to shutdown the server. + # systemd will wait until notification to proceed with other processes. # We have service Type=notify. systemd.daemon.notify('READY=1') @@ -283,6 +311,11 @@ def main() -> None: else: logger.info('FreedomBox privileged daemon exiting.') + # Exit the context manager. This calls server.close() which waits on + # all pending request threads to complete. + + logger.info('All requested completed. Exit.') + if __name__ == '__main__': main() diff --git a/plinth/setup.py b/plinth/setup.py index a90e3f3f4..9676eadcb 100644 --- a/plinth/setup.py +++ b/plinth/setup.py @@ -350,7 +350,7 @@ def run_setup_on_apps(app_ids, allow_install=True): else: setup_apps(app_ids, allow_install=allow_install) except Exception as exception: - logger.error('Error running setup - %s', exception) + logger.exception('Error running setup - %s', exception) raise diff --git a/plinth/tests/test_daemon.py b/plinth/tests/test_daemon.py index 55a58d60f..879774db2 100644 --- a/plinth/tests/test_daemon.py +++ b/plinth/tests/test_daemon.py @@ -81,54 +81,55 @@ def test_is_enabled(service_is_enabled, daemon): @patch('subprocess.run') def test_enable(subprocess_run, apps_init, app_list, mock_privileged, daemon): """Test that enabling the daemon works.""" + common_args = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) daemon.enable() subprocess_run.assert_has_calls( - [call(['systemctl', 'enable', 'test-unit'], check=False)]) + [call(['systemctl', 'enable', 'test-unit'], **common_args)]) subprocess_run.assert_any_call(['systemctl', 'start', 'test-unit'], - stdout=subprocess.DEVNULL, check=False) + **common_args) subprocess_run.reset_mock() daemon.alias = 'test-unit-2' daemon.enable() + subprocess_run.assert_has_calls([ - call(['systemctl', 'enable', 'test-unit'], check=False), - call(['systemctl', 'start', 'test-unit'], stdout=subprocess.DEVNULL, - check=False), - call(['systemctl', 'enable', 'test-unit-2'], check=False), - call(['systemctl', 'start', 'test-unit-2'], stdout=subprocess.DEVNULL, - check=False), + call(['systemctl', 'enable', 'test-unit'], **common_args), + call(['systemctl', 'start', 'test-unit'], **common_args), + call(['systemctl', 'enable', 'test-unit-2'], **common_args), + call(['systemctl', 'start', 'test-unit-2'], **common_args), ]) subprocess_run.assert_any_call(['systemctl', 'start', 'test-unit'], - stdout=subprocess.DEVNULL, check=False) + **common_args) subprocess_run.assert_any_call(['systemctl', 'start', 'test-unit-2'], - stdout=subprocess.DEVNULL, check=False) + **common_args) @patch('plinth.app.apps_init') @patch('subprocess.run') def test_disable(subprocess_run, apps_init, app_list, mock_privileged, daemon): """Test that disabling the daemon works.""" + common_args = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) daemon.disable() subprocess_run.assert_has_calls( - [call(['systemctl', 'disable', 'test-unit'], check=False)]) + [call(['systemctl', 'disable', 'test-unit'], **common_args)]) subprocess_run.assert_any_call(['systemctl', 'stop', 'test-unit'], - stdout=subprocess.DEVNULL, check=False) + **common_args) subprocess_run.reset_mock() daemon.alias = 'test-unit-2' daemon.disable() subprocess_run.assert_has_calls([ - call(['systemctl', 'disable', 'test-unit'], check=False), - call(['systemctl', 'stop', 'test-unit'], stdout=subprocess.DEVNULL, - check=False), - call(['systemctl', 'disable', 'test-unit-2'], check=False), - call(['systemctl', 'stop', 'test-unit-2'], stdout=subprocess.DEVNULL, - check=False), + call(['systemctl', 'disable', 'test-unit'], **common_args), + call(['systemctl', 'stop', 'test-unit'], **common_args), + call(['systemctl', 'disable', 'test-unit-2'], **common_args), + call(['systemctl', 'stop', 'test-unit-2'], **common_args), ]) subprocess_run.assert_any_call(['systemctl', 'stop', 'test-unit'], - stdout=subprocess.DEVNULL, check=False) + **common_args) subprocess_run.assert_any_call(['systemctl', 'stop', 'test-unit-2'], - stdout=subprocess.DEVNULL, check=False) + **common_args) @patch('plinth.action_utils.service_is_running') @@ -144,10 +145,21 @@ def test_is_running(service_is_running, daemon): @patch('plinth.app.apps_init') @patch('plinth.action_utils.service_is_running') +@patch('plinth.action_utils.service_show') @patch('subprocess.run') -def test_ensure_running(subprocess_run, service_is_running, apps_init, - app_list, mock_privileged, daemon): +def test_ensure_running(subprocess_run, service_show, service_is_running, + apps_init, app_list, mock_privileged, daemon): """Test that checking that the daemon is running works.""" + common_args = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) + + service_show.return_value = {'LoadState': 'not-found'} + with daemon.ensure_running() as starting_state: + assert not starting_state + assert subprocess_run.mock_calls == [] + + service_show.return_value = {'LoadState': 'loaded'} + service_is_running.return_value = True with daemon.ensure_running() as starting_state: assert starting_state @@ -159,16 +171,14 @@ def test_ensure_running(subprocess_run, service_is_running, apps_init, with daemon.ensure_running() as starting_state: assert not starting_state assert subprocess_run.mock_calls == [ - call(['systemctl', 'enable', 'test-unit'], check=False), - call(['systemctl', 'start', 'test-unit'], - stdout=subprocess.DEVNULL, check=False), + call(['systemctl', 'enable', 'test-unit'], **common_args), + call(['systemctl', 'start', 'test-unit'], **common_args), ] subprocess_run.reset_mock() assert subprocess_run.mock_calls == [ - call(['systemctl', 'disable', 'test-unit'], check=False), - call(['systemctl', 'stop', 'test-unit'], stdout=subprocess.DEVNULL, - check=False), + call(['systemctl', 'disable', 'test-unit'], **common_args), + call(['systemctl', 'stop', 'test-unit'], **common_args), ]