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