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