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