Print this page
5189 validate_pkg should support mediated links
Reviewed by: Richard Lowe <richlowe@richlowe.net>
Reviewed by: Josef 'Jeff' Sipek <josef.sipek@nexenta.com>
Split |
Close |
Expand all |
Collapse all |
--- old/usr/src/tools/scripts/validate_pkg.py
+++ new/usr/src/tools/scripts/validate_pkg.py
1 1 #!/usr/bin/python2.6
2 2 #
3 3 # CDDL HEADER START
4 4 #
5 5 # The contents of this file are subject to the terms of the
6 6 # Common Development and Distribution License (the "License").
7 7 # You may not use this file except in compliance with the License.
8 8 #
9 9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10 10 # or http://www.opensolaris.org/os/licensing.
11 11 # See the License for the specific language governing permissions
12 12 # and limitations under the License.
13 13 #
14 14 # When distributing Covered Code, include this CDDL HEADER in each
15 15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 16 # If applicable, add the following below this CDDL HEADER, with the
17 17 # fields enclosed by brackets "[]" replaced with your own identifying
18 18 # information: Portions Copyright [yyyy] [name of copyright owner]
19 19 #
20 20 # CDDL HEADER END
21 21 #
22 22
23 23 #
24 24 # Copyright 2010 Sun Microsystems, Inc. All rights reserved.
25 25 # Use is subject to license terms.
26 26 #
27 27
28 28 #
29 29 # Compare the content generated by a build to a set of manifests
30 30 # describing how that content is to be delivered.
31 31 #
32 32
33 33
34 34 import getopt
35 35 import os
36 36 import stat
37 37 import sys
38 38
39 39 from pkg import actions
40 40 from pkg import manifest
41 41
42 42
43 43 #
44 44 # Dictionary used to map action names to output format. Each entry is
45 45 # indexed by action name, and consists of a list of tuples that map
46 46 # FileInfo class members to output labels.
47 47 #
48 48 OUTPUTMAP = {
49 49 "dir": [
50 50 ("group", "group="),
51 51 ("mode", "mode="),
52 52 ("owner", "owner="),
↓ open down ↓ |
52 lines elided |
↑ open up ↑ |
53 53 ("path", "path=")
54 54 ],
55 55 "file": [
56 56 ("hash", ""),
57 57 ("group", "group="),
58 58 ("mode", "mode="),
59 59 ("owner", "owner="),
60 60 ("path", "path=")
61 61 ],
62 62 "link": [
63 + ("mediator", "mediator="),
63 64 ("path", "path="),
64 65 ("target", "target=")
65 66 ],
66 67 "hardlink": [
67 68 ("path", "path="),
68 69 ("hardkey", "target=")
69 70 ],
70 71 }
71 72
72 73 # Mode checks used to validate safe file and directory permissions
73 74 ALLMODECHECKS = frozenset(("m", "w", "s", "o"))
74 75 DEFAULTMODECHECKS = frozenset(("m", "w", "o"))
75 76
76 77 class FileInfo(object):
77 78 """Base class to represent a file.
78 79
79 80 Subclassed according to whether the file represents an actual filesystem
80 81 object (RealFileInfo) or an IPS manifest action (ActionInfo).
81 82 """
82 83
83 84 def __init__(self):
84 85 self.path = None
85 86 self.isdir = False
86 87 self.target = None
87 88 self.owner = None
88 89 self.group = None
89 90 self.mode = None
90 91 self.hardkey = None
91 92 self.hardpaths = set()
92 93 self.editable = False
93 94
94 95 def name(self):
95 96 """Return the IPS action name of a FileInfo object.
96 97 """
97 98 if self.isdir:
98 99 return "dir"
99 100
100 101 if self.target:
101 102 return "link"
102 103
103 104 if self.hardkey:
104 105 return "hardlink"
105 106
106 107 return "file"
107 108
108 109 def checkmodes(self, modechecks):
109 110 """Check for and report on unsafe permissions.
110 111
111 112 Returns a potentially empty list of warning strings.
112 113 """
113 114 w = []
114 115
115 116 t = self.name()
116 117 if t in ("link", "hardlink"):
117 118 return w
118 119 m = int(self.mode, 8)
119 120 o = self.owner
120 121 p = self.path
121 122
122 123 if "s" in modechecks and t == "file":
123 124 if m & (stat.S_ISUID | stat.S_ISGID):
124 125 if m & (stat.S_IRGRP | stat.S_IROTH):
125 126 w.extend(["%s: 0%o: setuid/setgid file should not be " \
126 127 "readable by group or other" % (p, m)])
127 128
128 129 if "o" in modechecks and o != "root" and ((m & stat.S_ISUID) == 0):
129 130 mu = (m & stat.S_IRWXU) >> 6
130 131 mg = (m & stat.S_IRWXG) >> 3
131 132 mo = m & stat.S_IRWXO
132 133 e = self.editable
133 134
134 135 if (((mu & 02) == 0 and (mo & mg & 04) == 04) or
135 136 (t == "file" and mo & 01 == 1) or
136 137 (mg, mo) == (mu, mu) or
137 138 ((t == "file" and not e or t == "dir" and o == "bin") and
138 139 (mg & 05 == mo & 05)) or
139 140 (t == "file" and o == "bin" and mu & 01 == 01) or
140 141 (m & 0105 != 0 and p.startswith("etc/security/dev/"))):
141 142 w.extend(["%s: owner \"%s\" may be safely " \
142 143 "changed to \"root\"" % (p, o)])
143 144
144 145 if "w" in modechecks and t == "file" and o != "root":
145 146 uwx = stat.S_IWUSR | stat.S_IXUSR
146 147 if m & uwx == uwx:
147 148 w.extend(["%s: non-root-owned executable should not " \
148 149 "also be writable by owner." % p])
149 150
150 151 if ("m" in modechecks and
151 152 m & (stat.S_IWGRP | stat.S_IWOTH) != 0 and
152 153 m & stat.S_ISVTX == 0):
153 154 w.extend(["%s: 0%o: should not be writable by group or other" %
154 155 (p, m)])
155 156
156 157 return w
157 158
158 159 def __ne__(self, other):
159 160 """Compare two FileInfo objects.
160 161
161 162 Note this is the "not equal" comparison, so a return value of False
162 163 indicates that the objects are functionally equivalent.
163 164 """
164 165 #
165 166 # Map the objects such that the lhs is always the ActionInfo,
166 167 # and the rhs is always the RealFileInfo.
167 168 #
168 169 # It's only really important that the rhs not be an
169 170 # ActionInfo; if we're comparing FileInfo the RealFileInfo, it
170 171 # won't actually matter what we choose.
171 172 #
172 173 if isinstance(self, ActionInfo):
173 174 lhs = self
174 175 rhs = other
175 176 else:
176 177 lhs = other
177 178 rhs = self
178 179
179 180 #
180 181 # Because the manifest may legitimately translate a relative
181 182 # path from the proto area into a different path on the installed
182 183 # system, we don't compare paths here. We only expect this comparison
183 184 # to be invoked on items with identical relative paths in
184 185 # first place.
185 186 #
186 187
187 188 #
188 189 # All comparisons depend on type. For symlink and directory, they
↓ open down ↓ |
116 lines elided |
↑ open up ↑ |
189 190 # must be the same. For file and hardlink, see below.
190 191 #
191 192 typelhs = lhs.name()
192 193 typerhs = rhs.name()
193 194 if typelhs in ("link", "dir"):
194 195 if typelhs != typerhs:
195 196 return True
196 197
197 198 #
198 199 # For symlinks, all that's left is the link target.
200 + # For mediated symlinks targets can differ.
199 201 #
200 202 if typelhs == "link":
201 - return lhs.target != rhs.target
203 + return (lhs.mediator is None) and (lhs.target != rhs.target)
202 204
203 205 #
204 206 # For a directory, it's important that both be directories,
205 207 # the modes be identical, and the paths are identical. We already
206 208 # checked all but the modes above.
207 209 #
208 210 # If both objects are files, then we're in the same boat.
209 211 #
210 212 if typelhs == "dir" or (typelhs == "file" and typerhs == "file"):
211 213 return lhs.mode != rhs.mode
212 214
213 215 #
214 216 # For files or hardlinks:
215 217 #
216 218 # Since the key space is different (inodes for real files and
217 219 # actual link targets for hard links), and since the proto area will
218 220 # identify all N occurrences as hardlinks, but the manifests as one
219 221 # file and N-1 hardlinks, we have to compare files to hardlinks.
220 222 #
221 223
222 224 #
223 225 # If they're both hardlinks, we just make sure that
224 226 # the same target path appears in both sets of
225 227 # possible targets.
226 228 #
227 229 if typelhs == "hardlink" and typerhs == "hardlink":
228 230 return len(lhs.hardpaths.intersection(rhs.hardpaths)) == 0
229 231
230 232 #
231 233 # Otherwise, we have a mix of file and hardlink, so we
232 234 # need to make sure that the file path appears in the
233 235 # set of possible target paths for the hardlink.
234 236 #
235 237 # We already know that the ActionInfo, if present, is the lhs
236 238 # operator. So it's the rhs operator that's guaranteed to
237 239 # have a set of hardpaths.
238 240 #
239 241 return lhs.path not in rhs.hardpaths
240 242
241 243 def __str__(self):
242 244 """Return an action-style representation of a FileInfo object.
243 245
244 246 We don't currently quote items with embedded spaces. If we
245 247 ever decide to parse this output, we'll want to revisit that.
246 248 """
247 249 name = self.name()
248 250 out = name
249 251
250 252 for member, label in OUTPUTMAP[name]:
251 253 out += " " + label + str(getattr(self, member))
252 254
253 255 return out
254 256
255 257 def protostr(self):
256 258 """Return a protolist-style representation of a FileInfo object.
257 259 """
258 260 target = "-"
259 261 major = "-"
260 262 minor = "-"
261 263
262 264 mode = self.mode
263 265 owner = self.owner
264 266 group = self.group
265 267
266 268 name = self.name()
267 269 if name == "dir":
268 270 ftype = "d"
269 271 elif name in ("file", "hardlink"):
270 272 ftype = "f"
271 273 elif name == "link":
272 274 ftype = "s"
273 275 target = self.target
274 276 mode = "777"
275 277 owner = "root"
276 278 group = "other"
277 279
278 280 out = "%c %-30s %-20s %4s %-5s %-5s %6d %2ld - -" % \
279 281 (ftype, self.path, target, mode, owner, group, 0, 1)
280 282
281 283 return out
282 284
283 285
284 286 class ActionInfo(FileInfo):
285 287 """Object to track information about manifest actions.
286 288
287 289 This currently understands file, link, dir, and hardlink actions.
288 290 """
289 291
290 292 def __init__(self, action):
291 293 FileInfo.__init__(self)
292 294 #
293 295 # Currently, all actions that we support have a "path"
294 296 # attribute. If that changes, then we'll need to
295 297 # catch a KeyError from this assignment.
296 298 #
297 299 self.path = action.attrs["path"]
298 300
↓ open down ↓ |
87 lines elided |
↑ open up ↑ |
299 301 if action.name == "file":
300 302 self.owner = action.attrs["owner"]
301 303 self.group = action.attrs["group"]
302 304 self.mode = action.attrs["mode"]
303 305 self.hash = action.hash
304 306 if "preserve" in action.attrs:
305 307 self.editable = True
306 308 elif action.name == "link":
307 309 target = action.attrs["target"]
308 310 self.target = os.path.normpath(target)
311 + self.mediator = action.attrs.get("mediator")
309 312 elif action.name == "dir":
310 313 self.owner = action.attrs["owner"]
311 314 self.group = action.attrs["group"]
312 315 self.mode = action.attrs["mode"]
313 316 self.isdir = True
314 317 elif action.name == "hardlink":
315 318 target = os.path.normpath(action.get_target_path())
316 319 self.hardkey = target
317 320 self.hardpaths.add(target)
318 321
319 322 @staticmethod
320 323 def supported(action):
321 324 """Indicates whether the specified IPS action time is
322 325 correctly handled by the ActionInfo constructor.
323 326 """
324 327 return action in frozenset(("file", "dir", "link", "hardlink"))
325 328
326 329
327 330 class UnsupportedFileFormatError(Exception):
328 331 """This means that the stat.S_IFMT returned something we don't
329 332 support, ie a pipe or socket. If it's appropriate for such an
330 333 object to be in the proto area, then the RealFileInfo constructor
331 334 will need to evolve to support it, or it will need to be in the
332 335 exception list.
333 336 """
334 337 def __init__(self, path, mode):
335 338 Exception.__init__(self)
336 339 self.path = path
337 340 self.mode = mode
338 341
339 342 def __str__(self):
340 343 return '%s: unsupported S_IFMT %07o' % (self.path, self.mode)
341 344
342 345
343 346 class RealFileInfo(FileInfo):
344 347 """Object to track important-to-packaging file information.
345 348
346 349 This currently handles regular files, directories, and symbolic links.
347 350
348 351 For multiple RealFileInfo objects with identical hardkeys, there
349 352 is no way to determine which of the hard links should be
350 353 delivered as a file, and which as hardlinks.
351 354 """
352 355
353 356 def __init__(self, root=None, path=None):
354 357 FileInfo.__init__(self)
355 358 self.path = path
356 359 path = os.path.join(root, path)
357 360 lstat = os.lstat(path)
358 361 mode = lstat.st_mode
↓ open down ↓ |
40 lines elided |
↑ open up ↑ |
359 362
360 363 #
361 364 # Per stat.py, these cases are mutually exclusive.
362 365 #
363 366 if stat.S_ISREG(mode):
364 367 self.hash = self.path
365 368 elif stat.S_ISDIR(mode):
366 369 self.isdir = True
367 370 elif stat.S_ISLNK(mode):
368 371 self.target = os.path.normpath(os.readlink(path))
372 + self.mediator = None
369 373 else:
370 374 raise UnsupportedFileFormatError(path, mode)
371 375
372 376 if not stat.S_ISLNK(mode):
373 377 self.mode = "%04o" % stat.S_IMODE(mode)
374 378 #
375 379 # Instead of reading the group and owner from the proto area after
376 380 # a non-root build, just drop in dummy values. Since we don't
377 381 # compare them anywhere, this should allow at least marginally
378 382 # useful comparisons of protolist-style output.
379 383 #
380 384 self.owner = "owner"
381 385 self.group = "group"
382 386
383 387 #
384 388 # refcount > 1 indicates a hard link
385 389 #
386 390 if lstat.st_nlink > 1:
387 391 #
388 392 # This could get ugly if multiple proto areas reside
389 393 # on different filesystems.
390 394 #
391 395 self.hardkey = lstat.st_ino
392 396
393 397
394 398 class DirectoryTree(dict):
395 399 """Meant to be subclassed according to population method.
396 400 """
397 401 def __init__(self, name):
398 402 dict.__init__(self)
399 403 self.name = name
400 404
401 405 def compare(self, other):
402 406 """Compare two different sets of FileInfo objects.
403 407 """
404 408 keys1 = frozenset(self.keys())
405 409 keys2 = frozenset(other.keys())
406 410
407 411 common = keys1.intersection(keys2)
408 412 onlykeys1 = keys1.difference(common)
409 413 onlykeys2 = keys2.difference(common)
410 414
411 415 if onlykeys1:
412 416 print "Entries present in %s but not %s:" % \
413 417 (self.name, other.name)
414 418 for path in sorted(onlykeys1):
415 419 print("\t%s" % str(self[path]))
416 420 print ""
417 421
418 422 if onlykeys2:
419 423 print "Entries present in %s but not %s:" % \
420 424 (other.name, self.name)
421 425 for path in sorted(onlykeys2):
422 426 print("\t%s" % str(other[path]))
423 427 print ""
424 428
425 429 nodifferences = True
426 430 for path in sorted(common):
427 431 if self[path] != other[path]:
428 432 if nodifferences:
429 433 nodifferences = False
430 434 print "Entries that differ between %s and %s:" \
431 435 % (self.name, other.name)
432 436 print("%14s %s" % (self.name, self[path]))
433 437 print("%14s %s" % (other.name, other[path]))
434 438 if not nodifferences:
435 439 print ""
436 440
437 441
438 442 class BadProtolistFormat(Exception):
439 443 """This means that the user supplied a file via -l, but at least
440 444 one line from that file doesn't have the right number of fields to
441 445 parse as protolist output.
442 446 """
443 447 def __str__(self):
444 448 return 'bad proto list entry: "%s"' % Exception.__str__(self)
445 449
446 450
447 451 class ProtoTree(DirectoryTree):
448 452 """Describes one or more proto directories as a dictionary of
449 453 RealFileInfo objects, indexed by relative path.
450 454 """
451 455
452 456 def adddir(self, proto, exceptions):
453 457 """Extends the ProtoTree dictionary with RealFileInfo
454 458 objects describing the proto dir, indexed by relative
455 459 path.
456 460 """
457 461 newentries = {}
458 462
459 463 pdir = os.path.normpath(proto)
460 464 strippdir = lambda r, n: os.path.join(r, n)[len(pdir)+1:]
461 465 for root, dirs, files in os.walk(pdir):
462 466 for name in dirs + files:
463 467 path = strippdir(root, name)
464 468 if path not in exceptions:
465 469 try:
466 470 newentries[path] = RealFileInfo(pdir, path)
467 471 except OSError, e:
468 472 sys.stderr.write("Warning: unable to stat %s: %s\n" %
469 473 (path, e))
470 474 continue
471 475 else:
472 476 exceptions.remove(path)
473 477 if name in dirs:
474 478 dirs.remove(name)
475 479
476 480 #
477 481 # Find the sets of paths in this proto dir that are hardlinks
478 482 # to the same inode.
479 483 #
480 484 # It seems wasteful to store this in each FileInfo, but we
481 485 # otherwise need a linking mechanism. With this information
482 486 # here, FileInfo object comparison can be self contained.
483 487 #
484 488 # We limit this aggregation to a single proto dir, as
485 489 # represented by newentries. That means we don't need to care
486 490 # about proto dirs on separate filesystems, or about hardlinks
487 491 # that cross proto dir boundaries.
488 492 #
489 493 hk2path = {}
490 494 for path, fileinfo in newentries.iteritems():
491 495 if fileinfo.hardkey:
492 496 hk2path.setdefault(fileinfo.hardkey, set()).add(path)
493 497 for fileinfo in newentries.itervalues():
494 498 if fileinfo.hardkey:
495 499 fileinfo.hardpaths.update(hk2path[fileinfo.hardkey])
496 500 self.update(newentries)
497 501
498 502 def addprotolist(self, protolist, exceptions):
499 503 """Read in the specified file, assumed to be the
500 504 output of protolist.
501 505
502 506 This has been tested minimally, and is potentially useful for
503 507 comparing across the transition period, but should ultimately
504 508 go away.
505 509 """
506 510
507 511 try:
508 512 plist = open(protolist)
509 513 except IOError, exc:
510 514 raise IOError("cannot open proto list: %s" % str(exc))
511 515
512 516 newentries = {}
513 517
514 518 for pline in plist:
515 519 pline = pline.split()
516 520 #
517 521 # Use a FileInfo() object instead of a RealFileInfo()
518 522 # object because we want to avoid the RealFileInfo
519 523 # constructor, because there's nothing to actually stat().
520 524 #
521 525 fileinfo = FileInfo()
522 526 try:
523 527 if pline[1] in exceptions:
524 528 exceptions.remove(pline[1])
525 529 continue
526 530 if pline[0] == "d":
527 531 fileinfo.isdir = True
528 532 fileinfo.path = pline[1]
529 533 if pline[2] != "-":
530 534 fileinfo.target = os.path.normpath(pline[2])
531 535 fileinfo.mode = int("0%s" % pline[3])
532 536 fileinfo.owner = pline[4]
533 537 fileinfo.group = pline[5]
534 538 if pline[6] != "0":
535 539 fileinfo.hardkey = pline[6]
536 540 newentries[pline[1]] = fileinfo
537 541 except IndexError:
538 542 raise BadProtolistFormat(pline)
539 543
540 544 plist.close()
541 545 hk2path = {}
542 546 for path, fileinfo in newentries.iteritems():
543 547 if fileinfo.hardkey:
544 548 hk2path.setdefault(fileinfo.hardkey, set()).add(path)
545 549 for fileinfo in newentries.itervalues():
546 550 if fileinfo.hardkey:
547 551 fileinfo.hardpaths.update(hk2path[fileinfo.hardkey])
548 552 self.update(newentries)
549 553
550 554
551 555 class ManifestParsingError(Exception):
552 556 """This means that the Manifest.set_content() raised an
553 557 ActionError. We raise this, instead, to tell us which manifest
554 558 could not be parsed, rather than what action error we hit.
555 559 """
556 560 def __init__(self, mfile, error):
557 561 Exception.__init__(self)
558 562 self.mfile = mfile
559 563 self.error = error
560 564
561 565 def __str__(self):
562 566 return "unable to parse manifest %s: %s" % (self.mfile, self.error)
563 567
564 568
565 569 class ManifestTree(DirectoryTree):
566 570 """Describes one or more directories containing arbitrarily
567 571 many manifests as a dictionary of ActionInfo objects, indexed
568 572 by the relative path of the data source within the proto area.
569 573 That path may or may not be the same as the path attribute of the
570 574 given action.
571 575 """
572 576
573 577 def addmanifest(self, root, mfile, arch, modechecks, exceptions):
574 578 """Treats the specified input file as a pkg(5) package
575 579 manifest, and extends the ManifestTree dictionary with entries
576 580 for the actions therein.
577 581 """
578 582 mfest = manifest.Manifest()
579 583 try:
580 584 mfest.set_content(open(os.path.join(root, mfile)).read())
581 585 except IOError, exc:
582 586 raise IOError("cannot read manifest: %s" % str(exc))
583 587 except actions.ActionError, exc:
584 588 raise ManifestParsingError(mfile, str(exc))
585 589
586 590 #
587 591 # Make sure the manifest is applicable to the user-specified
588 592 # architecture. Assumption: if variant.arch is not an
589 593 # attribute of the manifest, then the package should be
590 594 # installed on all architectures.
591 595 #
592 596 if arch not in mfest.attributes.get("variant.arch", (arch,)):
593 597 return
594 598
595 599 modewarnings = set()
596 600 for action in mfest.gen_actions():
597 601 if "path" not in action.attrs or \
598 602 not ActionInfo.supported(action.name):
599 603 continue
600 604
601 605 #
602 606 # The dir action is currently fully specified, in that it
603 607 # lists owner, group, and mode attributes. If that
604 608 # changes in pkg(5) code, we'll need to revisit either this
605 609 # code or the ActionInfo() constructor. It's possible
606 610 # that the pkg(5) system could be extended to provide a
607 611 # mechanism for specifying directory permissions outside
608 612 # of the individual manifests that deliver files into
609 613 # those directories. Doing so at time of manifest
610 614 # processing would mean that validate_pkg continues to work,
611 615 # but doing so at time of publication would require updates.
612 616 #
613 617
614 618 #
615 619 # See pkgsend(1) for the use of NOHASH for objects with
616 620 # datastreams. Currently, that means "files," but this
617 621 # should work for any other such actions.
618 622 #
619 623 if getattr(action, "hash", "NOHASH") != "NOHASH":
620 624 path = action.hash
621 625 else:
622 626 path = action.attrs["path"]
623 627
624 628 #
625 629 # This is the wrong tool in which to enforce consistency
626 630 # on a set of manifests. So instead of comparing the
627 631 # different actions with the same "path" attribute, we
628 632 # use the first one.
629 633 #
630 634 if path in self:
631 635 continue
632 636
633 637 #
634 638 # As with the manifest itself, if an action has specified
635 639 # variant.arch, we look for the target architecture
636 640 # therein.
637 641 #
638 642 var = None
639 643
640 644 #
641 645 # The name of this method changed in pkg(5) build 150, we need to
642 646 # work with both sets.
643 647 #
644 648 if hasattr(action, 'get_variants'):
645 649 var = action.get_variants()
646 650 else:
647 651 var = action.get_variant_template()
648 652 if "variant.arch" in var and arch not in var["variant.arch"]:
649 653 return
650 654
651 655 self[path] = ActionInfo(action)
652 656 if modechecks is not None and path not in exceptions:
653 657 modewarnings.update(self[path].checkmodes(modechecks))
654 658
655 659 if len(modewarnings) > 0:
656 660 print "warning: unsafe permissions in %s" % mfile
657 661 for w in sorted(modewarnings):
658 662 print w
659 663 print ""
660 664
661 665 def adddir(self, mdir, arch, modechecks, exceptions):
662 666 """Walks the specified directory looking for pkg(5) manifests.
663 667 """
664 668 for mfile in os.listdir(mdir):
665 669 if (mfile.endswith(".mog") and
666 670 stat.S_ISREG(os.lstat(os.path.join(mdir, mfile)).st_mode)):
667 671 try:
668 672 self.addmanifest(mdir, mfile, arch, modechecks, exceptions)
669 673 except IOError, exc:
670 674 sys.stderr.write("warning: %s\n" % str(exc))
671 675
672 676 def resolvehardlinks(self):
673 677 """Populates mode, group, and owner for resolved (ie link target
674 678 is present in the manifest tree) hard links.
675 679 """
676 680 for info in self.values():
677 681 if info.name() == "hardlink":
678 682 tgt = info.hardkey
679 683 if tgt in self:
680 684 tgtinfo = self[tgt]
681 685 info.owner = tgtinfo.owner
682 686 info.group = tgtinfo.group
683 687 info.mode = tgtinfo.mode
684 688
685 689 class ExceptionList(set):
686 690 """Keep track of an exception list as a set of paths to be excluded
687 691 from any other lists we build.
688 692 """
689 693
690 694 def __init__(self, files, arch):
691 695 set.__init__(self)
692 696 for fname in files:
693 697 try:
694 698 self.readexceptionfile(fname, arch)
695 699 except IOError, exc:
696 700 sys.stderr.write("warning: cannot read exception file: %s\n" %
697 701 str(exc))
698 702
699 703 def readexceptionfile(self, efile, arch):
700 704 """Build a list of all pathnames from the specified file that
701 705 either apply to all architectures (ie which have no trailing
702 706 architecture tokens), or to the specified architecture (ie
703 707 which have the value of the arch arg as a trailing
704 708 architecture token.)
705 709 """
706 710
707 711 excfile = open(efile)
708 712
709 713 for exc in excfile:
710 714 exc = exc.split()
711 715 if len(exc) and exc[0][0] != "#":
712 716 if arch in (exc[1:] or arch):
713 717 self.add(os.path.normpath(exc[0]))
714 718
715 719 excfile.close()
716 720
717 721
718 722 USAGE = """%s [-v] -a arch [-e exceptionfile]... [-L|-M [-X check]...] input_1 [input_2]
719 723
720 724 where input_1 and input_2 may specify proto lists, proto areas,
721 725 or manifest directories. For proto lists, use one or more
722 726
723 727 -l file
724 728
725 729 arguments. For proto areas, use one or more
726 730
727 731 -p dir
728 732
729 733 arguments. For manifest directories, use one or more
730 734
731 735 -m dir
732 736
733 737 arguments.
734 738
735 739 If -L or -M is specified, then only one input source is allowed, and
736 740 it should be one or more manifest directories. These two options are
737 741 mutually exclusive.
738 742
739 743 The -L option is used to generate a proto list to stdout.
740 744
741 745 The -M option is used to check for safe file and directory modes.
742 746 By default, this causes all mode checks to be performed. Individual
743 747 mode checks may be turned off using "-X check," where "check" comes
744 748 from the following set of checks:
745 749
746 750 m check for group or other write permissions
747 751 w check for user write permissions on files and directories
748 752 not owned by root
749 753 s check for group/other read permission on executable files
750 754 that have setuid/setgid bit(s)
751 755 o check for files that could be safely owned by root
752 756 """ % sys.argv[0]
753 757
754 758
755 759 def usage(msg=None):
756 760 """Try to give the user useful information when they don't get the
757 761 command syntax right.
758 762 """
759 763 if msg:
760 764 sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
761 765 sys.stderr.write(USAGE)
762 766 sys.exit(2)
763 767
764 768
765 769 def main(argv):
766 770 """Compares two out of three possible data sources: a proto list, a
767 771 set of proto areas, and a set of manifests.
768 772 """
769 773 try:
770 774 opts, args = getopt.getopt(argv, 'a:e:Ll:Mm:p:vX:')
771 775 except getopt.GetoptError, exc:
772 776 usage(str(exc))
773 777
774 778 if args:
775 779 usage()
776 780
777 781 arch = None
778 782 exceptionlists = []
779 783 listonly = False
780 784 manifestdirs = []
781 785 manifesttree = ManifestTree("manifests")
782 786 protodirs = []
783 787 prototree = ProtoTree("proto area")
784 788 protolists = []
785 789 protolist = ProtoTree("proto list")
786 790 modechecks = set()
787 791 togglemodechecks = set()
788 792 trees = []
789 793 comparing = set()
790 794 verbose = False
791 795
792 796 for opt, arg in opts:
793 797 if opt == "-a":
794 798 if arch:
795 799 usage("may only specify one architecture")
796 800 else:
797 801 arch = arg
798 802 elif opt == "-e":
799 803 exceptionlists.append(arg)
800 804 elif opt == "-L":
801 805 listonly = True
802 806 elif opt == "-l":
803 807 comparing.add("protolist")
804 808 protolists.append(os.path.normpath(arg))
805 809 elif opt == "-M":
806 810 modechecks.update(DEFAULTMODECHECKS)
807 811 elif opt == "-m":
808 812 comparing.add("manifests")
809 813 manifestdirs.append(os.path.normpath(arg))
810 814 elif opt == "-p":
811 815 comparing.add("proto area")
812 816 protodirs.append(os.path.normpath(arg))
813 817 elif opt == "-v":
814 818 verbose = True
815 819 elif opt == "-X":
816 820 togglemodechecks.add(arg)
817 821
818 822 if listonly or len(modechecks) > 0:
819 823 if len(comparing) != 1 or "manifests" not in comparing:
820 824 usage("-L and -M require one or more -m args, and no -l or -p")
821 825 if listonly and len(modechecks) > 0:
822 826 usage("-L and -M are mutually exclusive")
823 827 elif len(comparing) != 2:
824 828 usage("must specify exactly two of -l, -m, and -p")
825 829
826 830 if len(togglemodechecks) > 0 and len(modechecks) == 0:
827 831 usage("-X requires -M")
828 832
829 833 for s in togglemodechecks:
830 834 if s not in ALLMODECHECKS:
831 835 usage("unknown mode check %s" % s)
832 836 modechecks.symmetric_difference_update((s))
833 837
834 838 if len(modechecks) == 0:
835 839 modechecks = None
836 840
837 841 if not arch:
838 842 usage("must specify architecture")
839 843
840 844 exceptions = ExceptionList(exceptionlists, arch)
841 845 originalexceptions = exceptions.copy()
842 846
843 847 if len(manifestdirs) > 0:
844 848 for mdir in manifestdirs:
845 849 manifesttree.adddir(mdir, arch, modechecks, exceptions)
846 850 if listonly:
847 851 manifesttree.resolvehardlinks()
848 852 for info in manifesttree.values():
849 853 print "%s" % info.protostr()
850 854 sys.exit(0)
851 855 if modechecks is not None:
852 856 sys.exit(0)
853 857 trees.append(manifesttree)
854 858
855 859 if len(protodirs) > 0:
856 860 for pdir in protodirs:
857 861 prototree.adddir(pdir, exceptions)
858 862 trees.append(prototree)
859 863
860 864 if len(protolists) > 0:
861 865 for plist in protolists:
862 866 try:
863 867 protolist.addprotolist(plist, exceptions)
864 868 except IOError, exc:
865 869 sys.stderr.write("warning: %s\n" % str(exc))
866 870 trees.append(protolist)
867 871
868 872 if verbose and exceptions:
869 873 print "Entries present in exception list but missing from proto area:"
870 874 for exc in sorted(exceptions):
871 875 print "\t%s" % exc
872 876 print ""
873 877
874 878 usedexceptions = originalexceptions.difference(exceptions)
875 879 harmfulexceptions = usedexceptions.intersection(manifesttree)
876 880 if harmfulexceptions:
877 881 print "Entries present in exception list but also in manifests:"
878 882 for exc in sorted(harmfulexceptions):
879 883 print "\t%s" % exc
880 884 del manifesttree[exc]
881 885 print ""
882 886
883 887 trees[0].compare(trees[1])
884 888
885 889 if __name__ == '__main__':
886 890 try:
887 891 main(sys.argv[1:])
888 892 except KeyboardInterrupt:
889 893 sys.exit(1)
890 894 except IOError:
891 895 sys.exit(1)
↓ open down ↓ |
513 lines elided |
↑ open up ↑ |
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX