diff --git a/Vagrantfile b/Vagrantfile index e7566016e..3d518b5f0 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -16,7 +16,7 @@ Vagrant.configure(2) do |config| end config.vm.provision "shell", run: 'always', inline: <<-SHELL # Disable automatic upgrades - /vagrant/actions/upgrades disable-auto + echo -e 'APT::Periodic::Update-Package-Lists "0";\nAPT::Periodic::Unattended-Upgrade "0";' > //etc/apt/apt.conf.d/20auto-upgrades # Do not run system plinth systemctl stop plinth systemctl disable plinth diff --git a/container b/container index 7884a2913..bf031ca8f 100755 --- a/container +++ b/container @@ -193,7 +193,7 @@ mount -o remount /freedombox if [[ "{distribution}" == "stable" && ! -e $BACKPORTS_SOURCES_LIST ]] then echo "> In container: Enable backports" - /freedombox/actions/upgrades activate-backports + /freedombox/actions/actions upgrades activate_backports --no-args fi echo "> In container: Upgrade packages" @@ -698,7 +698,10 @@ def _setup(image_file, distribution): return logger.info('In container: Disabling automatic updates temporarily') - _runc(image_file, ['/usr/share/plinth/actions/upgrades', 'disable-auto']) + contents = 'APT::Periodic::Update-Package-Lists "0";\n' \ + 'APT::Periodic::Unattended-Upgrade "0";\n' + _runc(image_file, ['tee', '/etc/apt/apt.conf.d/20auto-upgrades'], + input=contents.encode()) logger.info('In container: Disabling FreedomBox service') _runc(image_file, ['systemctl', 'disable', 'plinth'], diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py index a530f868e..4a732df83 100644 --- a/plinth/modules/upgrades/__init__.py +++ b/plinth/modules/upgrades/__init__.py @@ -1,9 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -FreedomBox app for upgrades. -""" +"""FreedomBox app for upgrades.""" -import json import logging import os import subprocess @@ -13,14 +10,13 @@ from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_noop import plinth -from plinth import actions from plinth import app as app_module from plinth import cfg, glib, kvstore, menu from plinth.daemon import RelatedDaemon from plinth.modules.backups.components import BackupRestore from plinth.package import Packages -from . import manifest +from . import manifest, privileged first_boot_steps = [ { @@ -48,10 +44,6 @@ BACKPORTS_REQUESTED_KEY = 'upgrades_backports_requested' DIST_UPGRADE_ENABLED_KEY = 'upgrades_dist_upgrade_enabled' -SOURCES_LIST = '/etc/apt/sources.list' - -BACKPORTS_SOURCES_LIST = '/etc/apt/sources.list.d/freedombox2.list' - logger = logging.getLogger(__name__) @@ -140,11 +132,11 @@ class UpgradesApp(app_module.App): # Enable automatic upgrades but only on first install if not old_version and not cfg.develop: - actions.superuser_run('upgrades', ['enable-auto']) + privileged.enable_auto() # Update apt preferences whenever on first install and on version # increment. - actions.superuser_run('upgrades', ['setup']) + privileged.setup() # When upgrading from a version without first boot wizard for # backports, assume backports have been requested. @@ -161,30 +153,10 @@ class UpgradesApp(app_module.App): setup_repositories(None) -def is_enabled(): - """Return whether the module is enabled.""" - output = actions.run('upgrades', ['check-auto']) - return 'True' in output.split() - - -def enable(): - """Enable the module.""" - actions.superuser_run('upgrades', ['enable-auto']) - - -def disable(): - """Disable the module.""" - actions.superuser_run('upgrades', ['disable-auto']) - - def setup_repositories(_): """Setup apt repositories for backports.""" if is_backports_requested(): - command = ['activate-backports'] - if cfg.develop: - command.append('--develop') - - actions.superuser_run('upgrades', command) + privileged.activate_backports(cfg.develop) def check_dist_upgrade(_): @@ -196,12 +168,8 @@ def check_dist_upgrade(_): def try_start_dist_upgrade(test=False): """Try to start dist upgrade.""" from plinth.notification import Notification - command = ['start-dist-upgrade'] - if test: - command.append('--test') - output = actions.superuser_run('upgrades', command) - result = json.loads(output) + result = privileged.start_dist_upgrade(test) dist_upgrade_started = result['dist_upgrade_started'] reason = result['reason'] if 'found-previous' in reason: @@ -270,7 +238,7 @@ def set_dist_upgrade_enabled(enabled=True): def is_backports_enabled(): """Return whether backports are enabled in the system configuration.""" - return os.path.exists(BACKPORTS_SOURCES_LIST) + return os.path.exists(privileged.BACKPORTS_SOURCES_LIST) def get_current_release(): diff --git a/actions/upgrades b/plinth/modules/upgrades/privileged.py old mode 100755 new mode 100644 similarity index 78% rename from actions/upgrades rename to plinth/modules/upgrades/privileged.py index d5e7960f8..43a2c2ea3 --- a/actions/upgrades +++ b/plinth/modules/upgrades/privileged.py @@ -1,30 +1,26 @@ -#!/usr/bin/python3 # SPDX-License-Identifier: AGPL-3.0-or-later -""" -Configures or runs unattended-upgrades -""" +"""Configure or run unattended-upgrades.""" -import argparse -import json import logging import os import pathlib import re import subprocess -import sys import time -from typing import List, Tuple +from typing import List, Tuple, Union from plinth.action_utils import (apt_hold, apt_hold_flag, apt_hold_freedombox, apt_unhold_freedombox, debconf_set_selections, is_package_manager_busy, run_apt_command, service_daemon_reload, service_restart) +from plinth.actions import privileged from plinth.modules.apache.components import check_url from plinth.modules.snapshot import is_apt_snapshots_enabled from plinth.modules.snapshot import is_supported as snapshot_is_supported from plinth.modules.snapshot import load_augeas as snapshot_load_augeas -from plinth.modules.upgrades import (BACKPORTS_SOURCES_LIST, SOURCES_LIST, - get_current_release, is_backports_current) + +SOURCES_LIST = '/etc/apt/sources.list' +BACKPORTS_SOURCES_LIST = '/etc/apt/sources.list.d/freedombox2.list' AUTO_CONF_FILE = '/etc/apt/apt.conf.d/20auto-upgrades' LOG_FILE = '/var/log/unattended-upgrades/unattended-upgrades.log' @@ -32,7 +28,8 @@ DPKG_LOG_FILE = '/var/log/unattended-upgrades/unattended-upgrades-dpkg.log' RELEASE_FILE_URL = \ 'https://deb.debian.org/debian/dists/{}/Release' -APT_PREFERENCES_FREEDOMBOX = '''Explanation: This file is managed by FreedomBox, do not edit. +APT_PREFERENCES_FREEDOMBOX = \ + '''Explanation: This file is managed by FreedomBox, do not edit. Explanation: Allow carefully selected updates to 'freedombox' from backports. Package: src:freedombox Pin: release a={}-backports @@ -42,7 +39,8 @@ Pin-Priority: 500 # Whenever these preferences needs to change, increment the version number # upgrades app. This ensures that setup is run again and the new contents are # overwritten on the old file. -APT_PREFERENCES_APPS = '''Explanation: This file is managed by FreedomBox, do not edit. +APT_PREFERENCES_APPS = \ + '''Explanation: This file is managed by FreedomBox, do not edit. Explanation: matrix-synapse shall not be available in Debian stable but Explanation: only in backports. Upgrade priority of packages that have needed Explanation: versions only in backports. @@ -104,7 +102,7 @@ Description=Upgrade to new stable Debian release [Service] Type=oneshot -ExecStart=/usr/share/plinth/actions/upgrades dist-upgrade +ExecStart=/usr/share/plinth/actions/actions upgrades dist_upgrade --no-args KillMode=process TimeoutSec=12hr ''' @@ -116,46 +114,18 @@ dist_upgrade_flag = pathlib.Path( '/var/lib/freedombox/dist-upgrade-in-progress') -def parse_arguments(): - """Return parsed command line arguments as dictionary""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') - - subparsers.add_parser('run', help='Upgrade packages on the system') - subparsers.add_parser('check-auto', - help='Check if automatic upgrades are enabled') - subparsers.add_parser('enable-auto', help='Enable automatic upgrades') - subparsers.add_parser('disable-auto', help='Disable automatic upgrades.') - subparsers.add_parser('get-log', help='Print the automatic upgrades log') - - subparsers.add_parser('setup', help='Setup apt preferences') - - activate_backports = subparsers.add_parser( - 'activate-backports', help='Activate backports if possible') - activate_backports.add_argument('--develop', required=False, default=False, - action='store_true', - help='Development mode') - - start_dist_upgrade = subparsers.add_parser( - 'start-dist-upgrade', help='Check and start dist upgrade process') - start_dist_upgrade.add_argument( - '--test', required=False, default=False, action='store_true', - help='Test dist-upgrade from stable to testing') - subparsers.add_parser('dist-upgrade', help='Perform dist upgrade') - - subparsers.required = True - return parser.parse_args() - - def _release_held_freedombox(): - """In case freedombox package was left in held state by an interrupted - process, release it.""" + """If freedombox package was left in held state, release it. + + This can happen due to an interrupted process. + """ if apt_hold_flag.exists() and not is_package_manager_busy(): apt_unhold_freedombox() -def _run(): - """Run unattended-upgrades""" +@privileged +def run(): + """Run unattended-upgrades.""" subprocess.run(['dpkg', '--configure', '-a'], check=False) run_apt_command(['--fix-broken', 'install']) _release_held_freedombox() @@ -166,18 +136,6 @@ def _run(): start_new_session=True) -def subcommand_run(_): - """Run unattended-upgrades""" - try: - _run() - except FileNotFoundError: - print('Error: systemctl is not available.', file=sys.stderr) - sys.exit(2) - except Exception as error: - print('Error: {0}'.format(error), file=sys.stderr) - sys.exit(3) - - def _check_auto() -> bool: """Check if automatic upgrades are enabled.""" arguments = [ @@ -193,45 +151,48 @@ def _check_auto() -> bool: return bool(update_interval) -def subcommand_check_auto(_): - """Check if automatic upgrades are enabled""" - try: - print(_check_auto()) - except subprocess.CalledProcessError as error: - print('Error: {0}'.format(error), file=sys.stderr) - sys.exit(1) +@privileged +def check_auto() -> bool: + """Check if automatic upgrades are enabled.""" + return _check_auto() -def subcommand_enable_auto(_): - """Enable automatic upgrades""" +@privileged +def enable_auto(): + """Enable automatic upgrades.""" with open(AUTO_CONF_FILE, 'w', encoding='utf-8') as conffile: conffile.write('APT::Periodic::Update-Package-Lists "1";\n') conffile.write('APT::Periodic::Unattended-Upgrade "1";\n') -def subcommand_disable_auto(_): - """Disable automatic upgrades""" +@privileged +def disable_auto(): + """Disable automatic upgrades.""" with open(AUTO_CONF_FILE, 'w', encoding='utf-8') as conffile: conffile.write('APT::Periodic::Update-Package-Lists "0";\n') conffile.write('APT::Periodic::Unattended-Upgrade "0";\n') -def subcommand_get_log(_): - """Print the automatic upgrades log.""" +@privileged +def get_log() -> str: + """Return the automatic upgrades log.""" + log_lines = [] try: - print('==> ' + os.path.basename(LOG_FILE)) + log_lines.append('==> ' + os.path.basename(LOG_FILE)) with open(LOG_FILE, 'r', encoding='utf-8') as file_handle: - print(file_handle.read()) + log_lines.append(file_handle.read()) except IOError: pass try: - print('==> ' + os.path.basename(DPKG_LOG_FILE)) + log_lines.append('==> ' + os.path.basename(DPKG_LOG_FILE)) with open(DPKG_LOG_FILE, 'r', encoding='utf-8') as file_handle: - print(file_handle.read()) + log_lines.append(file_handle.read()) except IOError: pass + return '\n'.join(log_lines) + def _get_protocol() -> str: """Return the protocol to use for newly added repository sources.""" @@ -278,8 +239,10 @@ def _check_and_backports_sources(develop=False): if os.path.exists(old_sources_list): os.remove(old_sources_list) + from plinth.modules.upgrades import (get_current_release, + is_backports_current) if is_backports_current(): - print('Repositories list up-to-date. Skipping update.') + logging.info('Repositories list up-to-date. Skipping update.') return try: @@ -290,25 +253,26 @@ def _check_and_backports_sources(develop=False): for line in default_origin.readlines() ] except FileNotFoundError: - print('Could not open /etc/dpkg/origins/default') + logging.info('Could not open /etc/dpkg/origins/default') return if not any(matches): - print('System is running a derivative of Debian. Skip enabling ' - 'backports.') + logging.info('System is running a derivative of Debian. Skip enabling ' + 'backports.') return release, dist = get_current_release() if release == 'unstable' or (release == 'testing' and not develop): - print(f'System release is {release}. Skip enabling backports.') + logging.info(f'System release is {release}. Skip enabling backports.') return protocol = _get_protocol() if protocol == 'tor+http': - print('Package download over Tor is enabled.') + logging.info('Package download over Tor is enabled.') if not _is_release_file_available(protocol, dist, backports=True): - print(f'Release file for {dist}-backports is not available yet.') + logging.info( + f'Release file for {dist}-backports is not available yet.') return print(f'{dist}-backports is now available. Adding to sources.') @@ -328,12 +292,14 @@ def _add_apt_preferences(): # Don't try to remove 50freedombox3.pref as this file is shipped with the # Debian package and is removed using maintainer scripts. + from plinth.modules.upgrades import get_current_release _, dist = get_current_release() if dist == 'sid': - print(f'System distribution is {dist}. Skip setting apt preferences ' - 'for backports.') + logging.info( + f'System distribution is {dist}. Skip setting apt preferences ' + 'for backports.') else: - print(f'Setting apt preferences for {dist}-backports.') + logging.info(f'Setting apt preferences for {dist}-backports.') with open(base_path / '50freedombox4.pref', 'w', encoding='utf-8') as file_handle: file_handle.write(APT_PREFERENCES_FREEDOMBOX.format(dist)) @@ -350,17 +316,20 @@ def _is_sufficient_free_space() -> bool: def _check_dist_upgrade(test_upgrade=False) -> Tuple[bool, str]: - """Check for new stable release, if updates are enabled, and if there is + """Check if a distribution upgrade be performed. + + Check for new stable release, if updates are enabled, and if there is enough free space for the dist upgrade. If test_upgrade is True, also check for upgrade to testing. - Returns (boolean, string) indicating if the upgrade is ready, and a reason + Return (boolean, string) indicating if the upgrade is ready, and a reason if not. """ if dist_upgrade_flag.exists(): return (True, 'found-previous') + from plinth.modules.upgrades import get_current_release release, dist = get_current_release() if release in ['unstable', 'testing']: return (False, f'already-{release}') @@ -429,7 +398,8 @@ def _check_dist_upgrade(test_upgrade=False) -> Tuple[bool, str]: def _take_snapshot_and_disable() -> bool: """Take a snapshot if supported and enabled, then disable snapshots. - Return whether snapshots shall be re-enabled at the end.""" + Return whether snapshots shall be re-enabled at the end. + """ if snapshot_is_supported(): print('Taking a snapshot before dist upgrade...', flush=True) subprocess.run([ @@ -467,7 +437,8 @@ def _restore_snapshots_config(reenable=False): def _disable_searx() -> bool: """If searx is enabled, disable it until we can upgrade it properly. - Return whether searx was originally enabled.""" + Return whether searx was originally enabled. + """ searx_is_enabled = pathlib.Path( '/etc/uwsgi/apps-enabled/searx.ini').exists() if searx_is_enabled: @@ -481,7 +452,9 @@ def _disable_searx() -> bool: def _update_searx(reenable=False): """If searx is installed, update search engines list. - Re-enable if previously enabled.""" + + Re-enable if previously enabled. + """ if pathlib.Path('/etc/searx/settings.yml').exists(): print('Updating searx search engines list...', flush=True) subprocess.run([ @@ -567,19 +540,20 @@ def _perform_dist_upgrade(): dist_upgrade_flag.unlink() -def subcommand_setup(_): +@privileged +def setup(): """Setup apt preferences.""" _add_apt_preferences() -def subcommand_activate_backports(arguments): +@privileged +def activate_backports(develop: bool = False): """Setup software repositories needed for FreedomBox. Repositories list for now only contains the backports. If the file exists, assume that it contains backports. - """ - _check_and_backports_sources(arguments.develop) + _check_and_backports_sources(develop) def _start_dist_upgrade_service(): @@ -595,7 +569,8 @@ def _start_dist_upgrade_service(): start_new_session=True) -def subcommand_start_dist_upgrade(arguments): +@privileged +def start_dist_upgrade(test: bool = False) -> dict[str, Union[str, bool]]: """Start dist upgrade process. Check if a new stable release is available, and start dist-upgrade process @@ -603,31 +578,14 @@ def subcommand_start_dist_upgrade(arguments): """ _release_held_freedombox() - upgrade_ready, reason = _check_dist_upgrade(arguments.test) + upgrade_ready, reason = _check_dist_upgrade(test) if upgrade_ready: _start_dist_upgrade_service() - print( - json.dumps({ - 'dist_upgrade_started': upgrade_ready, - 'reason': reason, - })) + return {'dist_upgrade_started': upgrade_ready, 'reason': reason} -def subcommand_dist_upgrade(_): - """Perform major distribution upgrade. - """ +@privileged +def dist_upgrade(): + """Perform major distribution upgrade.""" _perform_dist_upgrade() - - -def main(): - """Parse arguments and perform all duties""" - arguments = parse_arguments() - - subcommand = arguments.subcommand.replace('-', '_') - subcommand_method = globals()['subcommand_' + subcommand] - subcommand_method(arguments) - - -if __name__ == '__main__': - main() diff --git a/plinth/modules/upgrades/views.py b/plinth/modules/upgrades/views.py index 7d5e6cd23..98700f3de 100644 --- a/plinth/modules/upgrades/views.py +++ b/plinth/modules/upgrades/views.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -FreedomBox app for upgrades. -""" +"""FreedomBox app for upgrades.""" + import subprocess from apt.cache import Cache @@ -13,34 +12,37 @@ from django.utils.translation import gettext as _ from django.views.generic import TemplateView from django.views.generic.edit import FormView -from plinth import __version__, actions, package -from plinth.errors import ActionError +from plinth import __version__, package from plinth.modules import first_boot, upgrades from plinth.views import AppView +from . import privileged from .forms import BackportsFirstbootForm, ConfigureForm, UpdateFirstbootForm class UpgradesConfigurationView(AppView): """Serve configuration page.""" + form_class = ConfigureForm success_url = reverse_lazy('upgrades:index') template_name = "upgrades_configure.html" app_id = 'upgrades' def get_initial(self): + """Return the initial values for the form.""" return { - 'auto_upgrades_enabled': upgrades.is_enabled(), + 'auto_upgrades_enabled': privileged.check_auto(), 'dist_upgrade_enabled': upgrades.is_dist_upgrade_enabled() } def get_context_data(self, *args, **kwargs): + """Add additional context data for template.""" context = super().get_context_data(*args, **kwargs) context['can_activate_backports'] = upgrades.can_activate_backports() context['is_backports_requested'] = upgrades.is_backports_requested() context['is_busy'] = (_is_updating() or package.is_package_manager_busy()) - context['log'] = get_log() + context['log'] = privileged.get_log() context['refresh_page_sec'] = 3 if context['is_busy'] else None context['version'] = __version__ context['new_version'] = is_newer_version_available() @@ -58,10 +60,10 @@ class UpgradesConfigurationView(AppView): try: if new_status['auto_upgrades_enabled']: - upgrades.enable() + privileged.enable_auto() else: - upgrades.disable() - except ActionError as exception: + privileged.disable_auto() + except Exception as exception: error = exception.args[2] messages.error( self.request, @@ -89,14 +91,14 @@ class UpgradesConfigurationView(AppView): def is_newer_version_available(): - """Returns whether a newer Freedombox version is available.""" + """Return whether a newer Freedombox version is available.""" cache = Cache() freedombox = cache['freedombox'] return not freedombox.candidate.is_installed def get_os_release(): - """Returns the Debian release number and name.""" + """Return the Debian release number and name.""" output = 'Error: Cannot read PRETTY_NAME in /etc/os-release.' with open('/etc/os-release', 'r', encoding='utf-8') as release_file: for line in release_file: @@ -107,11 +109,6 @@ def get_os_release(): return output -def get_log(): - """Return the current log for unattended upgrades.""" - return actions.superuser_run('upgrades', ['get-log']) - - def _is_updating(): """Check if manually triggered update is running.""" command = ['systemctl', 'is-active', 'freedombox-manual-upgrade'] @@ -124,9 +121,9 @@ def upgrade(request): """Serve the upgrade page.""" if request.method == 'POST': try: - actions.superuser_run('upgrades', ['run']) + privileged.run() messages.success(request, _('Upgrade process started.')) - except ActionError: + except Exception: messages.error(request, _('Starting upgrade failed.')) return redirect(reverse_lazy('upgrades:index')) @@ -144,6 +141,7 @@ def activate_backports(request): class BackportsFirstbootView(FormView): """View to configure backports during first boot wizard.""" + template_name = 'backports-firstboot.html' form_class = BackportsFirstbootForm @@ -179,6 +177,7 @@ class BackportsFirstbootView(FormView): class UpdateFirstbootView(FormView): """View to run initial update during first boot wizard.""" + template_name = 'update-firstboot.html' form_class = UpdateFirstbootForm @@ -197,7 +196,7 @@ class UpdateFirstbootView(FormView): """Run update if selected, and mark step as done.""" self.update = form.cleaned_data['update_now'] if self.update: - actions.superuser_run('upgrades', ['run']) + privileged.run() first_boot.mark_step_done('initial_update') return super().form_valid(form) @@ -205,6 +204,7 @@ class UpdateFirstbootView(FormView): class UpdateFirstbootProgressView(TemplateView): """View to show initial update progress.""" + template_name = 'update-firstboot-progress.html' def get_context_data(self, *args, **kwargs):