From 124fc14e13a959dabe59d1389137a68ed8791400 Mon Sep 17 00:00:00 2001 From: Fabio Erculiani Date: Wed, 24 Oct 2012 00:09:04 +0200 Subject: [PATCH] [Solo] implement the "solo remove" command, keep common code into the _manage module --- client/solo/commands/_manage.py | 171 +++++++++++++ client/solo/commands/remove.py | 421 ++++++++++++++++++++++++++++++++ docs/TODO | 52 ++-- 3 files changed, 613 insertions(+), 31 deletions(-) create mode 100644 client/solo/commands/_manage.py create mode 100644 client/solo/commands/remove.py diff --git a/client/solo/commands/_manage.py b/client/solo/commands/_manage.py new file mode 100644 index 000000000..1d0602fc2 --- /dev/null +++ b/client/solo/commands/_manage.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +""" + + @author: Fabio Erculiani + @contact: lxnay@sabayon.org + @copyright: Fabio Erculiani + @license: GPL-2 + + B{Entropy Command Line Client}. + +""" +import os +import sys +import argparse + +from entropy.const import const_convert_to_unicode +from entropy.i18n import _, ngettext +from entropy.output import darkgreen, blue, purple, teal, brown, bold, \ + darkred + +import entropy.tools + +from solo.utils import enlightenatom +from solo.commands.command import SoloCommand + +class SoloManage(SoloCommand): + """ + Abstract class used by Solo Package management + modules (remove, install, etc...). + This class contains all the shared code. + """ + + def __init__(self, args): + SoloCommand.__init__(self, args) + self._nsargs = None + + def man(self): + """ + Overridden from SoloCommand. + """ + return self._man() + + def parse(self): + """ + Parse command + """ + parser = self._get_parser() + try: + nsargs = parser.parse_args(self._args) + except IOError as err: + sys.stderr.write("%s\n" % (err,)) + return parser.print_help, [] + + self._nsargs = nsargs + return self._call_locked, [nsargs.func] + + def _show_config_files_update(self, entropy_client): + """ + Inform User about configuration file updates, if any. + """ + entropy_client.output( + blue(_("Scanning configuration files to update")), + header=darkgreen(" @@ "), + back=True) + + updates = entropy_client.ConfigurationUpdates() + scandata = updates.get() + + if not scandata: + entropy_client.output( + blue(_("No configuration files to update.")), + header=darkgreen(" @@ ")) + return + + mytxt = ngettext( + "There is %s configuration file needing update", + "There are %s configuration files needing update", + len(scandata)) % (len(scandata),) + entropy_client.output( + darkgreen(mytxt), + level="warning") + + mytxt = "%s: %s" % ( + purple(_("Please run")), + bold("equo conf update")) + entropy_client.output( + darkgreen(mytxt), + level="warning") + + def _show_did_you_mean(self, entropy_client, package, from_installed): + """ + Show "Did you mean?" results for the given package name. + """ + items = entropy_client.get_meant_packages( + package, from_installed=from_installed) + if not items: + return + + mytxt = "%s %s %s %s %s" % ( + bold(const_convert_to_unicode(" ?")), + teal(_("When you wrote")), + bold(const_convert_to_unicode(package)), + darkgreen(_("You Meant(tm)")), + teal(_("one of these below?")), + ) + entropy_client.output(mytxt) + + _cache = set() + for pkg_id, repo_id in items: + if from_installed: + repo = entropy_client.installed_repository() + else: + repo = entropy_client.open_repository(repo_id) + + key_slot = repo.retrieveKeySlotAggregated(pkg_id) + if key_slot not in _cache: + entropy_client.output( + enlightenatom(key_slot), + header=brown(" # ")) + _cache.add(key_slot) + + def _show_removal_info(self, entropy_client, package_ids, + manual = False): + """ + Show packages removal information. + """ + if manual: + entropy_client.output( + "%s:" % ( + blue(_("These are the packages that " + "should be MANUALLY removed")),), + header=darkred(" @@ ")) + else: + entropy_client.output( + "%s:" % ( + blue(_("These are the packages that " + "would be removed")),), + header=darkred(" @@ ")) + + total = len(package_ids) + count = 0 + inst_repo = entropy_client.installed_repository() + + for package_id in package_ids: + count += 1 + atom = inst_repo.retrieveAtom(package_id) + installedfrom = inst_repo.getInstalledPackageRepository( + package_id) + if installedfrom is None: + installedfrom = _("Not available") + + on_disk_size = inst_repo.retrieveOnDiskSize(package_id) + pkg_size = inst_repo.retrieveSize(package_id) + extra_downloads = inst_repo.retrieveExtraDownload(package_id) + for extra_download in extra_downloads: + pkg_size += extra_download['size'] + on_disk_size += extra_download['disksize'] + + disksize = entropy.tools.bytes_into_human(on_disk_size) + disksize_info = "%s%s%s" % ( + bold("["), + brown("%s" % (disksize,)), + bold("]")) + repo_info = bold("[") + brown(installedfrom) + bold("]") + + mytxt = "%s %s %s" % ( + repo_info, + enlightenatom(atom), + disksize_info) + + entropy_client.output(mytxt, header=darkred(" ## ")) diff --git a/client/solo/commands/remove.py b/client/solo/commands/remove.py new file mode 100644 index 000000000..37c8458cd --- /dev/null +++ b/client/solo/commands/remove.py @@ -0,0 +1,421 @@ +# -*- coding: utf-8 -*- +""" + + @author: Fabio Erculiani + @contact: lxnay@sabayon.org + @copyright: Fabio Erculiani + @license: GPL-2 + + B{Entropy Command Line Client}. + +""" +import sys +import argparse + +from entropy.const import const_convert_to_unicode +from entropy.i18n import _ +from entropy.output import blue, darkred, darkgreen, purple, teal, brown, \ + bold +from entropy.exceptions import DependenciesNotRemovable + +import entropy.tools + +from solo.utils import enlightenatom +from solo.commands.descriptor import SoloCommandDescriptor +from solo.commands._manage import SoloManage + +class SoloRemove(SoloManage): + """ + Main Solo Remove command. + """ + + NAME = "remove" + ALIASES = ["rm"] + ALLOW_UNPRIVILEGED = False + + INTRODUCTION = """\ +Remove previously installed packages from system. +""" + SEE_ALSO = "equo-install(1), equo-config(1)" + + def __init__(self, args): + SoloManage.__init__(self, args) + self._commands = {} + + def _get_parser(self): + """ + Overridden from SoloCommand. + """ + _commands = {} + + descriptor = SoloCommandDescriptor.obtain_descriptor( + SoloRemove.NAME) + parser = argparse.ArgumentParser( + description=descriptor.get_description(), + formatter_class=argparse.RawDescriptionHelpFormatter, + prog="%s %s" % (sys.argv[0], SoloRemove.NAME)) + parser.set_defaults(func=self._remove) + + parser.add_argument( + "packages", nargs='+', + metavar="", help=_("package name")) + + mg_group = parser.add_mutually_exclusive_group() + mg_group.add_argument( + "--ask", action="store_true", + default=False, + help=_("ask before making any changes")) + _commands["--ask"] = {} + mg_group.add_argument( + "--pretend", action="store_true", + default=False, + help=_("show what would be done")) + _commands["--pretend"] = {} + + parser.add_argument( + "--verbose", action="store_true", + default=False, + help=_("verbose output")) + parser.add_argument( + "--nodeps", action="store_true", + default=False, + help=_("exclude package dependencies")) + parser.add_argument( + "--norecursive", action="store_true", + default=False, + help=_("do not calculate dependencies recursively")) + parser.add_argument( + "--deep", action="store_true", + default=False, + help=_("include dependencies no longer needed")) + parser.add_argument( + "--empty", action="store_true", + default=False, + help=_("when used with --deep, include virtual packages")) + parser.add_argument( + "--configfiles", action="store_true", + default=False, + help=_("remove package configuration files no longer needed")) + parser.add_argument( + "--force-system", action="store_true", + default=False, + help=_("force system packages removal (dangerous!)")) + + self._commands = _commands + return parser + + def bashcomp(self, last_arg): + """ + Overridden from SoloCommand. + """ + self._get_parser() # this will generate self._commands + return self._hierarchical_bashcomp(last_arg, [], self._commands) + + def _remove(self, entropy_client): + """ + Solo Remove command. + """ + exit_st, _show_cfgupd = self._remove_action(entropy_client) + if _show_cfgupd: + self._show_config_files_update(entropy_client) + return exit_st + + def _match_packages(self, entropy_client, inst_repo, packages): + """ + Scan the Installed Packages repository for matches and + return a list of matched package identifiers. + """ + package_ids = [] + for package in packages: + package_id, _result = inst_repo.atomMatch(package) + if package_id == -1: + mytxt = "!!! %s: %s %s." % ( + purple(_("Warning")), + teal(const_convert_to_unicode(package)), + purple(_("is not installed")), + ) + entropy_client.output("!!!", level="warning") + entropy_client.output(mytxt, level="warning") + entropy_client.output("!!!", level="warning") + + if len(package) > 3: + self._show_did_you_mean( + entropy_client, package, True) + entropy_client.output("!!!", level="warning") + continue + package_ids.append(package_id) + + return package_ids + + @staticmethod + def _execute_action(entropy_client, inst_repo, removal_queue, + remove_config_files): + """ + Execute the actual packages removal activity. + """ + total = len(removal_queue) + count = 0 + for package_id in removal_queue: + count += 1 + + atom = inst_repo.retrieveAtom(package_id) + metaopts = {} + metaopts['removeconfig'] = remove_config_files + pkg = None + try: + pkg = entropy_client.Package() + pkg.prepare((package_id,), "remove", metaopts) + + if "remove_installed_vanished" not in pkg.pkgmeta: + + xterm_header = "equo (%s) :: %d of %d ::" % ( + _("removal"), count, total) + + entropy_client.output( + darkgreen(atom), + count=(count, total), + header=darkred(" --- ") + ">>> ") + + exit_st = pkg.run(xterm_header = xterm_header) + if exit_st != 0: + return 1 + + finally: + if pkg is not None: + pkg.kill() + + entropy_client.output( + "%s." % (blue(_("All done")),), + header=darkred(" @@ ")) + return 0 + + def _prompt_removal(self, entropy_client, inst_repo, package_ids, + system_packages_check): + """ + Show list of packages that would be removed and ask User + about dependencies calculation, if --ask has been provided. + """ + ask = self._nsargs.ask + verbose = self._nsargs.verbose + pretend = self._nsargs.pretend + plain_removal_queue = [] + deep_removal = True + + entropy_client.output( + "%s:" % ( + teal(_("These are the chosen packages")),), + header=darkgreen(" @@ ")) + + total = len(package_ids) + count = 0 + for package_id in package_ids: + count += 1 + atom = inst_repo.retrieveAtom(package_id) + + if system_packages_check: + valid = entropy_client.validate_package_removal( + package_id) + if not valid: + mytxt = "%s: %s. %s." % ( + enlightenatom(atom), + darkred(_("vital package")), + darkred(_("Removal forbidden")), + ) + entropy_client.output( + mytxt, + level="warning", + count=(count, total), + header=bold(" !!! ")) + continue + + plain_removal_queue.append(package_id) + installedfrom = inst_repo.getInstalledPackageRepository( + package_id) + if installedfrom is None: + installedfrom = _("Not available") + + on_disk_size = inst_repo.retrieveOnDiskSize(package_id) + extra_downloads = inst_repo.retrieveExtraDownload(package_id) + + for extra_download in extra_downloads: + on_disk_size += extra_download['disksize'] + + disksize = entropy.tools.bytes_into_human(on_disk_size) + disksizeinfo = " [%s]" % ( + bold(const_convert_to_unicode(disksize)),) + + entropy_client.output( + "[%s] %s %s" % ( + brown(installedfrom), + enlightenatom(atom), + disksizeinfo), + count=(count, total), + header=darkgreen(" # ")) + + if verbose or ask or pretend: + entropy_client.output( + "%s: %d" % ( + blue(_("Packages involved")), + total,), + header=purple(" @@ ")) + + return plain_removal_queue, deep_removal + + def _prompt_final_removal(self, entropy_client, + inst_repo, removal_queue): + """ + Prompt some final information to User with respect to + the removal queue. + """ + total = len(removal_queue) + mytxt = "%s: %s" % ( + blue(_("Packages that would be removed")), + darkred(const_convert_to_unicode(total)), + ) + entropy_client.output( + mytxt, header=darkred(" @@ ")) + + total_removal_size = 0 + total_pkg_size = 0 + for package_id in set(removal_queue): + on_disk_size = inst_repo.retrieveOnDiskSize(package_id) + if on_disk_size is None: + on_disk_size = 0 + + pkg_size = inst_repo.retrieveSize(package_id) + if pkg_size is None: + pkg_size = 0 + + extra_downloads = inst_repo.retrieveExtraDownload(package_id) + for extra_download in extra_downloads: + pkg_size += extra_download['size'] + on_disk_size += extra_download['disksize'] + + total_removal_size += on_disk_size + total_pkg_size += pkg_size + + human_removal_size = entropy.tools.bytes_into_human( + total_removal_size) + human_pkg_size = entropy.tools.bytes_into_human(total_pkg_size) + + mytxt = "%s: %s" % ( + blue(_("Freed disk space")), + bold(const_convert_to_unicode(human_removal_size)), + ) + entropy_client.output( + mytxt, header=darkred(" @@ ")) + + mytxt = "%s: %s" % ( + blue(_("Total bandwidth wasted")), + bold(str(human_pkg_size)), + ) + entropy_client.output( + mytxt, header=darkred(" @@ ")) + + def _remove_action(self, entropy_client): + """ + Solo Remove action implementation. + """ + system_packages_check = not self._nsargs.force_system + deps = not self._nsargs.nodeps + recursive = not self._nsargs.norecursive + pretend = self._nsargs.pretend + ask = self._nsargs.ask + empty = self._nsargs.empty + deep = self._nsargs.deep + remove_config_files = self._nsargs.configfiles + packages = self._nsargs.packages + + packages = entropy_client.packages_expand(packages) + inst_repo = entropy_client.installed_repository() + package_ids = self._match_packages( + entropy_client, inst_repo, packages) + + if not package_ids: + entropy_client.output( + darkred(_("No packages found")), + level="error", importance=1) + return 1, False + + plain_removal_queue, deep_removal = self._prompt_removal( + entropy_client, inst_repo, package_ids, system_packages_check) + + if not plain_removal_queue: + entropy_client.output( + darkred(_("Nothing to do")), + level="error", importance=1) + return 1, False + + if ask and not pretend: + if deps: + question = " %s" % ( + _("Would you like to calculate dependencies ?"), + ) + rc = entropy_client.ask_question(question) + if rc == _("No"): + return 1, False + else: + question = " %s" % ( + _("Would you like to remove them now ?"),) + deep_removal = False + rc = entropy_client.ask_question(question) + if rc == _("No"): + return 1, False + + removal_queue = [] + if deep_removal and deps: + try: + removal_queue += entropy_client.get_removal_queue( + plain_removal_queue, + deep = deep, recursive = recursive, + empty = empty, + system_packages = system_packages_check) + except DependenciesNotRemovable as err: + non_rm_pkg_ids = sorted( + [inst_repo.retrieveAtom(x[0]) for x in err.value]) + # otherwise we need to deny the request + entropy_client.output("", level="error") + entropy_client.output( + " %s, %s:" % ( + purple(_("Ouch!")), + brown(_("the following system packages" + " were pulled in")), + ), + level="error", importance=1) + for pkg_in in non_rm_pkg_ids: + pkg_name = inst_repo.retrieveAtom(pkg_in) + entropy_client.output( + teal(pkg_name), + header=purple(" # "), + level="error") + entropy_client.output("", level="error") + return 1, False + + removal_queue += [x for x in plain_removal_queue if x \ + not in removal_queue] + self._show_removal_info(entropy_client, removal_queue) + self._prompt_final_removal( + entropy_client, inst_repo, removal_queue) + + if pretend: + return 0, False + + if ask: + question = " %s" % ( + _("Would you like to proceed ?"),) + rc = entropy_client.ask_question(question) + if rc == _("No"): + return 1, False + + exit_st = self._execute_action( + entropy_client, inst_repo, removal_queue, + remove_config_files) + return exit_st, True + + +SoloCommandDescriptor.register( + SoloCommandDescriptor( + SoloRemove, + SoloRemove.NAME, + _("remove packages from system")) + ) diff --git a/docs/TODO b/docs/TODO index 3662b942a..4ee4eaed2 100644 --- a/docs/TODO +++ b/docs/TODO @@ -9,37 +9,18 @@ Backlog (raw) --verbose --replay --empty - --resume + --resume * drop --skipfirst --multifetch --multifetch=N - security - oscheck - --mtime - --assimilate - --reinstall - --quiet - --verbose - update - --force - list - --affected - --unaffected - info - install - --ask - --fetch - --pretend - --quiet - install --ask --pretend --fetch --nodeps --bdeps - --resume + --resume * drop --skipfirst --clean --empty @@ -65,20 +46,29 @@ Backlog (raw) --multifetch --multifetch=N - remove - --ask - --pretend - --nodeps - --deep - --empty - --configfiles - --force-system - --resume - config --ask --pretend + security + oscheck + --mtime + --assimilate + --reinstall + --quiet + --verbose + update + --force + list + --affected + --unaffected + info + install + --ask + --fetch + --pretend + --quiet + deptest --quiet --ask