upgrades: Use privileged decorator for actions

Tests:

- DONE: Functional tests work
- DONE: Initial setup works
  - DONE: Automatic upgrades are enable by default
  - DONE: apt preferences have been updated
- DONE: Enabling backports works
  - DONE: Configuration file is created
  - DONE: Correct status is shown in the app page
- DONE: Enabling/disabling automatic upgrades works
  - DONE: Configuration file is updated
  - DONE: Correct status is shown in the app page
- DONE: Manual triggering of updates work
  - DONE: Log is shown properly in the app page
- DONE: Checking for distribution upgrade works
- DONE: Distribution upgrade from stable to testing works
  - DONE: When running on btrfs distribution, snapshot is created before.
  - DONE: Snapshots will be disable before upgrade and re-enabled later.
  - DONE: When searx is enabled before upgrade, it's uwsgi will be disabled and
    re-enabled later.
  - Failures due to freedombox package not being the latest version (with the
    changes).
- DONE: Development Vagrant box
  - DONE: Automatic updates are disabled during development setup
- DONE: Development Container
  - DONE: Automatic updates are disabled during development setup
  - DONE: On stable, backports are enabled when running tests

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2022-09-03 07:31:31 -07:00 committed by James Valleroy
parent 11a27d8efc
commit 66c1ddc404
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
5 changed files with 110 additions and 181 deletions

2
Vagrantfile vendored
View File

@ -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

View File

@ -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'],

View File

@ -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():

View File

@ -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()

View File

@ -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):