diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py index 737371419..624c1b9a4 100644 --- a/plinth/modules/upgrades/__init__.py +++ b/plinth/modules/upgrades/__init__.py @@ -19,7 +19,7 @@ from plinth.diagnostic_check import DiagnosticCheck, Result from plinth.modules.backups.components import BackupRestore from plinth.package import Packages -from . import manifest, privileged +from . import distupgrade, manifest, privileged first_boot_steps = [ { @@ -214,58 +214,11 @@ def setup_repositories(_): def check_dist_upgrade(_): """Check for upgrade to new stable release.""" if is_dist_upgrade_enabled(): - try_start_dist_upgrade() - - -def try_start_dist_upgrade(test=False): - """Try to start dist upgrade.""" - from plinth.notification import Notification - - try: - privileged.start_dist_upgrade(test, _log_error=False) - except RuntimeError as exception: - reason = exception.args[0] - else: - logger.info('Started dist upgrade.') - title = gettext_noop('Distribution update started') - message = gettext_noop( - 'Started update to next stable release. This may take a long ' - 'time to complete.') - Notification.update_or_create(id='upgrades-dist-upgrade-started', - app_id='upgrades', severity='info', - title=title, message=message, actions=[{ - 'type': 'dismiss' - }], group='admin') - return - - if 'found-previous' in reason: - logger.info( - 'Found previous dist-upgrade. If it was interrupted, it will ' - 'be restarted.') - elif 'already-' in reason: - logger.info('Skip dist upgrade: System is already up-to-date.') - elif 'codename-not-found' in reason: - logger.warning('Skip dist upgrade: Codename not found in release ' - 'file.') - elif 'upgrades-not-enabled' in reason: - logger.info('Skip dist upgrade: Automatic updates are not enabled.') - elif 'test-not-set' in reason: - logger.info('Skip dist upgrade: --test is not set.') - elif 'not-enough-free-space' in reason: - logger.warning('Skip dist upgrade: Not enough free space in /.') - title = gettext_noop('Could not start distribution update') - message = gettext_noop( - 'There is not enough free space in the root partition to ' - 'start the distribution update. Please ensure at least 5 GB ' - 'is free. Distribution update will be retried after 24 hours,' - ' if enabled.') - Notification.update_or_create(id='upgrades-dist-upgrade-free-space', - app_id='upgrades', severity='warning', - title=title, message=message, actions=[{ - 'type': 'dismiss' - }], group='admin') - else: - logger.warning('Unhandled result of start-dist-upgrade: %s', reason) + status = distupgrade.get_status() + if status['next_action'] in ('continue', 'ready'): + privileged.start_dist_upgrade() + else: + logger.info('Not ready for distribution upgrade - %s', status) def is_backports_requested(): @@ -335,17 +288,6 @@ def can_enable_dist_upgrade(): return release not in ['unstable', 'testing', 'n/a'] -def can_test_dist_upgrade(): - """Return whether dist upgrade can be tested.""" - return can_enable_dist_upgrade() and cfg.develop - - -def test_dist_upgrade(): - """Test dist-upgrade from stable to testing.""" - if can_test_dist_upgrade(): - try_start_dist_upgrade(test=True) - - def _diagnose_held_packages(): """Check if any packages have holds.""" check = DiagnosticCheck(PKG_HOLD_DIAG_CHECK_ID, diff --git a/plinth/modules/upgrades/distupgrade.py b/plinth/modules/upgrades/distupgrade.py index 7f62205c4..576a9213a 100644 --- a/plinth/modules/upgrades/distupgrade.py +++ b/plinth/modules/upgrades/distupgrade.py @@ -2,10 +2,12 @@ """Perform distribution upgrade.""" import contextlib +import datetime import logging import pathlib import subprocess import time +from datetime import timezone from typing import Generator import augeas @@ -29,6 +31,46 @@ PRE_DEBCONF_SELECTIONS: list[str] = [ sources_list = pathlib.Path('/etc/apt/sources.list') temp_sources_list = pathlib.Path('/etc/apt/sources.list.fbx-dist-upgrade') +wait_period_after_release = datetime.timedelta(days=30) + +distribution_info: dict = { + 'bullseye': { + 'version': 11, + 'next': 'bookworm', + 'release_date': datetime.datetime(2021, 8, 14, tzinfo=timezone.utc), + }, + 'bookworm': { + 'version': 12, + 'next': 'trixie', + 'release_date': datetime.datetime(2023, 6, 10, tzinfo=timezone.utc), + }, + 'trixie': { + 'version': 13, + 'next': 'forky', + 'release_date': datetime.datetime(2025, 8, 20, tzinfo=timezone.utc), + }, + 'forky': { + 'version': 14, + 'next': 'duke', + 'release_date': None + }, + 'duke': { + 'version': 15, + 'next': None, + 'release_date': None + }, + 'testing': { + 'version': None, + 'next': None, + 'release_date': None + }, + 'unstable': { + 'version': None, + 'next': None, + 'release_date': None + } +} + def _apt_run(arguments: list[str], enable_triggers: bool = False): """Run an apt command and ensure that output is written to stdout.""" @@ -62,65 +104,122 @@ def _sources_list_update(old_codename: str, new_codename: str): aug_path.rename(temp_sources_list) -def _get_new_codename(test_upgrade: bool) -> str | None: - """Return the codename for the next release.""" - release_file_dist = 'stable' - if test_upgrade: - release_file_dist = 'testing' +def _get_sources_list_codename() -> str | None: + """Return the codename set in the /etc/apt/sources.list file.""" + aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + + augeas.Augeas.NO_MODL_AUTOLOAD) + aug.transform('aptsources', str(sources_list)) + aug.set('/augeas/context', '/files' + str(sources_list)) + aug.load() - url = utils.RELEASE_FILE_URL.format(release_file_dist) - command = ['curl', '--silent', '--location', '--fail', url] - protocol = utils.get_http_protocol() - if protocol == 'tor+http': - command.insert(0, 'torsocks') - logging.info('Package download over Tor is enabled.') + dists = set() + for match_ in aug.match('*'): + dist = aug.get(match_ + '/distribution') + dist = dist.removesuffix('-updates') + dist = dist.removesuffix('-security') + dists.add(dist) - try: - output = subprocess.check_output(command).decode() - except (subprocess.CalledProcessError, FileNotFoundError): - logging.warning('Error while checking for new %s release', - release_file_dist) - else: - for line in output.split('\n'): - if line.startswith('Codename:'): - return line.split()[1] + if len(dists) != 1: + return None - return None + return dists.pop() -def _check(test_upgrade: bool = False) -> tuple[str, str]: +def get_status() -> dict[str, bool | str | None]: """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. + Various outcomes: + + - Unattended upgrades are not enabled. + - Distribution upgrades are not enabled. + - Not enough free space on the disk to perform dist upgrade. + - Dist upgrade already running. + - Codename in base-files package more recent than codename in sources.list. + Previous run of dist upgrade was interrupted. + - Could not determine the distribution. Mixed or unknown distribution. + - On testing/unstable rolling distributions. Nothing to do. + - On latest stable, no dist upgrade is available. Can upgrade to testing + (with codename). + - On old stable, waiting for cool-off period before upgrade. Manual upgrade + possible. + - On old stable, ready to do dist upgrade. Manual upgrade possible. - Return (boolean, string) indicating if the upgrade is ready, and a reason - if not. """ - if not utils.check_auto(): - raise RuntimeError('upgrades-not-enabled') + from plinth.modules import upgrades + updates_enabled = utils.check_auto() + dist_upgrade_enabled = upgrades.is_dist_upgrade_enabled() + has_free_space = utils.is_sufficient_free_space() + running = action_utils.service_is_running('freedombox-dist-upgrade') - if not utils.is_sufficient_free_space(): - raise RuntimeError('not-enough-free-space') + current_codename = _get_sources_list_codename() + status = { + 'updates_enabled': updates_enabled, + 'dist_upgrade_enabled': dist_upgrade_enabled, + 'has_free_space': has_free_space, + 'running': running, + 'current_codename': current_codename, + 'current_version': None, + 'current_release_date': None, + 'next_codename': None, + 'next_version': None, + 'next_release_date': None, + 'next_action': None, + 'next_action_date': None + } - if action_utils.service_is_running('freedombox-dist-upgrade'): - raise RuntimeError('found-previous') + if current_codename in (None, 'testing', 'unstable'): + return status - from plinth.modules.upgrades import get_current_release - release, old_codename = get_current_release() - if release in ['unstable', 'testing', 'n/a']: - raise RuntimeError(f'already-{release}') + _, base_files_codename = upgrades.get_current_release() + if current_codename == 'stable': + current_codename = base_files_codename - new_codename = _get_new_codename(test_upgrade) - if not new_codename: - raise RuntimeError('codename-not-found') + if current_codename not in distribution_info: + return status - if new_codename == old_codename: - raise RuntimeError(f'already-{old_codename}') + current_version = distribution_info[current_codename]['version'] + current_release_date = distribution_info[current_codename]['release_date'] + next_codename = distribution_info[current_codename]['next'] + next_version = None + next_release_date = None + if next_codename: + next_version = distribution_info[next_codename]['version'] + next_release_date = distribution_info[next_codename]['release_date'] - return old_codename, new_codename + next_action = None + now = datetime.datetime.now(tz=timezone.utc) + next_action_date = None + if next_release_date: + next_action_date = next_release_date + wait_period_after_release + + if running: + next_action = None + elif base_files_codename == next_codename: + next_action = 'continue' # Previous run was interrupted + elif (not next_release_date or not updates_enabled + or not dist_upgrade_enabled or not has_free_space): + next_action = None + elif now >= next_action_date: # type: ignore + next_action = 'ready' + elif now < next_release_date: + next_action = 'manual' + else: + next_action = 'wait_or_manual' + + status.update({ + 'current_codename': current_codename, + 'current_version': current_version, + 'current_release_date': current_release_date, + 'next_codename': next_codename, + 'next_version': next_version, + 'next_release_date': next_release_date, + 'next_action': next_action, + 'next_action_date': next_action_date + }) + return status @contextlib.contextmanager @@ -319,7 +418,7 @@ def perform(): _trigger_on_complete() -def start_service(test_upgrade: bool): +def start_service(): """Create dist upgrade service and start it.""" # Cleanup old service old_service_path = pathlib.Path( @@ -328,9 +427,8 @@ def start_service(test_upgrade: bool): old_service_path.unlink(missing_ok=True) action_utils.service_daemon_reload() - old_codename, new_codename = _check(test_upgrade) - - _sources_list_update(old_codename, new_codename) + status = get_status() + _sources_list_update(status['current_codename'], status['next_codename']) args = [ '--unit=freedombox-dist-upgrade', diff --git a/plinth/modules/upgrades/privileged.py b/plinth/modules/upgrades/privileged.py index 63737c4f0..cedc432c6 100644 --- a/plinth/modules/upgrades/privileged.py +++ b/plinth/modules/upgrades/privileged.py @@ -236,7 +236,7 @@ def activate_backports(develop: bool = False): @privileged -def start_dist_upgrade(test: bool = False): +def start_dist_upgrade(): """Start dist upgrade process. Check if a new stable release is available, and start dist-upgrade process @@ -244,7 +244,7 @@ def start_dist_upgrade(test: bool = False): """ _release_held_freedombox() - distupgrade.start_service(test) + distupgrade.start_service() @privileged diff --git a/plinth/modules/upgrades/templates/upgrades-dist-upgrade-confirm.html b/plinth/modules/upgrades/templates/upgrades-dist-upgrade-confirm.html new file mode 100644 index 000000000..28a88fdf1 --- /dev/null +++ b/plinth/modules/upgrades/templates/upgrades-dist-upgrade-confirm.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{% comment %} +# SPDX-License-Identifier: AGPL-3.0-or-later +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} +{% load static %} + +{% block content %} +
+
+ +{% endblock %} diff --git a/plinth/modules/upgrades/templates/upgrades-dist-upgrade.html b/plinth/modules/upgrades/templates/upgrades-dist-upgrade.html new file mode 100644 index 000000000..f2a8170b7 --- /dev/null +++ b/plinth/modules/upgrades/templates/upgrades-dist-upgrade.html @@ -0,0 +1,162 @@ +{% extends "base.html" %} +{% comment %} +# SPDX-License-Identifier: AGPL-3.0-or-later +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} +{% load static %} + +{% block content %} ++ {% blocktrans trimmed %} + Distribution update is currently running. This operation may take + several hours. Most apps will be unavailable during this period. + {% endblocktrans %} +
++ {% trans "Current Distribution:" %} + {% if not status.current_codename %} + {% trans "Unknown or mixed" %} + {% elif status.current_codename == "testing" and status.current_codename == "unstable" %} + {{ status.current_codename }} + ({% trans "Rolling release distribution" %}) + {% else %} + {{ status.current_version }} + ({{ status.current_codename }}) + {% endif %} + + {% if status.current_release_date %} + {% blocktrans trimmed with date=status.current_release_date|date %} + Released: {{ date }}. + {% endblocktrans %} + {% endif %} +
+ ++ {% trans "Next Stable Distribution:" %} + {% if not status.next_codename %} + {% trans "Unknown" %} + {% else %} + {{ status.next_version }} ({{ status.next_codename }}) + {% endif %} + + {% if status.next_release_date %} + {% blocktrans trimmed with date=status.next_release_date|date %} + Likely release: {{ date }}. + {% endblocktrans %} + {% endif %} +
+ ++ {% if status.next_codename and not status.next_release_date %} + {% blocktrans trimmed %} + Next stable distribution is not available yet. + {% endblocktrans %} + {% endif %} + + {% if status.current_codename == "testing" or status.current_codename == "unstable" %} + {% blocktrans trimmed %} + You are on a rolling release distribution. No distribution update + is necessary. Thank you for helping test the {{ box_name }} project. + Please report any problems you notice. + {% endblocktrans %} + {% endif %} + + {% if status.next_action == "continue" %} + {% blocktrans trimmed %} + A previous run of distribution update may have been interrupted. Please + re-run the distribution update. + {% endblocktrans %} + {% endif %} + + {% if status.next_action == "wait_or_manual" %} + {% blocktrans trimmed with period=status.next_action_date|timeuntil %} + A new stable distribution is available. Your {{ box_name }} will be + updated automatically in {{ period }}. You may choose to update + manually now, if you wish. + {% endblocktrans %} + {% endif %} + + {% if status.next_action == "ready" %} + {% blocktrans trimmed %} + A new stable distribution is available. Your {{ box_name }} will be + updated automatically soon. You may choose to update manually now, if + you wish. + {% endblocktrans %} + {% endif %} + + {% if status.next_action == "manual" %} + {% blocktrans trimmed %} + You are on the latest stable distribution. This is recommended. + However, if you wish to help beta test {{ box_name }} functionality, + you may update to next distribution manually. This setup may + experience occational app failures until the next stable release. + {% endblocktrans %} + {% endif %} +
+ ++ {% if status.next_action == "ready" or status.next_action == "wait_or_manual" %} + + {% trans "Start Distribution Update" %} + + {% elif status.next_action == "continue" %} + + {% trans "Continue Distribution Update" %} + + {% elif status.next_action == "manual" %} + + {% trans "Start Distribution Update (for testing)" %} + + {% else %} + + {% trans "Start Distribution Update" %} + + {% endif %} +
+ {% endif %} +{% endblock %} diff --git a/plinth/modules/upgrades/templates/upgrades_configure.html b/plinth/modules/upgrades/templates/upgrades_configure.html index 8ce839b43..a78eef30e 100644 --- a/plinth/modules/upgrades/templates/upgrades_configure.html +++ b/plinth/modules/upgrades/templates/upgrades_configure.html @@ -10,6 +10,11 @@ {% block status %} {{ block.super}} {% comment %} To extend instead of overwrite {% endcomment %} + +- {% blocktrans trimmed %} - This will attempt to upgrade the system from stable to - testing. It is meant only for development use. - {% endblocktrans %} -
- - {% endif %} {% endblock %} diff --git a/plinth/modules/upgrades/tests/test_distupgrade.py b/plinth/modules/upgrades/tests/test_distupgrade.py index ae86908c2..ed3ed8934 100644 --- a/plinth/modules/upgrades/tests/test_distupgrade.py +++ b/plinth/modules/upgrades/tests/test_distupgrade.py @@ -5,6 +5,8 @@ Test various part of the dist upgrade process. import re import subprocess +from datetime import datetime as datetime_original +from datetime import timezone from unittest.mock import call, patch import pytest @@ -74,75 +76,161 @@ deb https://deb.debian.org/debian bookwormish main assert temp_sources_list.read_text() == modified -@patch('plinth.modules.upgrades.utils.get_http_protocol') -@patch('subprocess.check_output') -def test_get_new_codename(check_output, get_http_protocol): - """Test that getting a new distro codename works.""" - get_http_protocol.return_value = 'http' - check_output.return_value = b''' -Suite: testing -Codename: trixie -Description: Debian Testing distribution -''' - assert distupgrade._get_new_codename(False) == 'trixie' - check_output.assert_called_with([ - 'curl', '--silent', '--location', '--fail', - 'https://deb.debian.org/debian/dists/stable/Release' - ]) +def test_get_sources_list_codename(tmp_path): + """Test retrieving codename from sources.list file.""" + list1 = ''' +deb http://deb.debian.org/debian bookworm main non-free-firmware +deb-src http://deb.debian.org/debian bookworm main non-free-firmware - assert distupgrade._get_new_codename(True) == 'trixie' - check_output.assert_called_with([ - 'curl', '--silent', '--location', '--fail', - 'https://deb.debian.org/debian/dists/testing/Release' - ]) +deb http://deb.debian.org/debian bookworm-updates main non-free-firmware +deb-src http://deb.debian.org/debian bookworm-updates main non-free-firmware - check_output.side_effect = FileNotFoundError('curl not found') - assert not distupgrade._get_new_codename(True) +deb http://security.debian.org/debian-security/ bookworm-security main non-free-firmware +deb-src http://security.debian.org/debian-security/ bookworm-security main non-free-firmware +''' # noqa: E501 + + list2 = ''' +deb http://deb.debian.org/debian stable main non-free-firmware +deb-src http://deb.debian.org/debian stable main non-free-firmware + +deb http://deb.debian.org/debian bookworm-updates main non-free-firmware +deb-src http://deb.debian.org/debian bookworm-updates main non-free-firmware + +deb http://security.debian.org/debian-security/ bookworm-security main non-free-firmware +deb-src http://security.debian.org/debian-security/ bookworm-security main non-free-firmware +''' # noqa: E501 + + sources_list = tmp_path / 'sources.list' + module = 'plinth.modules.upgrades.distupgrade' + with patch(f'{module}.sources_list', sources_list): + sources_list.write_text(list1) + assert distupgrade._get_sources_list_codename() == 'bookworm' + + sources_list.write_text(list2) + assert distupgrade._get_sources_list_codename() is None -@patch('plinth.modules.upgrades.distupgrade._get_new_codename') +@patch('datetime.datetime') @patch('plinth.modules.upgrades.get_current_release') +@patch('plinth.modules.upgrades.distupgrade._get_sources_list_codename') @patch('plinth.action_utils.service_is_running') @patch('plinth.modules.upgrades.utils.is_sufficient_free_space') +@patch('plinth.modules.upgrades.is_dist_upgrade_enabled') @patch('plinth.modules.upgrades.utils.check_auto') -def test_check(check_auto, is_sufficient_free_space, service_is_running, - get_current_release, get_new_codename): - """Test checking for available dist upgrade.""" +def test_get_status(check_auto, is_dist_upgrade_enabled, + is_sufficient_free_space, service_is_running, + get_sources_list_codename, get_current_release, datetime): + """Test getting status of distribution upgrade.""" + # All checks fail, sources.list has 'testing' value check_auto.return_value = False - with pytest.raises(RuntimeError, match='upgrades-not-enabled'): - distupgrade._check() - - check_auto.return_value = True + is_dist_upgrade_enabled.return_value = False is_sufficient_free_space.return_value = False - with pytest.raises(RuntimeError, match='not-enough-free-space'): - distupgrade._check() + service_is_running.return_value = True + get_sources_list_codename.return_value = 'testing' + status = distupgrade.get_status() + assert not status['updates_enabled'] + assert not status['dist_upgrade_enabled'] + assert not status['has_free_space'] + assert status['running'] + assert status['current_codename'] == 'testing' + assert status['current_version'] is None + assert status['current_release_date'] is None + assert status['next_codename'] is None + assert status['next_version'] is None + assert status['next_release_date'] is None + assert status['next_action'] is None + # sources.list has mixed values + get_sources_list_codename.return_value = None + status = distupgrade.get_status() + assert status['current_codename'] is None + assert status['current_version'] is None + + # sources.list has 'unstable' value + get_sources_list_codename.return_value = 'unstable' + status = distupgrade.get_status() + assert status['current_codename'] == 'unstable' + assert status['current_version'] is None + + # sources.list has an unknown value + get_sources_list_codename.return_value = 'x-invalid' + get_current_release.return_value = (None, None) + status = distupgrade.get_status() + assert status['current_codename'] == 'x-invalid' + assert status['current_version'] is None + + # sources.list has 'stable' + get_sources_list_codename.return_value = 'stable' + get_current_release.return_value = (None, 'bookworm') + status = distupgrade.get_status() + assert status['current_codename'] == 'bookworm' + assert status['current_version'] == 12 + + # All checks pass, next release not yet available + check_auto.return_value = True + is_dist_upgrade_enabled.return_value = True + is_sufficient_free_space.return_value = True + service_is_running.return_value = False + get_sources_list_codename.return_value = 'bookworm' + get_current_release.return_value = (None, 'bookworm') + datetime.now.return_value = datetime_original(2024, 8, 10, + tzinfo=timezone.utc) + status = distupgrade.get_status() + assert status['updates_enabled'] + assert status['dist_upgrade_enabled'] + assert status['has_free_space'] + assert not status['running'] + assert status['current_codename'] == 'bookworm' + assert status['current_version'] == 12 + current_date = datetime_original(2023, 6, 10, tzinfo=timezone.utc) + assert status['current_release_date'] == current_date + assert status['next_codename'] == 'trixie' + assert status['next_version'] == 13 + next_date = datetime_original(2025, 8, 20, tzinfo=timezone.utc) + assert status['next_release_date'] == next_date + assert status['next_action'] == 'manual' + + # Distribution upgrade interrupted + get_current_release.return_value = (None, 'trixie') + status = distupgrade.get_status() + assert status['next_action'] == 'continue' + + # Less than 30 days after release + get_current_release.return_value = (None, 'bookworm') + datetime.now.return_value = datetime_original(2025, 8, 30, + tzinfo=timezone.utc) + status = distupgrade.get_status() + assert status['next_action'] == 'wait_or_manual' + + # More than 30 days after release + datetime.now.return_value = datetime_original(2025, 9, 30, + tzinfo=timezone.utc) + status = distupgrade.get_status() + assert status['next_action'] == 'ready' + + # Next release date not available + get_sources_list_codename.return_value = 'trixie' + assert distupgrade.get_status()['next_action'] is None + + # Automatic updates not enabled + get_sources_list_codename.return_value = 'bookworm' + check_auto.return_value = False + assert distupgrade.get_status()['next_action'] is None + + # Distribution updates not enabled + check_auto.return_value = True + is_dist_upgrade_enabled.return_value = False + assert distupgrade.get_status()['next_action'] is None + + # Not enough free space + is_dist_upgrade_enabled.return_value = True + is_sufficient_free_space.return_value = False + assert distupgrade.get_status()['next_action'] is None + + # Distribution upgrade running is_sufficient_free_space.return_value = True service_is_running.return_value = True - with pytest.raises(RuntimeError, match='found-previous'): - distupgrade._check() - - service_is_running.return_value = False - for release in ['unstable', 'testing', 'n/a']: - get_current_release.return_value = (release, release) - with pytest.raises(RuntimeError, match=f'already-{release}'): - distupgrade._check() - - get_current_release.return_value = ('12', 'bookworm') - get_new_codename.return_value = None - with pytest.raises(RuntimeError, match='codename-not-found'): - distupgrade._check() - get_new_codename.assert_called_with(False) - - distupgrade._check(True) - get_new_codename.assert_called_with(True) - - get_new_codename.return_value = 'bookworm' - with pytest.raises(RuntimeError, match='already-bookworm'): - distupgrade._check() - - get_new_codename.return_value = 'trixie' - assert distupgrade._check() == ('bookworm', 'trixie') + assert distupgrade.get_status()['next_action'] is None @patch('subprocess.run') diff --git a/plinth/modules/upgrades/urls.py b/plinth/modules/upgrades/urls.py index 15955bd91..6eabe7bd9 100644 --- a/plinth/modules/upgrades/urls.py +++ b/plinth/modules/upgrades/urls.py @@ -1,7 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -URLs for the upgrades module -""" +"""URLs for the upgrades module.""" from django.urls import re_path @@ -16,6 +14,9 @@ urlpatterns = [ views.BackportsFirstbootView.as_view(), name='backports-firstboot'), re_path(r'^sys/upgrades/upgrade/$', views.upgrade, name='upgrade'), - re_path(r'^sys/upgrades/test-dist-upgrade/$', views.test_dist_upgrade, - name='test-dist-upgrade'), + re_path(r'^sys/upgrades/dist-upgrade/$', views.DistUpgradeView.as_view(), + name='dist-upgrade'), + re_path(r'^sys/upgrades/dist-upgrade/confirm/$', + views.DistUpgradeConfirmView.as_view(), + name='dist-upgrade-confirm'), ] diff --git a/plinth/modules/upgrades/views.py b/plinth/modules/upgrades/views.py index 7488827b6..7fa4c67c8 100644 --- a/plinth/modules/upgrades/views.py +++ b/plinth/modules/upgrades/views.py @@ -9,6 +9,7 @@ from django.http import HttpResponseRedirect from django.shortcuts import redirect from django.urls import reverse_lazy from django.utils.translation import gettext as _ +from django.views.generic import TemplateView from django.views.generic.edit import FormView from plinth import __version__ @@ -16,7 +17,7 @@ from plinth.modules import first_boot, upgrades from plinth.privileged import packages as packages_privileged from plinth.views import AppView, messages_error -from . import privileged +from . import distupgrade, privileged from .forms import BackportsFirstbootForm, ConfigureForm @@ -47,7 +48,6 @@ class UpgradesConfigurationView(AppView): context['version'] = __version__ context['new_version'] = is_newer_version_available() context['os_release'] = get_os_release() - context['can_test_dist_upgrade'] = upgrades.can_test_dist_upgrade() return context def form_valid(self, form): @@ -84,6 +84,38 @@ class UpgradesConfigurationView(AppView): return super().form_valid(form) +class DistUpgradeView(TemplateView): + """View to show status of distribution upgrade.""" + template_name = 'upgrades-dist-upgrade.html' + + def get_context_data(self, **kwargs): + """Return additional context for rendering the template.""" + context = super().get_context_data(**kwargs) + context['status'] = distupgrade.get_status() + context['refresh_page_sec'] = None + if context['status']['running']: + context['refresh_page_sec'] = 3 + + return context + + +class DistUpgradeConfirmView(TemplateView): + """View to confirm and trigger trigger distribution upgrade.""" + template_name = 'upgrades-dist-upgrade-confirm.html' + + def get_context_data(self, **kwargs): + """Return additional context for rendering the template.""" + context = super().get_context_data(**kwargs) + context['status'] = distupgrade.get_status() + return context + + def post(self, request): + """Start the distribution upgrade process.""" + privileged.start_dist_upgrade() + messages.success(request, _('Started distribution update.')) + return redirect(reverse_lazy('upgrades:dist-upgrade')) + + def is_newer_version_available(): """Return whether a newer Freedombox version is available.""" cache = Cache() @@ -167,12 +199,3 @@ class BackportsFirstbootView(FormView): upgrades.setup_repositories(None) first_boot.mark_step_done('backports_wizard') return super().form_valid(form) - - -def test_dist_upgrade(request): - """Test dist-upgrade from stable to testing.""" - if request.method == 'POST': - upgrades.test_dist_upgrade() - messages.success(request, _('Starting distribution upgrade test.')) - - return redirect(reverse_lazy('upgrades:index'))