mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
Run `apt-get --fix-broken install` before installing package or manual update. This will attempt to correct broken dependencies. Tests: - Install a package without its dependencies using `dpkg -i`. - Both app install and manual update successfully recover from this situation. Signed-off-by: James Valleroy <jvalleroy@mailbox.org> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
266 lines
8.5 KiB
Python
Executable File
266 lines
8.5 KiB
Python
Executable File
#!/usr/bin/python3
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""
|
|
Configures or runs unattended-upgrades
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
from plinth.action_utils import run_apt_command
|
|
from plinth.modules.apache.components import check_url
|
|
|
|
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'
|
|
BUSTER_BACKPORTS_RELEASE_FILE_URL = \
|
|
'https://deb.debian.org/debian/dists/buster-backports/Release'
|
|
|
|
# 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 = '''Explanation: This file is managed by FreedomBox, do not edit.
|
|
Explanation: Allow carefully selected updates to 'freedombox' from backports.
|
|
Package: freedombox
|
|
Pin: release a=buster-backports
|
|
Pin-Priority: 500
|
|
|
|
Explanation: matrix-synapse 0.99.5 introduces room version 4. Older version
|
|
Explanation: 0.99.2 in buster won't be able join newly created rooms.
|
|
Package: matrix-synapse
|
|
Pin: release a=buster-backports
|
|
Pin-Priority: 500
|
|
|
|
Explanation: matrix-synapse >= 1.2 requires python3-service-identity >= 18.1
|
|
Package: python3-service-identity
|
|
Pin: release a=buster-backports
|
|
Pin-Priority: 500
|
|
|
|
Explanation: matrix-synapse >= 1.5 requires python3-typing-extensions >= 3.7.4
|
|
Package: python3-typing-extensions
|
|
Pin: release a=buster-backports
|
|
Pin-Priority: 500
|
|
|
|
Explanation: matrix-synapse >= 1.11 requires python3-signedjson >= 1.1.0
|
|
Package: python3-signedjson
|
|
Pin: release a=buster-backports
|
|
Pin-Priority: 500
|
|
|
|
Explanation: matrix-synapse >= 1.12 requires python3-twisted >= 18.9.0-8~
|
|
Package: python3-twisted
|
|
Pin: release a=buster-backports
|
|
Pin-Priority: 500
|
|
|
|
Explanation: python3-twisted requires matching version of python3-twisted-bin
|
|
Package: python3-twisted-bin
|
|
Pin: release a=buster-backports
|
|
Pin-Priority: 500
|
|
'''
|
|
|
|
|
|
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')
|
|
subparsers.add_parser('setup-repositories',
|
|
help='Setup software repositories for FreedomBox')
|
|
|
|
subparsers.required = True
|
|
return parser.parse_args()
|
|
|
|
|
|
def subcommand_run(_):
|
|
"""Run unattended-upgrades"""
|
|
run_apt_command(['--fix-broken', 'install'])
|
|
try:
|
|
subprocess.Popen(['systemctl', 'start', 'freedombox-manual-upgrade'],
|
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL, close_fds=True,
|
|
start_new_session=True)
|
|
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 subcommand_check_auto(_):
|
|
"""Check if automatic upgrades are enabled"""
|
|
arguments = [
|
|
'apt-config', 'shell', 'UpdateInterval',
|
|
'APT::Periodic::Update-Package-Lists'
|
|
]
|
|
try:
|
|
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
|
|
match = re.match(r"UpdateInterval='(.*)'", output)
|
|
if match:
|
|
update_interval = int(match.group(1))
|
|
|
|
print(bool(update_interval))
|
|
|
|
|
|
def subcommand_enable_auto(_):
|
|
"""Enable automatic upgrades"""
|
|
with open(AUTO_CONF_FILE, 'w') 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"""
|
|
with open(AUTO_CONF_FILE, 'w') 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."""
|
|
try:
|
|
print('==> ' + os.path.basename(LOG_FILE))
|
|
with open(LOG_FILE, 'r') as file_handle:
|
|
print(file_handle.read())
|
|
except IOError:
|
|
pass
|
|
|
|
try:
|
|
print('==> ' + os.path.basename(DPKG_LOG_FILE))
|
|
with open(DPKG_LOG_FILE, 'r') as file_handle:
|
|
print(file_handle.read())
|
|
except IOError:
|
|
pass
|
|
|
|
|
|
def _get_protocol():
|
|
"""Return the protocol to use for newly added repository sources."""
|
|
try:
|
|
from plinth.modules.tor import utils
|
|
if utils.is_apt_transport_tor_enabled():
|
|
return 'tor+http'
|
|
except Exception:
|
|
pass
|
|
|
|
return 'http'
|
|
|
|
|
|
def _is_release_file_available(protocol):
|
|
"""Return whether the release for backports is available."""
|
|
wrapper = None
|
|
if protocol == 'tor+http':
|
|
wrapper = 'torsocks'
|
|
|
|
result = check_url(BUSTER_BACKPORTS_RELEASE_FILE_URL, wrapper=wrapper)
|
|
return result == 'passed'
|
|
|
|
|
|
def _add_buster_backports_sources(sources_list, protocol):
|
|
"""Add buster 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 buster-backports main
|
|
deb-src {protocol}://deb.debian.org/debian buster-backports main
|
|
'''
|
|
sources = sources.format(protocol=protocol)
|
|
with open(sources_list, 'w') as file_handle:
|
|
file_handle.write(sources)
|
|
|
|
|
|
def _check_and_backports_sources():
|
|
"""Add buster 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)
|
|
|
|
sources_list = '/etc/apt/sources.list.d/freedombox2.list'
|
|
if os.path.exists(sources_list):
|
|
print('Repositories list up-to-date. Skipping update.')
|
|
return
|
|
|
|
try:
|
|
with open('/etc/dpkg/origins/default', 'r') as default_origin:
|
|
matches = [
|
|
re.match(r'Vendor:\s+Debian', line, flags=re.IGNORECASE)
|
|
for line in default_origin.readlines()
|
|
]
|
|
except FileNotFoundError:
|
|
print('Could not open /etc/dpkg/origins/default')
|
|
return
|
|
|
|
if not any(matches):
|
|
print('System is running a derivative of Debian. Skip enabling '
|
|
'backports.')
|
|
return
|
|
|
|
protocol = _get_protocol()
|
|
if protocol == 'tor+http':
|
|
print('Package download over Tor is enabled.')
|
|
|
|
if not _is_release_file_available(protocol):
|
|
print('Release file for Buster backports is not available yet.')
|
|
return
|
|
|
|
print('Buster backports is now available. Adding to sources.')
|
|
_add_buster_backports_sources(sources_list, protocol)
|
|
|
|
|
|
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.
|
|
|
|
with open(base_path / '50freedombox4.pref', 'w') as file_handle:
|
|
file_handle.write(APT_PREFERENCES)
|
|
|
|
|
|
def subcommand_setup(_):
|
|
"""Setup apt preferences."""
|
|
_add_apt_preferences()
|
|
|
|
|
|
def subcommand_setup_repositories(_):
|
|
"""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()
|
|
|
|
|
|
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()
|