18625 new disk space information needs improved formatting, wording
authorShawn Walker <shawn.walker@oracle.com>
Thu, 07 Jul 2011 15:06:18 -0700
changeset 2446 ba222bc0b1ce
parent 2445 3ae7fde31e19
child 2447 39d163223bd7
18625 new disk space information needs improved formatting, wording 18626 disk space computation doesn't account for cache or filesystem overhead 18647 pkg(1) planning output could be improved
doc/client_api_versions.txt
src/client.py
src/gui/modules/misc_non_gui.py
src/modules/api_common.py
src/modules/client/api.py
src/modules/client/image.py
src/modules/client/imageplan.py
src/modules/client/pkgplan.py
src/modules/lint/engine.py
src/modules/misc.py
src/modules/server/api.py
src/packagemanager.py
src/pkgdep.py
src/sysrepo.py
src/tests/pkg5unittest.py
src/util/distro-import/importer.py
--- a/doc/client_api_versions.txt	Thu Jul 07 14:54:17 2011 -0700
+++ b/doc/client_api_versions.txt	Thu Jul 07 15:06:18 2011 -0700
@@ -1,3 +1,14 @@
+Version 62:
+Incompatible with clients using versions 0-61.
+
+    pkg.client.api.PackageInfo has changed as follows:
+        * the 'fmri' property is now a PkgFmri object instead of a
+          string.
+
+    pkg.client.api.PlanDescription has changed as follows:
+        * get_services() now returns a list of tuples of strings,
+          see pkg.client.api for details.
+
 Version 61:
 Compatible with clients using version 60.
     pkg.client.api.PlanDescription has changed as follows:
--- a/src/client.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/client.py	Thu Jul 07 15:06:18 2011 -0700
@@ -44,6 +44,7 @@
 
 try:
         import calendar
+        import collections
         import datetime
         import errno
         import fnmatch
@@ -88,7 +89,7 @@
         import sys
         sys.exit(1)
 
-CLIENT_API_VERSION = 61
+CLIENT_API_VERSION = 62
 PKG_CLIENT_NAME = "pkg"
 
 JUST_UNKNOWN = 0
@@ -888,95 +889,145 @@
         plan = api_inst.describe()
 
         a, r, i, c = [], [], [], []
-
         for src, dest in plan.get_changes():
                 if dest is None:
                         r.append((src, dest))
                 elif src is None:
                         i.append((src, dest))
-                elif str(src) != str(dest):
+                elif src != dest:
                         c.append((src, dest))
                 else:
                         a.append((src, dest))
-        v = plan.get_varcets()
 
         def bool_str(val):
                 if val:
                         return _("Yes")
                 return _("No")
 
+        status = []
+        varcets = plan.get_varcets()
         if "basic" in disp:
-                def cond_show(s, v):
+                def cond_show(s1, s2, v):
                         if v:
-                                logger.info(s % v)
-
-                cond_show(_("                Packages to remove: %6d"), len(r))
-                cond_show(_("               Packages to install: %6d"), len(i))
-                cond_show(_("                Packages to update: %6d"), len(c))
-                cond_show(_("         Variants/facets to change: %6d"), len(v))
-
-                bytes = plan.bytes_added
-                if bytes:
-                        logger.info(_("Additional filesystem space needed: %6s"),
-                            misc.bytes_to_str(bytes))
-                        logger.info(_("        Filesystem space available: %6s"),
-                            misc.bytes_to_str(plan.bytes_avail))
-
-                if len(v):
-                        s = "                Packages to change: %6d"
+                                status.append((s1, s2 % v))
+
+                cond_show(_("Packages to remove:"), "%d", len(r))
+                cond_show(_("Packages to install:"), "%d", len(i))
+                cond_show(_("Packages to update:"), "%d", len(c))
+                cond_show(_("Variants/Facets to change:"), "%d", len(varcets))
+
+                if verbose:
+                        # Only show space information in verbose mode.
+                        abytes = plan.bytes_added
+                        if abytes:
+                                status.append((_("Estimated space available:"),
+                                    misc.bytes_to_str(plan.bytes_avail)))
+                                status.append((
+                                    _("Estimated space to be consumed:"),
+                                    misc.bytes_to_str(plan.bytes_added)))
+
+                if len(varcets):
+                        cond_show(_("Packages to change:"), "%d", len(a))
                 else:
-                        s = "                   Packages to fix: %6d"
-                cond_show(s, len(a))
-
-                logger.info(_("           Create boot environment: %6s") %
-                    bool_str(plan.new_be))
+                        cond_show(_("Packages to fix:"), "%d", len(a))
+
+                status.append((_("Create boot environment:"),
+                    bool_str(plan.new_be)))
 
                 if plan.new_be and (verbose or not plan.activate_be):
                         # Only show activation status if verbose or if new BE
                         # will not be activated.
