package: Implement low-level methods for uninstalling

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2022-08-18 22:02:24 -07:00 committed by James Valleroy
parent 145b3ecc65
commit 1908bd5366
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
4 changed files with 57 additions and 33 deletions

View File

@ -51,6 +51,8 @@ def parse_arguments():
help='list of packages to install')
subparser = subparsers.add_parser('remove', help='remove the package(s)')
subparser.add_argument(
'app_id', help='ID of app for which package is being uninstalled')
subparser.add_argument('--packages', required=True,
help='List of packages to remove', nargs='+')
@ -107,8 +109,19 @@ def subcommand_install(arguments):
def subcommand_remove(arguments):
"""Remove apt package(s)."""
sys.exit(run_apt_command(['remove'] + arguments.packages))
"""Remove packages using apt-get."""
try:
_assert_managed_packages(arguments.app_id, arguments.packages)
except Exception as exception:
print('Access check failed:', exception, file=sys.stderr)
sys.exit(99)
subprocess.run(['dpkg', '--configure', '-a'], check=False)
with apt_hold_freedombox():
run_apt_command(['--fix-broken', 'install'])
returncode = run_apt_command(['remove'] + arguments.packages)
sys.exit(returncode)
def _assert_managed_packages(app_id, packages):
@ -118,7 +131,7 @@ def _assert_managed_packages(app_id, packages):
app = app_module.App.get(app_id)
managed_packages = []
for component in app.get_components_of_type(Packages):
managed_packages += component.possible_packages
managed_packages += component.possible_packages + component.conflicts
for package in packages:
assert package in managed_packages

View File

@ -16,7 +16,7 @@ from plinth.modules.backups.components import BackupRestore
from plinth.modules.config import get_domainname
from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.package import Packages, remove
from plinth.package import Packages, uninstall
from plinth.signals import domain_added, domain_removed
from plinth.utils import format_lazy
@ -173,7 +173,7 @@ class EmailApp(plinth.app.App):
if packages_to_remove:
logger.info('Removing conflicting packages: %s',
packages_to_remove)
remove(packages_to_remove)
uninstall(packages_to_remove)
# Install
_clear_conflicts()

View File

@ -17,7 +17,7 @@ from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from plinth import actions, app
from plinth.errors import ActionError, MissingPackageError
from plinth.errors import MissingPackageError
from plinth.utils import format_lazy
from . import operation as operation_module
@ -144,7 +144,7 @@ class Packages(app.FollowerComponent):
self._packages.append(package)
self.skip_recommends = skip_recommends
self.conflicts = conflicts
self.conflicts = conflicts or []
self.conflicts_action = conflicts_action
@property
@ -324,6 +324,15 @@ class Transaction:
logger.exception('Error installing package: %s', exception)
raise
def uninstall(self):
"""Run an apt-get transaction to uninstall given packages."""
try:
self._run_apt_command(['remove', self.app_id, '--packages'] +
self.package_names)
except subprocess.CalledProcessError as exception:
logger.exception('Error uninstalling package: %s', exception)
raise
def refresh_package_lists(self):
"""Refresh apt package lists."""
try:
@ -351,7 +360,7 @@ class Transaction:
return_code = process.wait()
if return_code != 0:
raise PackageException(_('Error during installation'), self.stderr)
raise PackageException(_('Error running apt-get'), self.stderr)
def _read_stdout(self, process):
"""Read the stdout of the process and update progress."""
@ -421,6 +430,30 @@ def install(package_names, skip_recommends=False, force_configuration=None,
force_missing_configuration)
def uninstall(package_names):
"""Uninstall a set of packages."""
try:
operation = operation_module.Operation.get_operation()
except AttributeError:
raise RuntimeError(
'uninstall() must be called from within an operation.')
start_time = time.time()
while is_package_manager_busy():
if time.time() - start_time >= 24 * 3600: # One day
raise PackageException(_('Timeout waiting for package manager'))
time.sleep(3) # seconds
logger.info('Running uninstall for app - %s, packages - %s',
operation.app_id, package_names)
from . import package
transaction = package.Transaction(operation.app_id, package_names)
operation.thread_data['transaction'] = transaction
transaction.uninstall()
def is_package_manager_busy():
"""Return whether a package manager is running."""
try:
@ -467,11 +500,3 @@ def packages_installed(candidates: Union[list, tuple]) -> list:
pass
return installed_packages
def remove(packages: Union[list, tuple]) -> None:
"""Remove packages."""
try:
actions.superuser_run('packages', ['remove', '--packages'] + packages)
except ActionError:
pass

View File

@ -9,8 +9,8 @@ from unittest.mock import Mock, call, patch
import pytest
from plinth.app import App
from plinth.errors import ActionError, MissingPackageError
from plinth.package import Package, Packages, packages_installed, remove
from plinth.errors import MissingPackageError
from plinth.package import Package, Packages, packages_installed
class TestPackageExpressions(unittest.TestCase):
@ -54,7 +54,7 @@ def test_packages_init():
assert component.possible_packages == ['foo', 'bar']
assert component.component_id == 'test-component'
assert not component.skip_recommends
assert component.conflicts is None
assert component.conflicts == []
assert component.conflicts_action is None
with pytest.raises(ValueError):
@ -189,17 +189,3 @@ def test_packages_installed():
assert len(packages_installed(())) == 0
assert len(packages_installed(('unknown-package', ))) == 0
assert len(packages_installed(('python3', ))) == 1
@patch('plinth.actions.superuser_run')
def test_remove(run):
"""Test removing packages."""
remove(['package1', 'package2'])
run.assert_has_calls(
[call('packages', ['remove', '--packages', 'package1', 'package2'])])
run.reset_mock()
run.side_effect = ActionError()
remove(['package1'])
run.assert_has_calls(
[call('packages', ['remove', '--packages', 'package1'])])