1 #!/usr/bin/python2.4
   2 #
   3 # CDDL HEADER START
   4 #
   5 # The contents of this file are subject to the terms of the
   6 # Common Development and Distribution License (the "License").
   7 # You may not use this file except in compliance with the License.
   8 #
   9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
  10 # or http://www.opensolaris.org/os/licensing.
  11 # See the License for the specific language governing permissions
  12 # and limitations under the License.
  13 #
  14 # When distributing Covered Code, include this CDDL HEADER in each
  15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
  16 # If applicable, add the following below this CDDL HEADER, with the
  17 # fields enclosed by brackets "[]" replaced with your own identifying
  18 # information: Portions Copyright [yyyy] [name of copyright owner]
  19 #
  20 # CDDL HEADER END
  21 #
  22 
  23 #
  24 # Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
  25 # Use is subject to license terms.
  26 #
  27 
  28 """module describing a generic packaging object
  29 
  30 This module contains the Action class, which represents a generic packaging
  31 object."""
  32 
  33 from cStringIO import StringIO
  34 import errno
  35 import os
  36 try:
  37         # Some versions of python don't have these constants.
  38         os.SEEK_SET
  39 except AttributeError:
  40         os.SEEK_SET, os.SEEK_CUR, os.SEEK_END = range(3)
  41 import pkg.actions
  42 import pkg.client.retrieve as retrieve
  43 import pkg.portable as portable
  44 
  45 class Action(object):
  46         """Class representing a generic packaging object.
  47 
  48         An Action is a very simple wrapper around two dictionaries: a named set
  49         of data streams and a set of attributes.  Data streams generally
  50         represent files on disk, and attributes represent metadata about those
  51         files.
  52         """
  53 
  54         # 'name' is the name of the action, as specified in a manifest.
  55         name = "generic"
  56         # 'attributes' is a list of the known usable attributes.  Or something.
  57         # There probably isn't a good use for it.
  58         attributes = ()
  59         # 'key_attr' is the name of the attribute whose value must be unique in
  60         # the namespace of objects represented by a particular action.  For
  61         # instance, a file's key_attr would be its pathname.  Or a driver's
  62         # key_attr would be the driver name.  When 'key_attr' is None, it means
  63         # that all attributes of the action are distinguishing.
  64         key_attr = None
  65 
  66         # the following establishes the sort order between action types.
  67         # Directories must precede all
  68         # filesystem-modifying actions; hardlinks must follow all
  69         # filesystem-modifying actions.  Note that usr/group actions
  70         # preceed file actions; this implies that /etc/group and /etc/passwd
  71         # file ownership needs to be part of initial contents of those files
  72 
  73         orderdict = {}
  74         unknown = 0
  75 
  76         def loadorderdict(self):
  77                 ol = [
  78                         "set",
  79                         "depend",
  80                         "group",
  81                         "user",
  82                         "dir",
  83                         "file",
  84                         "hardlink",
  85                         "link",
  86                         "driver",
  87                         "unknown",
  88                         "legacy"
  89                         ]
  90                 self.orderdict.update(dict((
  91                     (pkg.actions.types[t], i) for i, t in enumerate(ol)
  92                     )))
  93                 self.unknown = self.orderdict[pkg.actions.types["unknown"]]
  94 
  95         def __init__(self, data=None, **attrs):
  96                 """Action constructor.
  97 
  98                 The optional 'data' argument may be either a string, a file-like
  99                 object, or a callable.  If it is a string, then it will be
 100                 substituted with a callable that will return an open handle to
 101                 the file represented by the string.  Otherwise, if it is not
 102                 already a callable, it is assumed to be a file-like object, and
 103                 will be substituted with a callable that will return the object.
 104                 If it is a callable, it will not be replaced at all.
 105 
 106                 Any remaining named arguments will be treated as attributes.
 107                 """
 108 
 109                 if not self.orderdict:
 110                         self.loadorderdict()
 111                 self.ord = self.orderdict.get(type(self), self.unknown)
 112 
 113                 self.attrs = attrs
 114 
 115                 if data == None:
 116                         self.data = None
 117                         return
 118 
 119                 if isinstance(data, basestring):
 120                         if not os.path.exists(data):
 121                                 raise pkg.actions.ActionDataError(
 122                                     _("No such file: '%s'.") % data)
 123                         elif os.path.isdir(data):
 124                                 raise pkg.actions.ActionDataError(
 125                                     _("'%s' is not a file.") % data)
 126 
 127                         def file_opener():
 128                                 return open(data, "rb")
 129                         self.data = file_opener
 130                         if "pkg.size" not in self.attrs:
 131                                 try:
 132                                         fs = os.stat(data)
 133                                         self.attrs["pkg.size"] = str(fs.st_size)
 134                                 except EnvironmentError, e:
 135                                         raise \
 136                                             pkg.actions.ActionDataError(
 137                                             e)
 138                         return
 139 
 140                 if callable(data):
 141                         # Data is not None, and is callable.
 142                         self.data = data
 143                         return
 144 
 145                 if "pkg.size" in self.attrs:
 146                         self.data = lambda: data
 147                         return
 148 
 149                 try:
 150                         sz = data.size
 151                 except AttributeError:
 152                         try:
 153                                 try:
 154                                         sz = os.fstat(data.fileno()).st_size
 155                                 except (AttributeError, TypeError):
 156                                         try:
 157                                                 try:
 158                                                         data.seek(0,
 159                                                             os.SEEK_END)
 160                                                         sz = data.tell()
 161                                                         data.seek(0)
 162                                                 except (AttributeError,
 163                                                     TypeError):
 164                                                         d = data.read()
 165                                                         sz = len(d)
 166                                                         data = StringIO(d)
 167                                         except (AttributeError, TypeError):
 168                                                 # Raw data was provided; fake a
 169                                                 # file object.
 170                                                 sz = len(data)
 171                                                 data = StringIO(data)
 172                         except EnvironmentError, e:
 173                                 raise pkg.actions.ActionDataError(e)
 174 
 175                 self.attrs["pkg.size"] = str(sz)
 176                 self.data = lambda: data
 177 
 178         def __str__(self):
 179                 """Serialize the action into manifest form.
 180 
 181                 The form is the name, followed by the hash, if it exists,
 182                 followed by attributes in the form 'key=value'.  All fields are
 183                 space-separated; fields with spaces in the values are quoted.
 184 
 185                 Note that an object with a datastream may have been created in
 186                 such a way that the hash field is not populated, or not
 187                 populated with real data.  The action classes do not guarantee
 188                 that at the time that __str__() is called, the hash is properly
 189                 computed.  This may need to be done externally.
 190                 """
 191 
 192                 out = self.name
 193                 if hasattr(self, "hash"):
 194                         out += " " + self.hash
 195 
 196                 def q(s):
 197                         if " " in s:
 198                                 return '"%s"' % s
 199                         else:
 200                                 return s
 201 
 202                 # Sort so that we get consistent action attribute ordering.
 203                 # We pay a performance penalty to do so, but it seems worth it.
 204                 for k in sorted(self.attrs.keys()):
 205                         v = self.attrs[k]
 206                         if isinstance(v, list):
 207                                 out += " " + " ".join([
 208                                     "%s=%s" % (k, q(lmt)) for lmt in v
 209                                 ])
 210                         elif " " in v:
 211                                 out += " " + k + "=\"" + v + "\""
 212                         else:
 213                                 out += " " + k + "=" + v
 214 
 215                 return out
 216 
 217         def __cmp__(self, other):
 218                 """Compare actions for ordering.  The ordinality of a
 219                    given action is computed and stored at action
 220                    initialization."""
 221 
 222                 res = cmp(self.ord, other.ord)
 223                 if res == 0:
 224                         return self.compare(other) # often subclassed
 225 
 226                 return res
 227 
 228         def compare(self, other):
 229                 return cmp(id(self), id(other))
 230 
 231         def different(self, other):
 232                 """Returns True if other represents a non-ignorable change from
 233                 self.
 234 
 235                 By default, this means two actions are different if any of their
 236                 attributes are different.  Subclasses should override this
 237                 behavior when appropriate.
 238                 """
 239 
 240                 # We could ignore key_attr, or possibly assert that it's the
 241                 # same.
 242                 sset = set(self.attrs.keys())
 243                 oset = set(other.attrs.keys())
 244                 if sset.symmetric_difference(oset):
 245                         return True
 246 
 247                 for a in self.attrs:
 248                         if self.attrs[a] != other.attrs[a]:
 249                                 return True
 250 
 251                 if hasattr(self, "hash"):
 252                         assert(hasattr(other, "hash"))
 253                         if self.hash != other.hash:
 254                                 return True
 255 
 256                 return False
 257 
 258         def differences(self, other):
 259                 """Returns a list of attributes that have different values
 260                 between other and self"""
 261                 sset = set(self.attrs.keys())
 262                 oset = set(other.attrs.keys())
 263                 l = list(sset.symmetric_difference(oset))
 264                 l.extend([ k
 265                            for k in sset.intersection(oset)
 266                            if self.attrs[k] != other.attrs[k]
 267                            ])
 268                 return (l)
 269 
 270         def generate_indices(self):
 271                 """Generate the information needed to index this action.
 272 
 273                 This method, and the overriding methods in subclasses, produce
 274                 a list of four-tuples.  The tuples are of the form
 275                 (action_name, key, token, full value).  action_name is the
 276                 string representation of the kind of action generating the
 277                 tuple.  'file' and 'depend' are two examples.  It is required to
 278                 not be None.  Key is the string representation of the name of
 279                 the attribute being indexed.  Examples include 'basename' and
 280                 'path'.  Token is the token to be searched against.  Full value
 281                 is the value to display to the user in the event this token
 282                 matches their query.  This is useful for things like categories
 283                 where what matched the query may be a substring of what the
 284                 desired user output is.
 285                 """
 286 
 287                 if hasattr(self, "hash"):
 288                         return [
 289                             (self.name, "content", self.hash, self.hash),
 290                         ]
 291                 return []
 292 
 293         def distinguished_name(self):
 294                 """ Return the distinguishing name for this action,
 295                     preceded by the type of the distinguishing name.  For
 296                     example, for a file action, 'path' might be the
 297                     key_attr.  So, the distinguished name might be
 298                     "path: usr/lib/libc.so.1".
 299                 """
 300 
 301                 if self.key_attr == None:
 302                         return str(self)
 303                 return "%s: %s" % \
 304                     (self.name, self.attrs.get(self.key_attr, "???"))
 305 
 306         @staticmethod
 307         def makedirs(path, **kw):
 308                 """Make directory specified by 'path' with given permissions, as
 309                 well as all missing parent directories.  Permissions are
 310                 specified by the keyword arguments 'mode', 'uid', and 'gid'.
 311 
 312                 The difference between this and os.makedirs() is that the
 313                 permissions specify only those of the leaf directory.  Missing
 314                 parent directories inherit the permissions of the deepest
 315                 existing directory.  The leaf directory will also inherit any
 316                 permissions not explicitly set."""
 317 
 318                 # generate the components of the path.  The first
 319                 # element will be empty since all absolute paths
 320                 # always start with a root specifier.
 321                 pathlist = portable.split_path(path)
 322 
 323                 # Fill in the first path with the root of the filesystem
 324                 # (this ends up being something like C:\ on windows systems,
 325                 # and "/" on unix.
 326                 pathlist[0] = portable.get_root(path)
 327 
 328                 g = enumerate(pathlist)
 329                 for i, e in g:
 330                         if not os.path.isdir(os.path.join(*pathlist[:i + 1])):
 331                                 break
 332                 else:
 333                         # XXX Because the filelist codepath may create
 334                         # directories with incorrect permissions (see
 335                         # pkgtarfile.py), we need to correct those permissions
 336                         # here.  Note that this solution relies on all
 337                         # intermediate directories being explicitly created by
 338                         # the packaging system; otherwise intermediate
 339                         # directories will not get their permissions corrected.
 340                         stat = os.stat(path)
 341                         mode = kw.get("mode", stat.st_mode)
 342                         uid = kw.get("uid", stat.st_uid)
 343                         gid = kw.get("gid", stat.st_gid)
 344                         try:
 345                                 if mode != stat.st_mode:
 346                                         os.chmod(path, mode)
 347                                 if uid != stat.st_uid or gid != stat.st_gid:
 348                                         portable.chown(path, uid, gid)
 349                         except  OSError, e:
 350                                 if e.errno != errno.EPERM and \
 351                                     e.errno != errno.ENOSYS:
 352                                         raise
 353                         return
 354 
 355                 stat = os.stat(os.path.join(*pathlist[:i]))
 356                 for i, e in g:
 357                         p = os.path.join(*pathlist[:i])
 358                         os.mkdir(p, stat.st_mode)
 359                         os.chmod(p, stat.st_mode)
 360                         try:
 361                                 portable.chown(p, stat.st_uid, stat.st_gid)
 362                         except OSError, e:
 363                                 if e.errno != errno.EPERM:
 364                                         raise
 365 
 366                 # Create the leaf with any requested permissions, substituting
 367                 # missing perms with the parent's perms.
 368                 mode = kw.get("mode", stat.st_mode)
 369                 uid = kw.get("uid", stat.st_uid)
 370                 gid = kw.get("gid", stat.st_gid)
 371                 os.mkdir(path, mode)
 372                 os.chmod(path, mode)
 373                 try:
 374                         portable.chown(path, uid, gid)
 375                 except OSError, e:
 376                         if e.errno != errno.EPERM:
 377                                 raise
 378 
 379         def get_varcet_keys(self):
 380                 """Return the names of any facet or variant tags in this
 381                 action."""
 382 
 383                 variants = []
 384                 facets = []
 385 
 386                 for k in self.attrs.iterkeys():
 387                         if k.startswith("variant."):
 388                                 variants.append(k)
 389                         if k.startswith("facet."):
 390                                 facets.append(k)
 391                 return variants, facets
 392 
 393         def get_remote_opener(self, img, fmri):
 394                 """Return an opener for the action's datastream which pulls from
 395                 the server.  The caller may have to decompress the
 396                 datastream."""
 397 
 398                 if not hasattr(self, "hash"):
 399                         return None
 400 
 401                 def opener():
 402                         return retrieve.get_datastream(img, fmri, self.hash)
 403 
 404                 return opener
 405 
 406         def verify(self, img, **args):
 407                 """Returns an empty list if correctly installed in the given
 408                 image."""
 409                 return []
 410 
 411         def needsdata(self, orig):
 412                 """Returns True if the action transition requires a
 413                 datastream."""
 414                 return False
 415 
 416         def attrlist(self, name):
 417                 """return list containing value of named attribute."""
 418                 value = self.attrs.get(name, [])
 419                 if isinstance(value, list):
 420                         return value
 421                 else:
 422                         return [ value ]
 423 
 424         def directory_references(self):
 425                 """Returns references to paths in action."""
 426                 if "path" in self.attrs:
 427                         return [os.path.dirname(os.path.normpath(
 428                             self.attrs["path"]))]
 429                 return []
 430 
 431         def preinstall(self, pkgplan, orig):
 432                 """Client-side method that performs pre-install actions."""
 433                 pass
 434 
 435         def install(self, pkgplan, orig):
 436                 """Client-side method that installs the object."""
 437                 pass
 438 
 439         def postinstall(self, pkgplan, orig):
 440                 """Client-side method that performs post-install actions."""
 441                 pass
 442 
 443         def preremove(self, pkgplan):
 444                 """Client-side method that performs pre-remove actions."""
 445                 pass
 446 
 447         def remove(self, pkgplan):
 448                 """Client-side method that removes the object."""
 449                 pass
 450 
 451         def postremove(self, pkgplan):
 452                 """Client-side method that performs post-remove actions."""
 453                 pass
 454 
 455         def include_this(self, excludes):
 456                 """Callables in excludes list returns True
 457                 if action is to be included, False if
 458                 not"""
 459                 for c in excludes:
 460                         if not c(self):
 461                                 return False
 462                 return True