upgrades: Detect and upgrade to next stable release

Process can be tested by upgrading to testing:
$ sudo ./actions/upgrades --develop --test-upgrade

Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
[sunil: cosmetic: isort fixes]
[sunil: Restore BACKPORTS_REQUESTED_KEY that was accidentally removed]
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
James Valleroy 2020-08-21 10:06:28 -04:00 committed by Sunil Mohan Adapa
parent 036f917efb
commit 0308d783e3
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
2 changed files with 133 additions and 23 deletions

View File

@ -11,10 +11,10 @@ import re
import subprocess import subprocess
import sys import sys
from plinth.action_utils import run_apt_command from plinth.action_utils import run_apt_command, service_restart
from plinth.modules.apache.components import check_url from plinth.modules.apache.components import check_url
from plinth.modules.upgrades import (get_current_release, is_backports_current, from plinth.modules.upgrades import (BACKPORTS_SOURCES_LIST, SOURCES_LIST,
SOURCES_LIST) get_current_release, is_backports_current)
AUTO_CONF_FILE = '/etc/apt/apt.conf.d/20auto-upgrades' AUTO_CONF_FILE = '/etc/apt/apt.conf.d/20auto-upgrades'
LOG_FILE = '/var/log/unattended-upgrades/unattended-upgrades.log' LOG_FILE = '/var/log/unattended-upgrades/unattended-upgrades.log'
@ -95,20 +95,28 @@ def parse_arguments():
setup_repositories.add_argument('--develop', required=False, default=False, setup_repositories.add_argument('--develop', required=False, default=False,
action='store_true', action='store_true',
help='Development mode') help='Development mode')
setup_repositories.add_argument(
'--test-upgrade', required=False, default=False, action='store_true',
help='Test dist-upgrade from stable to testing')
subparsers.required = True subparsers.required = True
return parser.parse_args() return parser.parse_args()
def subcommand_run(_): def _run():
"""Run unattended-upgrades""" """Run unattended-upgrades"""
subprocess.run(['dpkg', '--configure', '-a']) subprocess.run(['dpkg', '--configure', '-a'])
run_apt_command(['--fix-broken', 'install']) run_apt_command(['--fix-broken', 'install'])
subprocess.Popen(['systemctl', 'start', 'freedombox-manual-upgrade'],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, close_fds=True,
start_new_session=True)
def subcommand_run(_):
"""Run unattended-upgrades"""
try: try:
subprocess.Popen(['systemctl', 'start', 'freedombox-manual-upgrade'], _run()
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, close_fds=True,
start_new_session=True)
except FileNotFoundError: except FileNotFoundError:
print('Error: systemctl is not available.', file=sys.stderr) print('Error: systemctl is not available.', file=sys.stderr)
sys.exit(2) sys.exit(2)
@ -117,24 +125,28 @@ def subcommand_run(_):
sys.exit(3) sys.exit(3)
def subcommand_check_auto(_): def _check_auto():
"""Check if automatic upgrades are enabled""" """Check if automatic upgrades are enabled."""
arguments = [ arguments = [
'apt-config', 'shell', 'UpdateInterval', 'apt-config', 'shell', 'UpdateInterval',
'APT::Periodic::Update-Package-Lists' 'APT::Periodic::Update-Package-Lists'
] ]
try: output = subprocess.check_output(arguments).decode()
output = subprocess.check_output(arguments).decode()
except subprocess.CalledProcessError as error:
print('Error: {0}'.format(error), file=sys.stderr)
sys.exit(1)
update_interval = 0 update_interval = 0
match = re.match(r"UpdateInterval='(.*)'", output) match = re.match(r"UpdateInterval='(.*)'", output)
if match: if match:
update_interval = int(match.group(1)) update_interval = int(match.group(1))
print(bool(update_interval)) 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)
def subcommand_enable_auto(_): def subcommand_enable_auto(_):
@ -245,7 +257,7 @@ def _check_and_backports_sources(develop=False):
return return
print(f'{dist}-backports is now available. Adding to sources.') print(f'{dist}-backports is now available. Adding to sources.')
_add_backports_sources(SOURCES_LIST, protocol, dist) _add_backports_sources(BACKPORTS_SOURCES_LIST, protocol, dist)
# In case of dist upgrade, rewrite the preferences file. # In case of dist upgrade, rewrite the preferences file.
_add_apt_preferences() _add_apt_preferences()
@ -273,6 +285,97 @@ def _add_apt_preferences():
file_handle.write(APT_PREFERENCES_APPS) file_handle.write(APT_PREFERENCES_APPS)
def _check_and_dist_upgrade(develop=False, test_upgrade=False):
"""Check for new stable release. If there is one, and updates are
enabled, perform dist-upgrade.
If develop is True, check for possible upgrade from stable to testing.
If test_upgrade is True, also perform the upgrade to testing.
"""
release, dist = get_current_release()
if release in ['unstable', 'testing']:
print(f'System release is {release}. Skip checking for new stable '
'release.')
return
check_dists = ['stable']
if develop:
check_dists.append('testing')
codename = None
for check_dist in check_dists:
url = RELEASE_FILE_URL.format(check_dist)
command = ['curl', '--silent', '--location', '--fail', url]
protocol = _get_protocol()
if protocol == 'tor+http':
command.insert(0, 'torsocks')
print('Package download over Tor is enabled.')
try:
output = subprocess.check_output(command).decode()
except (subprocess.CalledProcessError, FileNotFoundError):
print(f'Error while checking for new {check_dist} release')
else:
for line in output.split('\n'):
if line.startswith('Codename:'):
codename = line.split()[1]
if not codename:
print('"Codename:" not found in release file.')
return
if codename == dist:
print(f'{dist} is already the latest release.')
return
if not _check_auto():
print('Automatic updates are not enabled.')
return
if check_dist == 'testing' and not test_upgrade:
print(f'Skipping dist-upgrade to {check_dist} since --test-upgrade is '
'not set.')
return
print(f'Upgrading from {dist} to {codename}...')
if check_dist == 'testing':
with open(SOURCES_LIST, 'r') as sources_list:
lines = sources_list.readlines()
with open(SOURCES_LIST, 'w') as sources_list:
for line in lines:
new_line = line.replace('stable', codename)
if 'security' in new_line:
new_line = new_line.replace('/updates', '-security')
sources_list.write(new_line)
run_apt_command(['update'])
run_apt_command(['install', 'base-files'])
run_apt_command(['install', 'unattended-upgrades'])
subprocess.run(['unattended-upgrade', '--verbose'])
# Remove obsolete packages that may prevent other packages from
# upgrading.
run_apt_command(['remove', 'libgcc1'])
# Hold packages known to have conffile prompts. FreedomBox service
# will handle their upgrade later.
HOLD_PACKAGES = ['firewalld', 'radicale']
try:
subprocess.run(['apt-mark', 'hold'] + HOLD_PACKAGES)
run_apt_command(['full-upgrade'])
finally:
subprocess.run(['apt-mark', 'unhold'] + HOLD_PACKAGES)
run_apt_command(['autoremove'])
# FreedomBox Service may have tried to restart several times
# during the upgrade, but failed. It will stop trying after 5
# retries. Restart it once more to ensure it is running.
service_restart('plinth')
def subcommand_setup(_): def subcommand_setup(_):
"""Setup apt preferences.""" """Setup apt preferences."""
_add_apt_preferences() _add_apt_preferences()
@ -284,8 +387,11 @@ def subcommand_setup_repositories(arguments):
Repositories list for now only contains the backports. If the file exists, Repositories list for now only contains the backports. If the file exists,
assume that it contains backports. assume that it contains backports.
Check if a new stable release is available, and perform
dist-upgrade if updates are enabled.
""" """
_check_and_backports_sources(arguments.develop) _check_and_backports_sources(arguments.develop)
_check_and_dist_upgrade(arguments.develop, arguments.test_upgrade)
def main(): def main():

