669 Need method to print package licenses before installation
authorDanek Duvall <danek.duvall@sun.com>
Fri, 18 Apr 2008 16:17:09 -0700
changeset 342 5e1f4d8429bf
parent 341 42383445191b
child 343 3a0cd685acc0
669 Need method to print package licenses before installation
src/client.py
src/man/pkg.1.txt
src/modules/actions/file.py
src/modules/actions/generic.py
src/modules/actions/license.py
src/modules/client/image.py
src/modules/client/retrieve.py
src/modules/manifest.py
src/modules/misc.py
src/tests/cli/t_commandline.py
--- a/src/client.py	Thu Apr 17 19:03:51 2008 -0700
+++ b/src/client.py	Fri Apr 18 16:17:09 2008 -0700
@@ -82,10 +82,10 @@
         pkg refresh [--full]
 
 Advanced subcommands:
-        pkg info [pkg_fmri_pattern ...]
+        pkg info [-lr] [--license] [pkg_fmri_pattern ...]
         pkg search [-lr] [-s server] token
         pkg verify [-fHqv] [pkg_fmri_pattern ...]
-        pkg contents [-Hm] [-o attribute ...] [-s sort_key] [-t action_type ... ]
+        pkg contents [-Hmr] [-o attribute ...] [-s sort_key] [-t action_type ... ]
             pkg_fmri_pattern [...]
         pkg image-create [-FPUz] [--full|--partial|--user] [--zone]
             [-k ssl_key] [-c ssl_cert] -a <prefix>=<url> dir
@@ -650,39 +650,127 @@
 
         return retcode
 
+def info_license(img, mfst, remote):
+        fmri = mfst.fmri
+
+        for i, license in enumerate(mfst.gen_actions_by_type("license")):
+                if i > 0:
+                        print
+
+                if remote:
+                        misc.gunzip_from_stream(
+                            license.get_remote_opener(img, fmri)(), sys.stdout)
+                else:
+                        print license.get_local_opener(img, fmri)().read()[:-1]
+
 def info(img, args):
         """Display information about a package or packages.
         """
 
-        # XXX Need remote-info option, to request equivalent information
-        # from repository.
+        display_license = False
+        info_local = False
+        info_remote = False
 
-        opts, pargs = getopt.getopt(args, "")
+        opts, pargs = getopt.getopt(args, "lr", ["license"])
+        for opt, arg in opts:
+                if opt == "-l":
+                        info_local = True
+                elif opt == "-r":
+                        info_remote = True
+                elif opt == "--license":
+                        display_license = True
+
+        if not info_local and not info_remote:
+                info_local = True
+        elif info_local and info_remote:
+                usage(_("info: -l and -r may not be combined"))
+
+        if info_remote and not pargs:
+                usage(_("info: must request remote info for specific packages"))
 
         img.load_catalogs(progress.NullProgressTracker())
 
-        err, fmris = installed_fmris_from_args(img, pargs)
-        if err != 0:
-                return err
-        if not fmris:
-                return 0
-        
+        if info_local:
+                err, fmris = installed_fmris_from_args(img, pargs)
+                if err != 0:
+                        return err
+                if not fmris:
+                        print _("""\
+pkg: no packages matching the patterns you specified are installed
+on the system.  Specify -r to retrieve requested information from the
+repository.""")
+        elif info_remote:
+                fmris = []
+
+                # XXX This loop really needs not to be copied from
+                # Image.make_install_plan()!
+                for p in pargs:
+                        try:
+                                matches = img.get_matching_fmris(p)
+                        except KeyError:
+                                print _("""\
+pkg: no package matching '%s' could be found in current catalog
+     suggest relaxing pattern, refreshing and/or examining catalogs""") % p
+                                error = 1
+                                continue
+
+                        pnames = {}
+                        pmatch = []
+                        npnames = {}
+                        npmatch = []
+                        for m in matches:
+                                if m.preferred_authority():
+                                        pnames[m.get_pkg_stem()] = 1
+                                        pmatch.append(m)
+                                else:
+                                        npnames[m.get_pkg_stem()] = 1
+                                        npmatch.append(m)
+
+                        if len(pnames.keys()) > 1:
+                                print \
+                                    _("pkg: '%s' matches multiple packages") % p
+                                for k in pnames.keys():
+                                        print "\t%s" % k
+                                error = 1
+                                continue
+                        elif len(pnames.keys()) < 1 and len(npnames.keys()) > 1:
+                                print \
+                                    _("pkg: '%s' matches multiple packages") % p
+                                for k in npnames.keys():
+                                        print "\t%s" % k
+                                error = 1
+                                continue
+
+                        # matches is a list reverse sorted by version, so take
+                        # the first; i.e., the latest.
+                        if len(pmatch) > 0:
+                                fmris.append(pmatch[0])
+                        else:
+                                fmris.append(npmatch[0]) 
+
         manifests = ( img.get_manifest(f, filtered = True) for f in fmris )
 
         for i, m in enumerate(manifests):
                 if i > 0:
                         print
 
