mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-04-08 09:41:35 +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>
260 lines
8.5 KiB
Python
260 lines
8.5 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""Configure or run unattended-upgrades."""
|
|
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
|
|
from plinth.action_utils import (apt_hold_flag, apt_unhold_freedombox,
|
|
is_package_manager_busy, run_apt_command,
|
|
service_is_running)
|
|
from plinth.actions import privileged
|
|
|
|
from . import distupgrade, utils
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
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'
|
|
DPKG_LOG_FILE = '/var/log/unattended-upgrades/unattended-upgrades-dpkg.log'
|
|
|
|
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 n={}-backports
|
|
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.
|
|
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.
|
|
Explanation: matrix-synapse >= 1.92.0-3 requires
|
|
Explanation: python3-canonicaljson >= 2.0.0~
|
|
Package: python3-canonicaljson
|
|
Pin: release n=bookworm-backports
|
|
Pin-Priority: 500
|
|
'''
|
|
|
|
|
|
def _release_held_freedombox():
|
|
"""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()
|
|
|
|
|
|
@privileged
|
|
def release_held_packages():
|
|
"""Release any packages that are being held."""
|
|
if is_package_manager_busy():
|
|
logger.warning('Package manager is busy, skipping releasing holds.')
|
|
return
|
|
|
|
if service_is_running('freedombox-dist-upgrade'):
|
|
logger.warning('Distribution upgrade in progress, skipping releasing '
|
|
'holds.')
|
|
return
|
|
|
|
output = subprocess.check_output(['apt-mark', 'showhold']).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)
|
|
|
|
|
|
@privileged
|
|
def run():
|
|
"""Run unattended-upgrades."""
|
|
subprocess.run(['dpkg', '--configure', '-a'], check=False)
|
|
run_apt_command(['--fix-broken', 'install'])
|
|
_release_held_freedombox()
|
|
|
|
subprocess.Popen(['systemctl', 'start', 'freedombox-manual-upgrade'],
|
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL, close_fds=True,
|
|
start_new_session=True)
|
|
|
|
|
|
@privileged
|
|
def check_auto() -> bool:
|
|
"""Check if automatic upgrades are enabled."""
|
|
return utils.check_auto()
|
|
|
|
|
|
@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')
|
|
|
|
|
|
@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')
|
|
|
|
|
|
@privileged
|
|
def get_log() -> str:
|
|
"""Return the automatic upgrades log."""
|
|
log_lines = []
|
|
try:
|
|
log_lines.append('==> ' + os.path.basename(LOG_FILE))
|
|
with open(LOG_FILE, 'r', encoding='utf-8') as file_handle:
|
|
log_lines.append(file_handle.read())
|
|
except IOError:
|
|
pass
|
|
|
|
try:
|
|
log_lines.append('==> ' + os.path.basename(DPKG_LOG_FILE))
|
|
with open(DPKG_LOG_FILE, 'r', encoding='utf-8') as file_handle:
|
|
log_lines.append(file_handle.read())
|
|
except IOError:
|
|
pass
|
|
|
|
return '\n'.join(log_lines)
|
|
|
|
|
|
def _add_backports_sources(sources_list: str, protocol: str, dist: str):
|
|
"""Add backports sources to freedombox repositories list."""
|
|
sources = '''# This file is managed by FreedomBox, do not edit.
|
|
# Allow carefully selected updates to 'freedombox' from backports.
|
|
|
|
deb {protocol}://deb.debian.org/debian {dist}-backports main
|
|
deb-src {protocol}://deb.debian.org/debian {dist}-backports main
|
|
'''
|
|
sources = sources.format(protocol=protocol, dist=dist)
|
|
with open(sources_list, 'w', encoding='utf-8') as file_handle:
|
|
file_handle.write(sources)
|
|
|
|
|
|
def _check_and_backports_sources(develop=False):
|
|
"""Add backports sources after checking if it is available."""
|
|
old_sources_list = '/etc/apt/sources.list.d/freedombox.list'
|
|
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():
|
|
logging.info('Repositories list up-to-date. Skipping update.')
|
|
return
|
|
|
|
try:
|
|
with open('/etc/dpkg/origins/default', 'r',
|
|
encoding='utf-8') as default_origin:
|
|
matches = [
|
|
re.match(r'Vendor:\s+(Debian|FreedomBox)', line,
|
|
flags=re.IGNORECASE)
|
|
for line in default_origin.readlines()
|
|
]
|
|
except FileNotFoundError:
|
|
logging.info('Could not open /etc/dpkg/origins/default')
|
|
return
|
|
|
|
if not any(matches):
|
|
logging.info('System is running a derivative of Debian. Skip enabling '
|
|
'backports.')
|
|
return
|
|
|
|
release, dist = get_current_release()
|
|
if release in ['unstable', 'testing', 'n/a'] and not develop:
|
|
logging.info(f'System release is {release}. Skip enabling backports.')
|
|
return
|
|
|
|
protocol = utils.get_http_protocol()
|
|
if protocol == 'tor+http':
|
|
logging.info('Package download over Tor is enabled.')
|
|
|
|
if not utils.is_release_file_available(protocol, dist, backports=True):
|
|
logging.info(
|
|
f'Release file for {dist}-backports is not available yet.')
|
|
return
|
|
|
|
print(f'{dist}-backports is now available. Adding to sources.')
|
|
_add_backports_sources(BACKPORTS_SOURCES_LIST, protocol, dist)
|
|
# In case of dist upgrade, rewrite the preferences file.
|
|
_add_apt_preferences()
|
|
|
|
|
|
def _add_apt_preferences():
|
|
"""Setup APT preferences to upgrade selected packages from backports."""
|
|
base_path = pathlib.Path('/etc/apt/preferences.d')
|
|
for file_name in ['50freedombox.pref', '50freedombox2.pref']:
|
|
full_path = base_path / file_name
|
|
if full_path.exists():
|
|
full_path.unlink()
|
|
|
|
# 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':
|
|
logging.info(
|
|
f'System distribution is {dist}. Skip setting apt preferences '
|
|
'for backports.')
|
|
else:
|
|
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))
|
|
with open(base_path / '51freedombox-apps.pref', 'w',
|
|
encoding='utf-8') as file_handle:
|
|
file_handle.write(APT_PREFERENCES_APPS)
|
|
|
|
|
|
@privileged
|
|
def setup():
|
|
"""Setup apt preferences."""
|
|
_add_apt_preferences()
|
|
|
|
|
|
@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(develop)
|
|
|
|
|
|
@privileged
|
|
def start_dist_upgrade():
|
|
"""Start dist upgrade process.
|
|
|
|
Check if a new stable release is available, and start dist-upgrade process
|
|
if updates are enabled.
|
|
"""
|
|
_release_held_freedombox()
|
|
|
|
distupgrade.start_service()
|
|
|
|
|
|
@privileged
|
|
def dist_upgrade():
|
|
"""Perform major distribution upgrade."""
|
|
distupgrade.perform()
|
|
|
|
|
|
@privileged
|
|
def dist_upgrade_on_complete():
|
|
"""Perform cleanup operations after distribution upgrade."""
|
|
distupgrade.on_complete()
|