View File

@ -45,7 +45,9 @@ app = None
BACKPORTS_REQUESTED_KEY = 'upgrades_backports_requested' BACKPORTS_REQUESTED_KEY = 'upgrades_backports_requested'
SOURCES_LIST = '/etc/apt/sources.list.d/freedombox2.list' SOURCES_LIST = '/etc/apt/sources.list'
BACKPORTS_SOURCES_LIST = '/etc/apt/sources.list.d/freedombox2.list'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -72,8 +74,10 @@ class UpgradesApp(app_module.App):
self._show_new_release_notification() self._show_new_release_notification()
# Check every day for setting up apt backport sources, every 3 minutes # Check every day (every 3 minutes in debug mode):
# in debug mode. # - backports becomes available -> configure it if selected by user
# - new stable release becomes available -> perform dist-upgrade if
# updates are enabled
interval = 180 if cfg.develop else 24 * 3600 interval = 180 if cfg.develop else 24 * 3600
glib.schedule(interval, setup_repositories) glib.schedule(interval, setup_repositories)
@ -146,7 +150,7 @@ def disable():
def setup_repositories(data): def setup_repositories(data):
"""Setup apt backport repositories.""" """Setup apt repositories for backports or new stable release."""
if is_backports_requested(): if is_backports_requested():
command = ['setup-repositories'] command = ['setup-repositories']
if cfg.develop: if cfg.develop:
@ -170,7 +174,7 @@ def set_backports_requested(requested):
def is_backports_enabled(): def is_backports_enabled():
"""Return whether backports are enabled in the system configuration.""" """Return whether backports are enabled in the system configuration."""
return os.path.exists(SOURCES_LIST) return os.path.exists(BACKPORTS_SOURCES_LIST)
def get_current_release(): def get_current_release():