+                if display_license:
+                        info_license(img, m, info_remote)
+                        continue
+
                 authority, name, version = m.fmri.tuple()
                 authority = fmri.strip_auth_pfx(authority)
                 summary = m.get("description", "")
                 if m.fmri.preferred_authority():
                         authority += _(" (preferred)")
+                if img.is_installed(m.fmri):
+                        state = _("Installed")
+                else:
+                        state = _("Not installed")
 
                 print "          Name:", name
                 print "       Summary:", summary
-                # XXX Hard wired for now.
-                print "         State: Installed"
+                print "         State:", state
 
                 # XXX even more info on the authority would be nice?
                 print "     Authority:", authority
@@ -701,8 +789,6 @@
                 # XXX need to properly humanize the manifest.size
                 # XXX add license/copyright info here?
 
-
-
 def display_contents_results(actionlist, attrs, sort_attrs, action_types,
     display_headers):
         """ Print results of a "list" operation """
@@ -810,8 +896,6 @@
         for line in sorted(lines, key = key_extract):
                 print (fmt % tuple(line)).rstrip()
 
-
-
 def list_contents(img, args):
         """List package contents.
 
@@ -823,7 +907,7 @@
         # XXX Need remote-info option, to request equivalent information
         # from repository.
 
-        opts, pargs = getopt.getopt(args, "Ho:s:t:mf")
+        opts, pargs = getopt.getopt(args, "Ho:s:t:mfr")
 
         valid_special_attrs = [ "action.name", "action.key", "action.raw",
             "pkg.name", "pkg.fmri", "pkg.shortfmri", "pkg.authority",
@@ -832,6 +916,8 @@
         display_headers = True
         display_raw = False
         display_nofilters = False
+        remote = False
+        local = False
         attrs = []
         sort_attrs = []
         action_types = []
@@ -844,12 +930,19 @@
                         sort_attrs.append(arg)
                 elif opt == "-t":
                         action_types.extend(arg.split(","))
+                elif opt == "-r":
+                        remote = True
                 elif opt == "-m":
                         display_raw = True
                 elif opt == "-f":
                         # Undocumented, for now.
                         display_nofilters = True
 
+        if not remote and not local:
+                local = True
+        elif local and remote:
+                usage(_("contents: -l and -r may not be combined"))
+
         if display_raw:
                 display_headers = False
                 attrs = [ "action.raw" ]
@@ -868,11 +961,60 @@
 
         img.load_catalogs(progress.NullProgressTracker())
 
-        err, fmris = installed_fmris_from_args(img, pargs)
-        if err != 0:
-                return err
-        if not fmris:
-                return 0
+        if local:
+                err, fmris = installed_fmris_from_args(img, pargs)
+                if err != 0:
+                        return err
+                if not fmris:
+                        return 0
+        elif remote:
+                fmris = []
+
+                # XXX This loop really needs not to be copied from
+                # Image.make_install_plan()!
+                for p in pargs:
+                        try:
+                                matches = img.get_matching_fmris(p)
+                        except KeyError:
+                                print _("""\
+pkg: no package matching '%s' could be found in current catalog
+     suggest relaxing pattern, refreshing and/or examining catalogs""") % p
+                                error = 1
+                                continue
+
+                        pnames = {}
+                        pmatch = []
+                        npnames = {}
+                        npmatch = []
+                        for m in matches:
+                                if m.preferred_authority():
+                                        pnames[m.get_pkg_stem()] = 1
+                                        pmatch.append(m)
+                                else:
+                                        npnames[m.get_pkg_stem()] = 1
+                                        npmatch.append(m)
+
+                        if len(pnames.keys()) > 1:
+                                print \
+                                    _("pkg: '%s' matches multiple packages") % p
+                                for k in pnames.keys():
+                                        print "\t%s" % k
+                                error = 1
+                                continue
+                        elif len(pnames.keys()) < 1 and len(npnames.keys()) > 1:
+                                print \
+                                    _("pkg: '%s' matches multiple packages") % p
+                                for k in npnames.keys():
+                                        print "\t%s" % k
+                                error = 1
+                                continue
+
+                        # matches is a list reverse sorted by version, so take
+                        # the first; i.e., the latest.
+                        if len(pmatch) > 0:
+                                fmris.append(pmatch[0])
+                        else:
+                                fmris.append(npmatch[0]) 
 
         #
         # If the user specifies no specific attrs, and no specific
@@ -904,9 +1046,6 @@
         display_contents_results(actionlist, attrs, sort_attrs, action_types,
             display_headers)
 
-        
-        
-
 def display_catalog_failures(failures):
         total, succeeded = failures.args[1:3]
         print _("pkg: %s/%s catalogs successfully updated:") % \
--- a/src/man/pkg.1.txt	Thu Apr 17 19:03:51 2008 -0700
+++ b/src/man/pkg.1.txt	Fri Apr 18 16:17:09 2008 -0700
@@ -11,8 +11,8 @@
      /usr/bin/pkg uninstall [-nrvq] pkg_fmri ...
 
      /usr/bin/pkg verify [-fHvq] [pkg_fmri_pattern ...]
-     /usr/bin/pkg info pkg_fmri_pattern [pkg_fmri_pattern ...]
-     /usr/bin/pkg contents [-Hm] [-o attribute ...] [-s sort_key]
+     /usr/bin/pkg info [-lr] [--license] [pkg_fmri_pattern ...]
+     /usr/bin/pkg contents [-Hmr] [-o attribute ...] [-s sort_key]
          [-t action_type ... ] [pkg_fmri_pattern ...]
      /usr/bin/pkg list [-aHsuv] [pkg_fmri_pattern ...]
      /usr/bin/pkg search [-lr] [-s server] token
@@ -93,12 +93,21 @@
           uninstall any packages which are dependent on the initial
           package.
 
-     info [pkg_fmri_pattern ...]
+     info [-lr] [--license] [pkg_fmri_pattern ...]
           Display information about packages in a human-readable form.
           Multiple FMRI patterns may be specified; with no patterns,
           display information on all installed packages in the image.
 
-     contents [-Hm] [-o attribute ...] [-s sort_key] [-t action_type ... ]
+          With -l, use the data available from locally installed packages.
+          This is the default.
+
+          With -r, retrieve the data from the servers.  Note that you must
+          specify one or more package patterns in this case.
+
+          With --license, print out the license text(s) for the package.
+          This may be combined with -l or -r.
+
+     contents [-Hmr] [-o attribute ...] [-s sort_key] [-t action_type ... ]
               [pkg_fmri_pattern ...]
           Display the contents (action attributes) of packages in the
           current image.  By default, only the path attribute is displayed,
@@ -116,10 +125,13 @@
 
           The -H option causes the headers to be omitted.
 
+          The -r option retrieves the requested data from the server, for
+          use in cases when the package is not already installed.
+
           With no arguments, the output includes all installed packages.
           Alternatively, multiple FMRI patterns may be specified, which
-          restricts the display to the contents of the matching
-          packages.
+          restricts the display to the contents of the matching packages.
+          When using -r, one or more pkg_fmri_patterns must be specified.
 
           Several special "pseudo" attribute names are available for
           convenience:
--- a/src/modules/actions/file.py	Thu Apr 17 19:03:51 2008 -0700
+++ b/src/modules/actions/file.py	Fri Apr 18 16:17:09 2008 -0700
@@ -35,6 +35,7 @@
 import sha
 from stat import *
 import generic
+import pkg.misc as misc
 import pkg.portable as portable
 try:
         import pkg.elf as elf
@@ -107,7 +108,7 @@
 
                         stream = self.data()
                         tfile = file(temp, "wb")
-                        shasum = generic.gunzip_from_stream(stream, tfile)
+                        shasum = misc.gunzip_from_stream(stream, tfile)
 
                         tfile.close()
                         stream.close()
--- a/src/modules/actions/generic.py	Thu Apr 17 19:03:51 2008 -0700
+++ b/src/modules/actions/generic.py	Fri Apr 18 16:17:09 2008 -0700
@@ -28,8 +28,7 @@
 """module describing a generic packaging object
 
 This module contains the Action class, which represents a generic packaging
