# # This file is part of Plinth. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # """ Framework for installing and updating distribution packages """ from django.utils.translation import ugettext as _ import logging import subprocess from plinth.utils import import_from_gi glib = import_from_gi('GLib', '2.0') packagekit = import_from_gi('PackageKitGlib', '1.0') logger = logging.getLogger(__name__) transactions = {} packages_resolved = {} class PackageException(Exception): """A package operation has failed.""" def __init__(self, error_string=None, error_details=None, *args, **kwargs): """Store packagekit error string and details.""" super(PackageException, self).__init__(*args, **kwargs) self.error_string = error_string self.error_details = error_details def __str__(self): """Return the strin representation of the exception.""" return 'PackageException(error_string="{0}", error_details="{1}")' \ .format(self.error_string, self.error_details) class Transaction(object): """Information about an ongoing transaction.""" def __init__(self, package_names): """Initialize transaction object. Set most values to None until they are sent as progress update. """ self.package_names = package_names # Progress self.allow_cancel = None self.percentage = None self.status = None self.status_string = None self.flags = None self.package = None self.package_id = None self.item_progress = None self.role = None self.caller_active = None self.download_size_remaining = None self.speed = None def get_id(self): """Return a identifier to use as a key in a map of transactions.""" return frozenset(self.package_names) def __str__(self): """Return the string representation of the object""" return ('Transaction(packages={0}, allow_cancel={1}, status={2}, ' ' percentage={3}, package={4}, item_progress={5})').format( self.package_names, self.allow_cancel, self.status_string, self.percentage, self.package, self.item_progress) def install(self): """Run a PackageKit transaction to install given packages.""" try: self._do_install() except glib.Error as exception: raise PackageException(exception.message) from exception def _do_install(self): """Run a PackageKit transaction to install given packages. Raise exception in case of error. """ client = packagekit.Client() client.set_interactive(False) # Refresh package cache from all enabled repositories results = client.refresh_cache( False, None, self.progress_callback, self) self._assert_success(results) # Resolve packages again to get the latest versions after refresh results = client.resolve(packagekit.FilterEnum.INSTALLED, tuple(self.package_names) + (None, ), None, self.progress_callback, self) self._assert_success(results) for package in results.get_package_array(): packages_resolved[package.get_name()] = package package_ids = [] for package_name in self.package_names: if package_name not in packages_resolved or \ not packages_resolved[package_name]: raise PackageException(_('packages not found')) package_ids.append(packages_resolved[package_name].get_id()) # Start package installation results = client.install_packages( packagekit.TransactionFlagEnum.ONLY_TRUSTED, package_ids + [None], None, self.progress_callback, self) self._assert_success(results) def _assert_success(self, results): """Check that the most recent operation was a success.""" if results and results.get_error_code() is not None: error = results.get_error_code() error_code = error.get_code() if error else None error_string = packagekit.ErrorEnum.to_string(error_code) \ if error_code else None error_details = error.get_details() if error else None raise PackageException(error_string, error_details) def progress_callback(self, progress, progress_type, user_data): """Process progress updates on package resolve operation""" if progress_type == packagekit.ProgressType.PERCENTAGE: self.percentage = progress.props.percentage elif progress_type == packagekit.ProgressType.PACKAGE: self.package = progress.props.package elif progress_type == packagekit.ProgressType.ALLOW_CANCEL: self.allow_cancel = progress.props.allow_cancel elif progress_type == packagekit.ProgressType.PACKAGE_ID: self.package_id = progress.props.package_id elif progress_type == packagekit.ProgressType.ITEM_PROGRESS: self.item_progress = progress.props.item_progress elif progress_type == packagekit.ProgressType.STATUS: self.status = progress.props.status self.status_string = \ packagekit.StatusEnum.to_string(progress.props.status) elif progress_type == packagekit.ProgressType.TRANSACTION_FLAGS: self.flags = progress.props.transaction_flags elif progress_type == packagekit.ProgressType.ROLE: self.role = progress.props.role elif progress_type == packagekit.ProgressType.CALLER_ACTIVE: self.caller_active = progress.props.caller_active elif progress_type == packagekit.ProgressType.DOWNLOAD_SIZE_REMAINING: self.download_size_remaining = \ progress.props.download_size_remaining elif progress_type == packagekit.ProgressType.SPEED: self.speed = progress.props.speed else: logger.info('Unhandle packagekit progress callback - %s, %s', progress, progress_type) class AptTransaction(object): """Install a package using Apt.""" def __init__(self, package_names): """Initialize transaction object.""" self.package_names = package_names def install(self): """Run a PackageKit transaction to install given packages. Plinth needs to be running as root when calling this. Currently, this is meant to be only during first time setup when --setup is argument is passed. """ try: subprocess.run(['apt-get', 'update']) subprocess.run(['apt-get', '-y', 'install'] + self.package_names, check=True) except subprocess.CalledProcessError as exception: logger.exception('Error installing package: %s', exception) raise