-                        logger.info(_("         Activate boot environment: %6s") %
-                            bool_str(plan.activate_be))
+                        status.append((_("Activate boot environment:"),
+                            bool_str(plan.activate_be)))
 
                 if not plan.new_be:
-                        cond_show(_("               Services to restart: %6d"),
+                        cond_show(_("Services to change:"), "%d",
                             len(plan.get_services()))
 
         if "boot-archive" in disp:
-                logger.info(_("              Rebuild boot archive: %6s") %
-                    bool_str(plan.update_boot_archive))
-
-        if "variants/facets" in disp and v:
+                status.append((_("Rebuild boot archive:"),
+                    bool_str(plan.update_boot_archive)))
+
+        # Right-justify all status strings based on length of longest string.
+        rjust_status = max(len(s[0]) for s in status)
+        rjust_value = max(len(s[1]) for s in status)
+        for s in status:
+                logger.info("%s %s" % (s[0].rjust(rjust_status),
+                    s[1].rjust(rjust_value)))
+
+        if status:
+                # Ensure there is a blank line between status information and
+                # remainder.
+                logger.info("")
+
+        if "variants/facets" in disp and varcets:
                 logger.info(_("Changed variants/facets:"))
-                for x in v:
+                for x in varcets:
                         logger.info("  %s" % x)
 
         if "solver-errors" in disp:
-                logger.info(_("Solver dependency errors:"))
+                first = True
                 for l in plan.get_solver_errors():
+                        if first:
+                                logger.info(_("Solver dependency errors:"))
+                                first = False
                         logger.info(l)
 
         if "fmris" in disp:
-                if len(c) + len(r) + len(i) > 0:
-                        logger.info(_("Changed fmris:"))
-                        for src, dest in r + i + c:
-                                logger.info("  %s -> %s", src, dest)
+                changed = collections.defaultdict(list)
+                for src, dest in itertools.chain(r, i, c):
+                        if src and dest:
+                                if src.publisher != dest.publisher:
+                                        pparent = "%s -> %s" % (src.publisher,
+                                            dest.publisher)
+                                else:
+                                        pparent = dest.publisher
+                                pname = dest.pkg_stem
+                                pver = "%s -> %s" % (src.fmri.version,
+                                    dest.fmri.version)
+                        elif dest:
+                                pparent = dest.publisher
+                                pname = dest.pkg_stem
+                                pver = "None -> %s" % dest.fmri.version
+                        else:
+                                pparent = src.publisher
+                                pname = src.pkg_stem
+                                pver = "%s -> None" % src.fmri.version
+
+                        changed[pparent].append((pname, pver))
+
+                if changed:
+                        logger.info(_("Changed packages:"))
+                        last_parent = None
+                        for pparent, pname, pver in (
+                            (pparent, pname, pver)
+                            for pparent in sorted(changed)
+                            for pname, pver in changed[pparent]
+                        ):
+                                if pparent != last_parent:
+                                        logger.info(pparent)
+
+                                logger.info("  %s" % pname)
+                                logger.info("    %s" % pver)
+                                last_parent = pparent
+
                 if len(a):
                         logger.info(_("Affected fmris:"))
                         for src, dest in a:
                                 logger.info("  %s", src)
 
-        if "services" in disp:
-                if not plan.new_be:
-                        logger.info(_("Services:"))
-                        l = plan.get_services()
-                        if l:
-                                for a in l:
-                                        logger.info("  %s" % a)
-                        else:
-                                logger.info(_("  None"))
+        if "services" in disp and not plan.new_be:
+                last_action = None
+                for action, smf_fmri in plan.get_services():
+                        if last_action is None:
+                                logger.info("Services:")
+                        if action != last_action:
+                                logger.info("  %s:" % action)
+                        logger.info("    %s" % smf_fmri)
+                        last_action = action
 
         if "actions" in disp:
-                logger.info("Actions")
+                logger.info("Actions:")
                 for a in plan.get_actions():
                         logger.info("  %s" % a)
 
--- a/src/gui/modules/misc_non_gui.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/gui/modules/misc_non_gui.py	Thu Jul 07 15:06:18 2011 -0700
@@ -41,7 +41,7 @@
 
 # The current version of the Client API the PM, UM and
 # WebInstall GUIs have been tested against and are known to work with.
-CLIENT_API_VERSION = 60
+CLIENT_API_VERSION = 62
 LOG_DIR = "/var/tmp"
 LOG_ERROR_EXT = "_error.log"
 LOG_INFO_EXT = "_info.log"
--- a/src/modules/api_common.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/modules/api_common.py	Thu Jul 07 15:06:18 2011 -0700
@@ -170,7 +170,7 @@
                     version=version.release,
                     build_release=version.build_release, branch=version.branch,
                     packaging_date=version.get_timestamp().strftime("%c"),
-                    pfmri=str(f))
+                    pfmri=f)
 
 
 def _get_pkg_cat_data(cat, info_needed, actions=None,
--- a/src/modules/client/api.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/modules/client/api.py	Thu Jul 07 15:06:18 2011 -0700
@@ -72,7 +72,7 @@
 from pkg.client.pkgdefs import *
 from pkg.smf import NonzeroExitException
 
-CURRENT_API_VERSION = 61
+CURRENT_API_VERSION = 62
 CURRENT_P5I_VERSION = 1
 
 # Image type constants.
@@ -282,7 +282,7 @@
                 other platforms, a value of False will allow any image location.
                 """
 
-                compatible_versions = set([CURRENT_API_VERSION, 60])
+                compatible_versions = set([CURRENT_API_VERSION])
 
                 if version_id not in compatible_versions:
                         raise apx.VersionException(CURRENT_API_VERSION,
@@ -3189,7 +3189,7 @@
                                     states=states, publisher=pub, version=release,
                                     build_release=build_release, branch=branch,
                                     packaging_date=packaging_date, size=size,
-                                    pfmri=str(pfmri), licenses=licenses,
+                                    pfmri=pfmri, licenses=licenses,
                                     links=links, hardlinks=hardlinks, files=files,
                                     dirs=dirs, dependencies=dependencies,
                                     description=description))
@@ -4306,7 +4306,8 @@
                 and 'dest_pi' is the new version of the package it is
                 being upgraded to."""
 
-                for pp in self.__plan.pkg_plans:
+                for pp in sorted(self.__plan.pkg_plans,
+                    key=operator.attrgetter("origin_fmri", "destination_fmri")):
                         yield (PackageInfo.build_from_fmri(pp.origin_fmri),
                             PackageInfo.build_from_fmri(pp.destination_fmri))
 
@@ -4416,24 +4417,24 @@
 
         @property
         def bytes_added(self):
-                """Returns approximate number of bytes added"""
+                """Estimated number of bytes added"""
                 return self.__plan.bytes_added
 
         @property
         def cbytes_added(self):
-                """Returns approximate number of download cache bytes added"""
+                """Estimated number of download cache bytes added"""
                 return self.__plan.cbytes_added
 
         @property
         def bytes_avail(self):
-                """Returns approximate number of bytes available in image /"""
+                """Estimated number of bytes available in image /"""
                 return self.__plan.bytes_avail
 
         @property
         def cbytes_avail(self):
-                """Returns approximate number of space in download cache"""
+                """Estimated number of bytes available in download cache"""
                 return self.__plan.cbytes_avail
-         
+
 
 def image_create(pkg_client_name, version_id, root, imgtype, is_zone,
     cancel_state_callable=None, facets=misc.EmptyDict, force=False,
--- a/src/modules/client/image.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/modules/client/image.py	Thu Jul 07 15:06:18 2011 -0700
@@ -289,7 +289,8 @@
         def write_cache_path(self):
                 """Returns a path to fs that holds write cache - used to
                 compute whether we have sufficent space for downloads"""
-                return self.__user_cache_dir or os.path.join(self.root, IMG_PUB_DIR)
+                return self.__user_cache_dir or \
+                    os.path.join(self.imgdir, IMG_PUB_DIR)
 
         @staticmethod
         def alloc(*args, **kwargs):
--- a/src/modules/client/imageplan.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/modules/client/imageplan.py	Thu Jul 07 15:06:18 2011 -0700
@@ -239,11 +239,13 @@
 
         @property
         def services(self):
-                """Returns a list of strings describing affected services"""
-                return [
-                    "%s: %s" % (fmri, smf)
-                    for fmri, smf in self.__actuators.get_services_list()
-                ]
+                """Returns a list of string tuples describing affected services
+                (action, SMF FMRI)."""
+                return sorted(
+                    ((str(action), str(smf_fmri))
+                    for action, smf_fmri in self.__actuators.get_services_list()),
+                    key=operator.itemgetter(0, 1)
+                )
 
         @property
         def varcets(self):
@@ -1523,23 +1525,59 @@
                 self.merge_actions()
 
                 for p in self.pkg_plans:
-                        cbytes, bytes = p.get_bytes_added()
-                        self.__cbytes_added += cbytes
-                        self.__bytes_added += bytes
+                        cpbytes, pbytes = p.get_bytes_added()
+                        if p.destination_fmri:
+                                mpath = self.image.get_manifest_path(
+                                    p.destination_fmri)
+                                try:
+                                        # Manifest data is essentially stored
+                                        # three times (original, cache, catalog).
+                                        # For now, include this in cbytes_added
+                                        # since that's closest to where the
+                                        # download cache is stored.
+                                        self.__cbytes_added += \
+                                            os.stat(mpath).st_size * 3
+                                except EnvironmentError, e:
+                                        raise apx._convert_error(e)
+                        self.__cbytes_added += cpbytes
+                        self.__bytes_added += pbytes
+
+                # Include state directory in cbytes_added for now since it's
+                # closest to where the download cache is stored.  (Twice the
+                # amount is used because image state update involves using
+                # a complete copy of existing state.)
+                self.__cbytes_added += \
+                    misc.get_dir_size(self.image._statedir) * 2
+
+                # Our slop factor is 25%; overestimating is safer than under-
+                # estimating.  This attempts to approximate how much overhead
+                # the filesystem will impose on the operation.  Empirical
+                # testing suggests that overhead can vary wildly depending on
+                # average file size, fragmentation, zfs metadata overhead, etc.
+                # For an install of a package such as solaris-small-server into
+                # an image, a 12% difference between actual size and installed
+                # size was found, so this seems safe enough.  (And helps account
+                # for any bootarchives, fs overhead, etc.)
+                self.__cbytes_added *= 1.25
+                self.__bytes_added *= 1.25
+
+                # XXX For now, include cbytes_added in bytes_added total; in the
+                # future, this should only happen if they share the same
+                # filesystem.
+                self.__bytes_added += self.__cbytes_added
 
                 self.__update_avail_space()
 
         def __update_avail_space(self):
-                """Update amount of available space on FS"""                
+                """Update amount of available space on FS"""
                 self.__cbytes_avail = misc.spaceavail(
                     self.image.write_cache_path)
-                        
+
                 self.__bytes_avail = misc.spaceavail(self.image.root)
                 # if we don't have a full image yet
                 if self.__cbytes_avail < 0:
                         self.__cbytes_avail = self.__bytes_avail
 
- 
         def evaluate_pkg_plans(self):
                 """Internal helper function that does the work of converting
                 fmri changes into pkg plans."""
@@ -2036,17 +2074,17 @@
                 # check if we're going to have enough room
                 # stat fs again just in case someone else is using space...
                 self.__update_avail_space()
-                if self.__cbytes_added > self.__cbytes_avail * 1.2: 
+                if self.__cbytes_added > self.__cbytes_avail: 
                         raise api_errors.ImageInsufficentSpace(
                             self.__cbytes_added,
-                            self.__cbytes_avail * 1.2,
+                            self.__cbytes_avail,
                             _("Download cache"))
-                if self.__bytes_added > self.__bytes_avail * 1.2:
+                if self.__bytes_added > self.__bytes_avail:
                         raise api_errors.ImageInsufficentSpace(
                             self.__bytes_added,
-                            self.__bytes_avail * 1.2,
+                            self.__bytes_avail,
                             _("Root filesystem"))
-                
+
                 # Remove history about manifest/catalog transactions.  This
                 # helps the stats engine by only considering the performance of
                 # bulk downloads.
@@ -2115,10 +2153,10 @@
 
                 # check for available space
                 self.__update_avail_space()
-                if self.__bytes_added > self.__bytes_avail * 1.2:
+                if self.__bytes_added > self.__bytes_avail:
                         raise api_errors.ImageInsufficentSpace(
                             self.__bytes_added,
-                            self.__bytes_avail * 1.2,
+                            self.__bytes_avail,
                             _("Root filesystem"))
 
                 #
--- a/src/modules/client/pkgplan.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/modules/client/pkgplan.py	Thu Jul 07 15:06:18 2011 -0700
@@ -360,12 +360,13 @@
                 because they're usually pinned by snapshots"""
                 def sum_dest_size(a, b):
                         if b[1]:
-                                return (a[0] + int(b[1].attrs.get("pkg.csize",0)),
-                                    a[1] + int(b[1].attrs.get("pkg.size",0)))
-                        else:
-                                return (a[0], a[1])
+                                return (a[0] + int(b[1].attrs.get("pkg.csize" ,0)),
+                                    a[1] + int(b[1].attrs.get("pkg.size", 0)))
+                        return (a[0], a[1])
 
-                return reduce(sum_dest_size, itertools.chain(*self.actions), (0,0))
+                return reduce(sum_dest_size, itertools.chain(*self.actions),
+                    (0, 0))
+
         def get_xfername(self):
                 if self.destination_fmri:
                         return self.destination_fmri.get_name()
--- a/src/modules/lint/engine.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/modules/lint/engine.py	Thu Jul 07 15:06:18 2011 -0700
@@ -39,7 +39,7 @@
 import sys
 
 PKG_CLIENT_NAME = "pkglint"
-CLIENT_API_VERSION = 60
+CLIENT_API_VERSION = 62
 pkg.client.global_settings.client_name = PKG_CLIENT_NAME
 
 class LintEngineException(Exception):
--- a/src/modules/misc.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/modules/misc.py	Thu Jul 07 15:06:18 2011 -0700
@@ -1073,7 +1073,13 @@
         except OSError:
                 return -1
 
-
-
-
-
+def get_dir_size(path):
+        """Return the size (in bytes) of a directory and all of its contents."""
+        try:
+                return sum(
+                    os.path.getsize(os.path.join(d, fname))
+                    for d, dnames, fnames in os.walk(path)
+                    for fname in fnames
+                )
+        except EnvironmentError, e:
+                raise apx._convert_error(e)
--- a/src/modules/server/api.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/modules/server/api.py	Thu Jul 07 15:06:18 2011 -0700
@@ -249,7 +249,7 @@
                             publisher=pub, version=release,
                             build_release=build_release, branch=branch,
                             packaging_date=packaging_date, size=size,
-                            pfmri=str(f), licenses=licenses, links=links,
+                            pfmri=f, licenses=licenses, links=links,
                             hardlinks=hardlinks, files=files, dirs=dirs,
                             dependencies=dependencies, description=description))
                 return {
--- a/src/packagemanager.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/packagemanager.py	Thu Jul 07 15:06:18 2011 -0700
@@ -4308,8 +4308,7 @@
                         info = self.api_o.info(pkg_stems, True, frozenset(
                                     [api.PackageInfo.STATE, api.PackageInfo.IDENTITY]))
                         for info_s in info.get(0):
-                                pkg_stem = fmri.PkgFmri(info_s.fmri).get_pkg_stem(
-                                    include_scheme = True)
+                                pkg_stem = info_s.fmri.get_pkg_stem()
                                 if api.PackageInfo.INSTALLED in info_s.states:
                                         pkg_stem_states[pkg_stem] = \
                                                 api.PackageInfo.INSTALLED
--- a/src/pkgdep.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/pkgdep.py	Thu Jul 07 15:06:18 2011 -0700
@@ -41,7 +41,7 @@
 import pkg.publish.dependencies as dependencies
 from pkg.misc import msg, emsg, PipeError
 
-CLIENT_API_VERSION = 60
+CLIENT_API_VERSION = 62
 PKG_CLIENT_NAME = "pkgdepend"
 
 DEFAULT_SUFFIX = ".res"
--- a/src/sysrepo.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/sysrepo.py	Thu Jul 07 15:06:18 2011 -0700
@@ -53,7 +53,7 @@
 orig_cwd = None
 
 PKG_CLIENT_NAME = "pkg.sysrepo"
-CLIENT_API_VERSION = 60
+CLIENT_API_VERSION = 62
 pkg.client.global_settings.client_name = PKG_CLIENT_NAME
 
 # exit codes
--- a/src/tests/pkg5unittest.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/tests/pkg5unittest.py	Thu Jul 07 15:06:18 2011 -0700
@@ -127,7 +127,7 @@
 
 # Version test suite is known to work with.
 PKG_CLIENT_NAME = "pkg"
-CLIENT_API_VERSION = 60
+CLIENT_API_VERSION = 62
 
 ELIDABLE_ERRORS = [ TestSkippedException, depotcontroller.DepotStateException ]
 
--- a/src/util/distro-import/importer.py	Thu Jul 07 14:54:17 2011 -0700
+++ b/src/util/distro-import/importer.py	Thu Jul 07 15:06:18 2011 -0700
@@ -56,7 +56,7 @@
 from pkg.misc import emsg
 from pkg.portable import PD_LOCAL_PATH, PD_PROTO_DIR, PD_PROTO_DIR_LIST
 
-CLIENT_API_VERSION = 60
+CLIENT_API_VERSION = 62
 PKG_CLIENT_NAME = "importer.py"
 pkg.client.global_settings.client_name = PKG_CLIENT_NAME