-object.  It also contains a helper function, gunzip_from_stream(), which actions
-may use to decompress their data payloads."""
+object."""
 
 import os
 import sha
@@ -37,73 +36,9 @@
 import errno
 
 import pkg.actions
+import pkg.client.retrieve as retrieve
 import pkg.portable as portable
 
-def gunzip_from_stream(gz, outfile):
-        """Decompress a gzipped input stream into an output stream.
-
-        The argument 'gz' is an input stream of a gzipped file (XXX make it do
-        either a gzipped file or raw zlib compressed data), and 'outfile' is is
-        an output stream.  gunzip_from_stream() decompresses data from 'gz' and
-        writes it to 'outfile', and returns the hexadecimal SHA-1 sum of that
-        data.
-        """
-
-        FHCRC = 2
-        FEXTRA = 4
-        FNAME = 8
-        FCOMMENT = 16
-
-        # Read the header
-        magic = gz.read(2)
-        if magic != "\037\213":
-                raise IOError, "Not a gzipped file"
-        method = ord(gz.read(1))
-        if method != 8:
-                raise IOError, "Unknown compression method"
-        flag = ord(gz.read(1))
-        gz.read(6) # Discard modtime, extraflag, os
-
-        # Discard an extra field
-        if flag & FEXTRA:
-                xlen = ord(gz.read(1))
-                xlen = xlen + 256 * ord(gz.read(1))
-                gz.read(xlen)
-
-        # Discard a null-terminated filename
-        if flag & FNAME:
-                while True:
-                        s = gz.read(1)
-                        if not s or s == "\000":
-                                break
-
-        # Discard a null-terminated comment
-        if flag & FCOMMENT:
-                while True:
-                        s = gz.read(1)
-                        if not s or s == "\000":
-                                break
-
-        # Discard a 16-bit CRC
-        if flag & FHCRC:
-                gz.read(2)
-
-        shasum = sha.new()
-        dcobj = zlib.decompressobj(-zlib.MAX_WBITS)
-
-        while True:
-                buf = gz.read(64 * 1024)
-                if buf == "":
-                        ubuf = dcobj.flush()
-                        shasum.update(ubuf)
-                        outfile.write(ubuf)
-                        break
-                ubuf = dcobj.decompress(buf)
-                shasum.update(ubuf)
-                outfile.write(ubuf)
-
-        return shasum.hexdigest()
-
 class Action(object):
         """Class representing a generic packaging object.
 
@@ -409,6 +344,18 @@
                         if e.errno != errno.EPERM:
                                 raise
 
+        def get_remote_opener(self, img, fmri):
+                """Return an opener for the action's datastream which pulls from
+                the server.  The caller may have to decompress the datastream."""
+
+                if not hasattr(self, "hash"):
+                        return None
+
+                def opener():
+                        return retrieve.get_datastream(img, fmri, self.hash)
+
+                return opener
+
         def verify(self, img, **args):
                 """returns True if correctly installed in the given image"""
                 return ["verify method for action type %s unimplemented" % self.name]
--- a/src/modules/actions/license.py	Thu Apr 17 19:03:51 2008 -0700
+++ b/src/modules/actions/license.py	Fri Apr 18 16:17:09 2008 -0700
@@ -38,6 +38,7 @@
 from stat import *
 
 import generic
+import pkg.misc as misc
 import pkg.portable as portable
 
 class LicenseAction(generic.Action):
@@ -79,7 +80,7 @@
                 lfile = file(path, "wb")
                 # XXX Should throw an exception if shasum doesn't match
                 # self.hash
-                shasum = generic.gunzip_from_stream(stream, lfile)
+                shasum = misc.gunzip_from_stream(stream, lfile)
 
                 lfile.close()
                 stream.close()
@@ -131,3 +132,16 @@
                 except OSError,e:
                         if e.errno != errno.ENOENT:
                                 raise
+
+        def get_local_opener(self, img, fmri):
+                """Return an opener for the license text from the local disk."""
+
+                path = os.path.normpath(os.path.join(img.imgdir, "pkg",
+                    fmri.get_dir_path(), "license." + self.attrs["license"]))
+
+                def opener():
+                        # XXX Do we check to make sure that what's there is what
+                        # we think is there (i.e., re-hash)?
+                        return file(path, "rb")
+
+                return opener
--- a/src/modules/client/image.py	Thu Apr 17 19:03:51 2008 -0700
+++ b/src/modules/client/image.py	Fri Apr 18 16:17:09 2008 -0700
@@ -972,8 +972,10 @@
 
         def load_optional_dependencies(self):
                 for fmri in self.gen_installed_pkgs():
-                        deps = self.get_manifest(fmri, filtered = True).get_dependencies()
-                        for required, min_fmri, max_fmri in deps:
+                        mfst = self.get_manifest(fmri, filtered = True)
+
+                        for dep in mfst.gen_actions_by_type("depend"):
+                                required, min_fmri, max_fmri = dep.parse(self)
                                 if required == False:
                                         self.update_optional_dependency(min_fmri)
 
--- a/src/modules/client/retrieve.py	Thu Apr 17 19:03:51 2008 -0700
+++ b/src/modules/client/retrieve.py	Fri Apr 18 16:17:09 2008 -0700
@@ -72,14 +72,14 @@
                     fmri.get_url_path(), ssl_creds = ssl_tuple,
                     imgtype = img.type)
         except urllib2.HTTPError, e:
-                raise NameError, "could not retrieve file '%s' from '%s'" % \
-                    (hash, url_prefix)
+                raise NameError, "could not retrieve manifest '%s' from '%s'" % \
+                    (fmri.get_url_path(), url_prefix)
         except urllib2.URLError, e:
                 if len(e.args) == 1 and isinstance(e.args[0], socket.sslerror):
                         raise RuntimeError, e
 
                 raise NameError, "could not retrieve manifest '%s' from '%s'" % \
-                    (hash, url_prefix)
+                    (fmri.get_url_path(), url_prefix)
         except:
                 raise NameError, "could not retrieve manifest '%s' from '%s'" % \
                     (fmri.get_url_path(), url_prefix)
--- a/src/modules/manifest.py	Thu Apr 17 19:03:51 2008 -0700
+++ b/src/modules/manifest.py	Fri Apr 18 16:17:09 2008 -0700
@@ -23,6 +23,7 @@
 # Use is subject to license terms.
 
 import os
+import errno
 import cPickle
 from itertools import groupby, chain
 
@@ -185,13 +186,10 @@
                                 out += "%s -> %s\n" % (src, dest)
                 return out
 
-        def get_dependencies(self):
-                """ generate list of dependencies in this manifest """
-                return [
-                           a.parse(self.img)
-                           for a in self.actions
-                           if a.name == "depend"
-                ]
+        def gen_actions_by_type(self, type):
+                """Generate actions in the manifest of type "type"."""
+
+                return (a for a in self.actions if a.name == type)
 
         def filter(self, filters):
                 """Filter out actions from the manifest based on filters."""
@@ -302,7 +300,11 @@
                 try:
                         mfile = file(mfst_path, "w")
                 except IOError:
-                        os.makedirs(os.path.dirname(mfst_path))
+                        try:
+                                os.makedirs(os.path.dirname(mfst_path))
+                        except OSError, e:
+                                if e.errno != errno.EEXIST:
+                                        raise
                         mfile = file(mfst_path, "w")
 
                 #
--- a/src/modules/misc.py	Thu Apr 17 19:03:51 2008 -0700
+++ b/src/modules/misc.py	Fri Apr 18 16:17:09 2008 -0700
@@ -30,6 +30,8 @@
 import httplib
 import platform
 import re
+import sha
+import zlib
 
 import pkg.urlhelpers as urlhelpers
 import pkg.portable as portable
@@ -152,3 +154,68 @@
                 return True
 
         return False
+
+def gunzip_from_stream(gz, outfile):
+        """Decompress a gzipped input stream into an output stream.
+
+        The argument 'gz' is an input stream of a gzipped file (XXX make it do
+        either a gzipped file or raw zlib compressed data), and 'outfile' is is
+        an output stream.  gunzip_from_stream() decompresses data from 'gz' and
+        writes it to 'outfile', and returns the hexadecimal SHA-1 sum of that
+        data.
+        """
+
+        FHCRC = 2
+        FEXTRA = 4
+        FNAME = 8
+        FCOMMENT = 16
+
+        # Read the header
+        magic = gz.read(2)
+        if magic != "\037\213":
+                raise IOError, "Not a gzipped file"
+        method = ord(gz.read(1))
+        if method != 8:
+                raise IOError, "Unknown compression method"
+        flag = ord(gz.read(1))
+        gz.read(6) # Discard modtime, extraflag, os
+
+        # Discard an extra field
+        if flag & FEXTRA:
+                xlen = ord(gz.read(1))
+                xlen = xlen + 256 * ord(gz.read(1))
+                gz.read(xlen)
+
+        # Discard a null-terminated filename
+        if flag & FNAME:
+                while True:
+                        s = gz.read(1)
+                        if not s or s == "\000":
+                                break
+
+        # Discard a null-terminated comment
+        if flag & FCOMMENT:
+                while True:
+                        s = gz.read(1)
+                        if not s or s == "\000":
+                                break
+
+        # Discard a 16-bit CRC
+        if flag & FHCRC:
+                gz.read(2)
+
+        shasum = sha.new()
+        dcobj = zlib.decompressobj(-zlib.MAX_WBITS)
+
+        while True:
+                buf = gz.read(64 * 1024)
+                if buf == "":
+                        ubuf = dcobj.flush()
+                        shasum.update(ubuf)
+                        outfile.write(ubuf)
+                        break
+                ubuf = dcobj.decompress(buf)
+                shasum.update(ubuf)
+                outfile.write(ubuf)
+
+        return shasum.hexdigest()
--- a/src/tests/cli/t_commandline.py	Thu Apr 17 19:03:51 2008 -0700
+++ b/src/tests/cli/t_commandline.py	Fri Apr 18 16:17:09 2008 -0700
@@ -140,5 +140,42 @@
                 self.pkg("set-authority -O http://test1:abcde test2", exit=1)
                 self.pkg("set-authority -O ftp://test2 test2", exit=1)
 
+        def test_info_local_remote(self):
+                """pkg: check that info behaves for local and remote cases."""
+
+                pkg1 = """
+                    open [email protected],5.11-0
+                    add dir mode=0755 owner=root group=bin path=/bin
+                    close
+                """
+
+                pkg2 = """
+                    open [email protected],5.11-0
+                    add dir mode=0755 owner=root group=bin path=/bin
+                    close
+                """
+
+                durl = self.dc.get_depot_url()
+
+                self.pkgsend_bulk(durl, pkg1)
+                self.pkgsend_bulk(durl, pkg2)
+
+                self.image_create(durl)
+
+                # Install one package and verify
+                self.pkg("install jade")
+                self.pkg("verify -v")
+                
+                # Check local info
+                self.pkg("info jade | grep 'State: Installed'")
+                self.pkg("info turquoise | grep 'no packages matching'")
+                self.pkg("info emerald", exit = 1)
+                self.pkg("info emerald 2>&1 | grep 'no matching packages'")
+
+                # Check remote info
+                self.pkg("info -r jade | grep 'State: Installed'")
+                self.pkg("info -r turquoise| grep 'State: Not installed'")
+                self.pkg("info -r emerald | grep 'no package matching'")
+
 if __name__ == "__main__":
         unittest.main()