mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
Closes: #2090 - Create a new page for distribution upgrade. - If distribution upgrade is running show its status here without any other UI. - Show various conditions for not allowing distribution upgrades. - Automatic updates disabled - Distribution updates disabled - Not enough free space. - Unknown or mixed distribution in sources.list. - If distribution upgrade was interrupted, show that information here and allow triggering distribution upgrade again. This is detected by noticing that codename in base-files is higher than one detected in sources.list. - If the user is not testing/unstable, show a message and don't allow triggering. - If next stable has not been released, don't auto-upgrade but allow manual upgrade. Show special warnings. - If next stable has been released but only recently, don't auto-upgrade but allow manual upgrade. - If next stable has been released and it has been 30 days, allow auto-upgrade and manual upgrade. - Seek confirmation before triggering manual upgrade. Provide appropriate advice. - Rely on hard-coded list of releases and their release dates instead of querying the server. Tests: - When automatic updates or distribution updates are disabled, an alert message is shown distribution upgrade page. If both are disabled, both messages show up in the alert. The start distribution upgrade button is disabled. Clicking on the button does not work. - Reducing the available free disk space will cause alert message to show up and start upgrade button to be disabled. - When the distribution in /etc/apt/sources.list is mixed or unknown, an alert message is shown. the start distribution upgrade button is disabled. - When the distribution in /etc/apt/sources.list is testing or unstable, an alert message is shown "You are on a rolling release distribution...". the start distribution upgrade button is disabled. The current distribution is 'None (testing)' or 'None (unstable)'. Next stable distribution is Unknown. - If get_current_release is hard-coded to return (None, 'trixie'). Then a message is show in the distribution update page 'A previous run of distribution update may have been interrupted. Please re-run the distribution update.' A 'Continue Distribution Update' button is shown in warning color. The button takes to confirm page where the confirm button is shown in blue and is enabled. - On a bookworm VM, visiting the page shows the message "You are on the latest stable distribution...". Upgrade button shows in red. Clicking it takes to confirmation page. The page shows a warning alert and red confirmation button. - Setting the clock to '2025-08-21' shows the message "A new stable distribution is available. Your FreedomBox will be update automatically in 4 weeks...". Upgrade button shows in blue. Clicking it takes to confirmation page. The page does show warning. The button is in blue. - Setting the clock to '2025-09-30' shows the message "A new status distribution is available. Your FreedomBox will be updated automatically soon...". Upgrade button shows in blue. Clicking it takes to confirmation page. The page does show warning. The button is in blue. - Clicking the confirmation button starts the distribution upgrade process. This distribution upgrade page is shown. The page shows spinner with a message and no other UI. Page is refreshed every 3 seconds. When the distribution upgrade process is completed, the page shows the current status. - Killing the apt-get process during distribution upgrade stop the page refresh. The page shows that process was interrupted and also continuation. Clicking on the confirmation button resumes the distribution upgrade process. - After distribution upgrade, the page shows the current distribution and next distribution properly. There is not release date for the next distribution. A message shows: "Next stable distribution is not available yet." Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
305 lines
10 KiB
Python
305 lines
10 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""FreedomBox app for upgrades."""
|
|
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
|
|
from aptsources import sourceslist
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils.translation import gettext_noop
|
|
|
|
import plinth
|
|
from plinth import action_utils
|
|
from plinth import app as app_module
|
|
from plinth import cfg, glib, kvstore, menu, package
|
|
from plinth.config import DropinConfigs
|
|
from plinth.daemon import RelatedDaemon
|
|
from plinth.diagnostic_check import DiagnosticCheck, Result
|
|
from plinth.modules.backups.components import BackupRestore
|
|
from plinth.package import Packages
|
|
|
|
from . import distupgrade, manifest, privileged
|
|
|
|
first_boot_steps = [
|
|
{
|
|
'id': 'backports_wizard',
|
|
'url': 'upgrades:backports-firstboot',
|
|
'order': 5,
|
|
},
|
|
]
|
|
|
|
_description = [
|
|
_('Check for and apply the latest software and security updates.'),
|
|
_('Updates are run at 06:00 everyday according to local time zone. Set '
|
|
'your time zone in Date & Time app. Apps are restarted after update '
|
|
'causing them to be unavailable briefly. If system reboot is deemed '
|
|
'necessary, it is done automatically at 02:00 causing all apps to be '
|
|
'unavailable briefly.')
|
|
]
|
|
|
|
BACKPORTS_REQUESTED_KEY = 'upgrades_backports_requested'
|
|
|
|
DIST_UPGRADE_ENABLED_KEY = 'upgrades_dist_upgrade_enabled'
|
|
|
|
PKG_HOLD_DIAG_CHECK_ID = 'upgrades-package-holds'
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class UpgradesApp(app_module.App):
|
|
"""FreedomBox app for software upgrades."""
|
|
|
|
app_id = 'upgrades'
|
|
|
|
_version = 18
|
|
|
|
can_be_disabled = False
|
|
|
|
def __init__(self) -> None:
|
|
"""Create components for the app."""
|
|
super().__init__()
|
|
|
|
info = app_module.Info(app_id=self.app_id, version=self._version,
|
|
is_essential=True, name=_('Software Update'),
|
|
icon='fa-refresh', description=_description,
|
|
manual_page='Upgrades', tags=manifest.tags)
|
|
self.add(info)
|
|
|
|
menu_item = menu.Menu('menu-upgrades', info.name, info.icon, info.tags,
|
|
'upgrades:index',
|
|
parent_url_name='system:system', order=50)
|
|
self.add(menu_item)
|
|
|
|
packages = Packages('packages-upgrades',
|
|
['unattended-upgrades', 'needrestart'])
|
|
self.add(packages)
|
|
|
|
dropin_configs = DropinConfigs('dropin-configs-upgrades', [
|
|
'/etc/apt/apt.conf.d/20freedombox',
|
|
'/etc/apt/apt.conf.d/20freedombox-allow-release-info-change',
|
|
'/etc/apt/apt.conf.d/60unattended-upgrades',
|
|
'/etc/needrestart/conf.d/freedombox.conf',
|
|
])
|
|
self.add(dropin_configs)
|
|
|
|
daemon = RelatedDaemon('related-daemon-upgrades',
|
|
'freedombox-dist-upgrade')
|
|
self.add(daemon)
|
|
|
|
backup_restore = BackupRestore('backup-restore-upgrades',
|
|
**manifest.backup)
|
|
self.add(backup_restore)
|
|
|
|
def post_init(self):
|
|
"""Perform post initialization operations."""
|
|
self._show_new_release_notification()
|
|
|
|
# Check every day if backports becomes available, then configure it if
|
|
# selected by user.
|
|
glib.schedule(24 * 3600, setup_repositories)
|
|
|
|
# Check every day if new stable release becomes available, then perform
|
|
# dist-upgrade if updates are enabled
|
|
glib.schedule(24 * 3600, check_dist_upgrade)
|
|
|
|
def _show_new_release_notification(self):
|
|
"""When upgraded to new release, show a notification."""
|
|
from plinth.notification import Notification
|
|
try:
|
|
note = Notification.get('upgrades-new-release')
|
|
if note.data['version'] == plinth.__version__:
|
|
# User already has notification for update to this version. It
|
|
# may be dismissed or not yet dismissed
|
|
return
|
|
|
|
# User currently has a notification for an older version, update.
|
|
dismiss = False
|
|
except KeyError:
|
|
# Don't show notification for the first version user runs, create
|
|
# but don't show it.
|
|
dismiss = True
|
|
|
|
data = {
|
|
'version': plinth.__version__,
|
|
'app_name': 'translate:' + gettext_noop('Software Update'),
|
|
'app_icon': 'fa-refresh'
|
|
}
|
|
title = gettext_noop('FreedomBox Updated')
|
|
note = Notification.update_or_create(
|
|
id='upgrades-new-release', app_id='upgrades', severity='info',
|
|
title=title, body_template='upgrades-new-release.html', data=data,
|
|
group='admin')
|
|
note.dismiss(should_dismiss=dismiss)
|
|
|
|
def _show_first_manual_update_notification(self):
|
|
"""After first setup, show notification to manually run updates."""
|
|
from plinth.notification import Notification
|
|
title = gettext_noop('Run software update manually')
|
|
message = gettext_noop(
|
|
'Automatic software update runs daily by default. For the first '
|
|
'time, manually run it now.')
|
|
data = {
|
|
'app_name': 'translate:' + gettext_noop('Software Update'),
|
|
'app_icon': 'fa-refresh'
|
|
}
|
|
actions = [{
|
|
'type': 'link',
|
|
'class': 'primary',
|
|
'text': gettext_noop('Go to {app_name}'),
|
|
'url': 'upgrades:index'
|
|
}, {
|
|
'type': 'dismiss'
|
|
}]
|
|
Notification.update_or_create(id='upgrades-first-manual-update',
|
|
app_id='upgrades', severity='info',
|
|
title=title, message=message,
|
|
actions=actions, data=data,
|
|
group='admin', dismissed=False)
|
|
|
|
def setup(self, old_version):
|
|
"""Install and configure the app."""
|
|
super().setup(old_version)
|
|
|
|
# Enable automatic upgrades but only on first install
|
|
if not old_version and not cfg.develop:
|
|
privileged.enable_auto()
|
|
|
|
# Request user to run manual update as a one time activity
|
|
if not old_version:
|
|
self._show_first_manual_update_notification()
|
|
|
|
# Update apt preferences whenever on first install and on version
|
|
# increment.
|
|
privileged.setup()
|
|
|
|
# When upgrading from a version without first boot wizard for
|
|
# backports, assume backports have been requested.
|
|
if old_version and old_version < 7:
|
|
set_backports_requested(can_activate_backports())
|
|
|
|
# Enable dist upgrade for new installs, and once when upgrading
|
|
# from version without flag.
|
|
if not old_version or old_version < 8:
|
|
set_dist_upgrade_enabled(can_enable_dist_upgrade())
|
|
|
|
# Try to setup apt repositories, if needed, if possible, on first
|
|
# install and on version increment.
|
|
setup_repositories(None)
|
|
|
|
def diagnose(self) -> list[DiagnosticCheck]:
|
|
"""Run diagnostics and return the results."""
|
|
results = super().diagnose()
|
|
results.append(_diagnose_held_packages())
|
|
return results
|
|
|
|
def repair(self, failed_checks: list) -> bool:
|
|
"""Handle repair for custom diagnostic."""
|
|
remaining_checks = []
|
|
for check in failed_checks:
|
|
if check.check_id == PKG_HOLD_DIAG_CHECK_ID:
|
|
privileged.release_held_packages()
|
|
else:
|
|
remaining_checks.append(check)
|
|
|
|
return super().repair(remaining_checks)
|
|
|
|
|
|
def setup_repositories(_):
|
|
"""Setup apt repositories for backports."""
|
|
if is_backports_requested():
|
|
privileged.activate_backports(cfg.develop)
|
|
|
|
|
|
def check_dist_upgrade(_):
|
|
"""Check for upgrade to new stable release."""
|
|
if is_dist_upgrade_enabled():
|
|
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():
|
|
"""Return whether user has chosen to activate backports."""
|
|
return kvstore.get_default(BACKPORTS_REQUESTED_KEY, False)
|
|
|
|
|
|
def set_backports_requested(requested):
|
|
"""Set whether user has chosen to activate backports."""
|
|
kvstore.set(BACKPORTS_REQUESTED_KEY, requested)
|
|
logger.info('Backports requested - %s', requested)
|
|
|
|
|
|
def is_dist_upgrade_enabled():
|
|
"""Return whether user has enabled dist upgrade."""
|
|
return kvstore.get_default(DIST_UPGRADE_ENABLED_KEY, False)
|
|
|
|
|
|
def set_dist_upgrade_enabled(enabled=True):
|
|
"""Set whether user has enabled dist upgrade."""
|
|
kvstore.set(DIST_UPGRADE_ENABLED_KEY, enabled)
|
|
logger.info('Distribution upgrade configured - %s', enabled)
|
|
|
|
|
|
def is_backports_enabled():
|
|
"""Return whether backports are enabled in the system configuration."""
|
|
return os.path.exists(privileged.BACKPORTS_SOURCES_LIST)
|
|
|
|
|
|
def get_current_release():
|
|
"""Return current release and codename as a tuple."""
|
|
output = subprocess.check_output(
|
|
['lsb_release', '--release', '--codename',
|
|
'--short']).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():
|
|
return False
|
|
|
|
_, dist = get_current_release()
|
|
dist += '-backports'
|
|
sources = sourceslist.SourcesList()
|
|
for source in sources:
|
|
if source.dist == dist:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def can_activate_backports():
|
|
"""Return whether backports can be activated."""
|
|
if cfg.develop:
|
|
return True
|
|
|
|
# Release will be 'n/a' in latest unstable and testing distributions.
|
|
release, _ = get_current_release()
|
|
return release not in ['unstable', 'testing', 'n/a']
|
|
|
|
|
|
def can_enable_dist_upgrade():
|
|
"""Return whether dist upgrade can be enabled."""
|
|
release, _ = get_current_release()
|
|
return release not in ['unstable', 'testing', 'n/a']
|
|
|
|
|
|
def _diagnose_held_packages():
|
|
"""Check if any packages have holds."""
|
|
check = DiagnosticCheck(PKG_HOLD_DIAG_CHECK_ID,
|
|
gettext_noop('Check for package holds'),
|
|
Result.NOT_DONE)
|
|
if (package.is_package_manager_busy()
|
|
or action_utils.service_is_running('freedombox-dist-upgrade')):
|
|
check.result = Result.SKIPPED
|
|
return check
|
|
|
|
output = subprocess.check_output(['apt-mark', 'showhold']).decode().strip()
|
|
held_packages = output.split()
|
|
check.result = Result.FAILED if held_packages else Result.PASSED
|
|
return check
|