Print this page
5166 sendmail package should be replaceable
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 + if "mediator" in action.attrs:
312 + self.mediator = action.attrs["mediator"]
313 + else:
314 + self.mediator = None
309 315 elif action.name == "dir":
310 316 self.owner = action.attrs["owner"]
311 317 self.group = action.attrs["group"]
312 318 self.mode = action.attrs["mode"]
313 319 self.isdir = True
314 320 elif action.name == "hardlink":
315 321 target = os.path.normpath(action.get_target_path())
316 322 self.hardkey = target
317 323 self.hardpaths.add(target)
318 324
319 325 @staticmethod
320 326 def supported(action):
321 327 """Indicates whether the specified IPS action time is
322 328 correctly handled by the ActionInfo constructor.
323 329 """
324 330 return action in frozenset(("file", "dir", "link", "hardlink"))
325 331
326 332
327 333 class UnsupportedFileFormatError(Exception):
328 334 """This means that the stat.S_IFMT returned something we don't
329 335 support, ie a pipe or socket. If it's appropriate for such an
330 336 object to be in the proto area, then the RealFileInfo constructor
331 337 will need to evolve to support it, or it will need to be in the
332 338 exception list.
333 339 """
334 340 def __init__(self, path, mode):
335 341 Exception.__init__(self)
336 342 self.path = path
337 343 self.mode = mode
338 344
339 345 def __str__(self):
340 346 return '%s: unsupported S_IFMT %07o' % (self.path, self.mode)
341 347
342 348
343 349 class RealFileInfo(FileInfo):
344 350 """Object to track important-to-packaging file information.
345 351
346 352 This currently handles regular files, directories, and symbolic links.
347 353
348 354 For multiple RealFileInfo objects with identical hardkeys, there
349 355 is no way to determine which of the hard links should be
350 356 delivered as a file, and which as hardlinks.
351 357 """
352 358
353 359 def __init__(self, root=None, path=None):
354 360 FileInfo.__init__(self)
355 361 self.path = path
356 362 path = os.path.join(root, path)
357 363 lstat = os.lstat(path)
358 364 mode = lstat.st_mode
↓ open down ↓ |
40 lines elided |
↑ open up ↑ |
359 365
360 366 #
361 367 # Per stat.py, these cases are mutually exclusive.
362 368 #
363 369 if stat.S_ISREG(mode):
364 370 self.hash = self.path
365 371 elif stat.S_ISDIR(mode):
366 372 self.isdir = True
367 373 elif stat.S_ISLNK(mode):
368 374 self.target = os.path.normpath(os.readlink(path))
375 + self.mediator = None
369 376 else:
370 377 raise UnsupportedFileFormatError(path, mode)
371 378
372 379 if not stat.S_ISLNK(mode):
373 380 self.mode = "%04o" % stat.S_IMODE(mode)
374 381 #
375 382 # Instead of reading the group and owner from the proto area after
376 383 # a non-root build, just drop in dummy values. Since we don't
377 384 # compare them anywhere, this should allow at least marginally
378 385 # useful comparisons of protolist-style output.
379 386 #
380 387 self.owner = "owner"
381 388 self.group = "group"
382 389
383 390 #
384 391 # refcount > 1 indicates a hard link
385 392 #
386 393 if lstat.st_nlink > 1:
387 394 #
388 395 # This could get ugly if multiple proto areas reside
389 396 # on different filesystems.
390 397 #
391 398 self.hardkey = lstat.st_ino
392 399
393 400
394 401 class DirectoryTree(dict):
395 402 """Meant to be subclassed according to population method.
396 403 """
397 404 def __init__(self, name):
398 405 dict.__init__(self)
399 406 self.name = name
400 407
401 408 def compare(self, other):
402 409 """Compare two different sets of FileInfo objects.
403 410 """
404 411 keys1 = frozenset(self.keys())
405 412 keys2 = frozenset(other.keys())
406 413
407 414 common = keys1.intersection(keys2)
408 415 onlykeys1 = keys1.difference(common)
409 416 onlykeys2 = keys2.difference(common)
410 417
411 418 if onlykeys1:
412 419 print "Entries present in %s but not %s:" % \
413 420 (self.name, other.name)
414 421 for path in sorted(onlykeys1):
415 422 print("\t%s" % str(self[path]))
416 423 print ""
417 424
418 425 if onlykeys2:
419 426 print "Entries present in %s but not %s:" % \
420 427 (other.name, self.name)
421 428 for path in sorted(onlykeys2):
422 429 print("\t%s" % str(other[path]))
423 430 print ""
424 431
425 432 nodifferences = True
426 433 for path in sorted(common):
427 434 if self[path] != other[path]:
428 435 if nodifferences:
429 436 nodifferences = False
430 437 print "Entries that differ between %s and %s:" \
431 438 % (self.name, other.name)
432 439 print("%14s %s" % (self.name, self[path]))
433 440 print("%14s %s" % (other.name, other[path]))
434 441 if not nodifferences:
435 442 print ""
436 443
437 444
438 445 class BadProtolistFormat(Exception):
439 446 """This means that the user supplied a file via -l, but at least
440 447 one line from that file doesn't have the right number of fields to
441 448 parse as protolist output.
442 449 """
443 450 def __str__(self):
444 451 return 'bad proto list entry: "%s"' % Exception.__str__(self)
445 452
446 453
447 454 class ProtoTree(DirectoryTree):
448 455 """Describes one or more proto directories as a dictionary of
449 456 RealFileInfo objects, indexed by relative path.
450 457 """
451 458
452 459 def adddir(self, proto, exceptions):
453 460 """Extends the ProtoTree dictionary with RealFileInfo
454 461 objects describing the proto dir, indexed by relative
455 462 path.
456 463 """
457 464 newentries = {}
458 465
459 466 pdir = os.path.normpath(proto)
460 467 strippdir = lambda r, n: os.path.join(r, n)[len(pdir)+1:]
461 468 for root, dirs, files in os.walk(pdir):
462 469 for name in dirs + files:
463 470 path = strippdir(root, name)
464 471 if path not in exceptions:
465 472 try:
466 473 newentries[path] = RealFileInfo(pdir, path)
467 474 except OSError, e:
468 475 sys.stderr.write("Warning: unable to stat %s: %s\n" %
469 476 (path, e))
470 477 continue
471 478 else:
472 479 exceptions.remove(path)
473 480 if name in dirs:
474 481 dirs.remove(name)
475 482
476 483 #
477 484 # Find the sets of paths in this proto dir that are hardlinks
478 485 # to the same inode.
479 486 #
480 487 # It seems wasteful to store this in each FileInfo, but we
481 488 # otherwise need a linking mechanism. With this information
482 489 # here, FileInfo object comparison can be self contained.
483 490 #
484 491 # We limit this aggregation to a single proto dir, as
485 492 # represented by newentries. That means we don't need to care
486 493 # about proto dirs on separate filesystems, or about hardlinks
487 494 # that cross proto dir boundaries.
488 495 #
489 496 hk2path = {}
490 497 for path, fileinfo in newentries.iteritems():
491 498 if fileinfo.hardkey:
492 499 hk2path.setdefault(fileinfo.hardkey, set()).add(path)
493 500 for fileinfo in newentries.itervalues():
494 501 if fileinfo.hardkey:
495 502 fileinfo.hardpaths.update(hk2path[fileinfo.hardkey])
496 503 self.update(newentries)
497 504
498 505 def addprotolist(self, protolist, exceptions):
499 506 """Read in the specified file, assumed to be the
500 507 output of protolist.
501 508
502 509 This has been tested minimally, and is potentially useful for
503 510 comparing across the transition period, but should ultimately
504 511 go away.
505 512 """
506 513
507 514 try:
508 515 plist = open(protolist)
509 516 except IOError, exc:
510 517 raise IOError("cannot open proto list: %s" % str(exc))
511 518
512 519 newentries = {}
513 520
514 521 for pline in plist:
515 522 pline = pline.split()
516 523 #
517 524 # Use a FileInfo() object instead of a RealFileInfo()
518 525 # object because we want to avoid the RealFileInfo
519 526 # constructor, because there's nothing to actually stat().
520 527 #
521 528 fileinfo = FileInfo()
522 529 try:
523 530 if pline[1] in exceptions:
524 531 exceptions.remove(pline[1])
525 532 continue
526 533 if pline[0] == "d":
527 534 fileinfo.isdir = True
528 535 fileinfo.path = pline[1]
529 536 if pline[2] != "-":
530 537 fileinfo.target = os.path.normpath(pline[2])
531 538 fileinfo.mode = int("0%s" % pline[3])
532 539 fileinfo.owner = pline[4]
533 540 fileinfo.group = pline[5]
534 541 if pline[6] != "0":
535 542 fileinfo.hardkey = pline[6]
536 543 newentries[pline[1]] = fileinfo
537 544 except IndexError:
538 545 raise BadProtolistFormat(pline)
539 546
540 547 plist.close()
541 548 hk2path = {}
542 549 for path, fileinfo in newentries.iteritems():
543 550 if fileinfo.hardkey:
544 551 hk2path.setdefault(fileinfo.hardkey, set()).add(path)
545 552 for fileinfo in newentries.itervalues():
546 553 if fileinfo.hardkey:
547 554 fileinfo.hardpaths.update(hk2path[fileinfo.hardkey])
548 555 self.update(newentries)
549 556
550 557
551 558 class ManifestParsingError(Exception):
552 559 """This means that the Manifest.set_content() raised an
553 560 ActionError. We raise this, instead, to tell us which manifest
554 561 could not be parsed, rather than what action error we hit.
555 562 """
556 563 def __init__(self, mfile, error):
557 564 Exception.__init__(self)
558 565 self.mfile = mfile
559 566 self.error = error
560 567
561 568 def __str__(self):
562 569 return "unable to parse manifest %s: %s" % (self.mfile, self.error)
563 570
564 571
565 572 class ManifestTree(DirectoryTree):
566 573 """Describes one or more directories containing arbitrarily
567 574 many manifests as a dictionary of ActionInfo objects, indexed
568 575 by the relative path of the data source within the proto area.
569 576 That path may or may not be the same as the path attribute of the
570 577 given action.
571 578 """
572 579
573 580 def addmanifest(self, root, mfile, arch, modechecks, exceptions):
574 581 """Treats the specified input file as a pkg(5) package
575 582 manifest, and extends the ManifestTree dictionary with entries
576 583 for the actions therein.
577 584 """
578 585 mfest = manifest.Manifest()
579 586 try:
580 587 mfest.set_content(open(os.path.join(root, mfile)).read())
581 588 except IOError, exc:
582 589 raise IOError("cannot read manifest: %s" % str(exc))
583 590 except actions.ActionError, exc:
584 591 raise ManifestParsingError(mfile, str(exc))
585 592
586 593 #
587 594 # Make sure the manifest is applicable to the user-specified
588 595 # architecture. Assumption: if variant.arch is not an
589 596 # attribute of the manifest, then the package should be
590 597 # installed on all architectures.
591 598 #
592 599 if arch not in mfest.attributes.get("variant.arch", (arch,)):
593 600 return
594 601
595 602 modewarnings = set()
596 603 for action in mfest.gen_actions():
597 604 if "path" not in action.attrs or \
598 605 not ActionInfo.supported(action.name):
599 606 continue
600 607
601 608 #
602 609 # The dir action is currently fully specified, in that it
603 610 # lists owner, group, and mode attributes. If that
604 611 # changes in pkg(5) code, we'll need to revisit either this
605 612 # code or the ActionInfo() constructor. It's possible
606 613 # that the pkg(5) system could be extended to provide a
607 614 # mechanism for specifying directory permissions outside
608 615 # of the individual manifests that deliver files into
609 616 # those directories. Doing so at time of manifest
610 617 # processing would mean that validate_pkg continues to work,
611 618 # but doing so at time of publication would require updates.
612 619 #
613 620
614 621 #
615 622 # See pkgsend(1) for the use of NOHASH for objects with
616 623 # datastreams. Currently, that means "files," but this
617 624 # should work for any other such actions.
618 625 #
619 626 if getattr(action, "hash", "NOHASH") != "NOHASH":
620 627 path = action.hash
621 628 else:
622 629 path = action.attrs["path"]
623 630
624 631 #
625 632 # This is the wrong tool in which to enforce consistency
626 633 # on a set of manifests. So instead of comparing the
627 634 # different actions with the same "path" attribute, we
628 635 # use the first one.
629 636 #
630 637 if path in self:
631 638 continue
632 639
633 640 #
634 641 # As with the manifest itself, if an action has specified
635 642 # variant.arch, we look for the target architecture
636 643 # therein.
637 644 #
638 645 var = None
639 646
640 647 #
641 648 # The name of this method changed in pkg(5) build 150, we need to
642 649 # work with both sets.
643 650 #
644 651 if hasattr(action, 'get_variants'):
645 652 var = action.get_variants()
646 653 else:
647 654 var = action.get_variant_template()
648 655 if "variant.arch" in var and arch not in var["variant.arch"]:
649 656 return
650 657
651 658 self[path] = ActionInfo(action)
652 659 if modechecks is not None and path not in exceptions:
653 660 modewarnings.update(self[path].checkmodes(modechecks))
654 661
655 662 if len(modewarnings) > 0:
656 663 print "warning: unsafe permissions in %s" % mfile
657 664 for w in sorted(modewarnings):
658 665 print w
659 666 print ""
660 667
661 668 def adddir(self, mdir, arch, modechecks, exceptions):
662 669 """Walks the specified directory looking for pkg(5) manifests.
663 670 """
664 671 for mfile in os.listdir(mdir):
665 672 if (mfile.endswith(".mog") and
666 673 stat.S_ISREG(os.lstat(os.path.join(mdir, mfile)).st_mode)):
667 674 try:
668 675 self.addmanifest(mdir, mfile, arch, modechecks, exceptions)
669 676 except IOError, exc:
670 677 sys.stderr.write("warning: %s\n" % str(exc))
671 678
672 679 def resolvehardlinks(self):
673 680 """Populates mode, group, and owner for resolved (ie link target
674 681 is present in the manifest tree) hard links.
675 682 """
676 683 for info in self.values():
677 684 if info.name() == "hardlink":
678 685 tgt = info.hardkey
679 686 if tgt in self:
680 687 tgtinfo = self[tgt]
681 688 info.owner = tgtinfo.owner
682 689 info.group = tgtinfo.group
683 690 info.mode = tgtinfo.mode
684 691
685 692 class ExceptionList(set):
686 693 """Keep track of an exception list as a set of paths to be excluded
687 694 from any other lists we build.
688 695 """
689 696
690 697 def __init__(self, files, arch):
691 698 set.__init__(self)
692 699 for fname in files:
693 700 try:
694 701 self.readexceptionfile(fname, arch)
695 702 except IOError, exc:
696 703 sys.stderr.write("warning: cannot read exception file: %s\n" %
697 704 str(exc))
698 705
699 706 def readexceptionfile(self, efile, arch):
700 707 """Build a list of all pathnames from the specified file that
701 708 either apply to all architectures (ie which have no trailing
702 709 architecture tokens), or to the specified architecture (ie
703 710 which have the value of the arch arg as a trailing
704 711 architecture token.)
705 712 """
706 713
707 714 excfile = open(efile)
708 715
709 716 for exc in excfile:
710 717 exc = exc.split()
711 718 if len(exc) and exc[0][0] != "#":
712 719 if arch in (exc[1:] or arch):
713 720 self.add(os.path.normpath(exc[0]))
714 721
715 722 excfile.close()
716 723
717 724
718 725 USAGE = """%s [-v] -a arch [-e exceptionfile]... [-L|-M [-X check]...] input_1 [input_2]
719 726
720 727 where input_1 and input_2 may specify proto lists, proto areas,
721 728 or manifest directories. For proto lists, use one or more
722 729
723 730 -l file
724 731
725 732 arguments. For proto areas, use one or more
726 733
727 734 -p dir
728 735
729 736 arguments. For manifest directories, use one or more
730 737
731 738 -m dir
732 739
733 740 arguments.
734 741
735 742 If -L or -M is specified, then only one input source is allowed, and
736 743 it should be one or more manifest directories. These two options are
737 744 mutually exclusive.
738 745
739 746 The -L option is used to generate a proto list to stdout.
740 747
741 748 The -M option is used to check for safe file and directory modes.
742 749 By default, this causes all mode checks to be performed. Individual
743 750 mode checks may be turned off using "-X check," where "check" comes
744 751 from the following set of checks:
745 752
746 753 m check for group or other write permissions
747 754 w check for user write permissions on files and directories
748 755 not owned by root
749 756 s check for group/other read permission on executable files
750 757 that have setuid/setgid bit(s)
751 758 o check for files that could be safely owned by root
752 759 """ % sys.argv[0]
753 760
754 761
755 762 def usage(msg=None):
756 763 """Try to give the user useful information when they don't get the
757 764 command syntax right.
758 765 """
759 766 if msg:
760 767 sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
761 768 sys.stderr.write(USAGE)
762 769 sys.exit(2)
763 770
764 771
765 772 def main(argv):
766 773 """Compares two out of three possible data sources: a proto list, a
767 774 set of proto areas, and a set of manifests.
768 775 """
769 776 try:
770 777 opts, args = getopt.getopt(argv, 'a:e:Ll:Mm:p:vX:')
771 778 except getopt.GetoptError, exc:
772 779 usage(str(exc))
773 780
774 781 if args:
775 782 usage()
776 783
777 784 arch = None
778 785 exceptionlists = []
779 786 listonly = False
780 787 manifestdirs = []
781 788 manifesttree = ManifestTree("manifests")
782 789 protodirs = []
783 790 prototree = ProtoTree("proto area")
784 791 protolists = []
785 792 protolist = ProtoTree("proto list")
786 793 modechecks = set()
787 794 togglemodechecks = set()
788 795 trees = []
789 796 comparing = set()
790 797 verbose = False
791 798
792 799 for opt, arg in opts:
793 800 if opt == "-a":
794 801 if arch:
795 802 usage("may only specify one architecture")
796 803 else:
797 804 arch = arg
798 805 elif opt == "-e":
799 806 exceptionlists.append(arg)
800 807 elif opt == "-L":
801 808 listonly = True
802 809 elif opt == "-l":
803 810 comparing.add("protolist")
804 811 protolists.append(os.path.normpath(arg))
805 812 elif opt == "-M":
806 813 modechecks.update(DEFAULTMODECHECKS)
807 814 elif opt == "-m":
808 815 comparing.add("manifests")
809 816 manifestdirs.append(os.path.normpath(arg))
810 817 elif opt == "-p":
811 818 comparing.add("proto area")
812 819 protodirs.append(os.path.normpath(arg))
813 820 elif opt == "-v":
814 821 verbose = True
815 822 elif opt == "-X":
816 823 togglemodechecks.add(arg)
817 824
818 825 if listonly or len(modechecks) > 0:
819 826 if len(comparing) != 1 or "manifests" not in comparing:
820 827 usage("-L and -M require one or more -m args, and no -l or -p")
821 828 if listonly and len(modechecks) > 0:
822 829 usage("-L and -M are mutually exclusive")
823 830 elif len(comparing) != 2:
824 831 usage("must specify exactly two of -l, -m, and -p")
825 832
826 833 if len(togglemodechecks) > 0 and len(modechecks) == 0:
827 834 usage("-X requires -M")
828 835
829 836 for s in togglemodechecks:
830 837 if s not in ALLMODECHECKS:
831 838 usage("unknown mode check %s" % s)
832 839 modechecks.symmetric_difference_update((s))
833 840
834 841 if len(modechecks) == 0:
835 842 modechecks = None
836 843
837 844 if not arch:
838 845 usage("must specify architecture")
839 846
840 847 exceptions = ExceptionList(exceptionlists, arch)
841 848 originalexceptions = exceptions.copy()
842 849
843 850 if len(manifestdirs) > 0:
844 851 for mdir in manifestdirs:
845 852 manifesttree.adddir(mdir, arch, modechecks, exceptions)
846 853 if listonly:
847 854 manifesttree.resolvehardlinks()
848 855 for info in manifesttree.values():
849 856 print "%s" % info.protostr()
850 857 sys.exit(0)
851 858 if modechecks is not None:
852 859 sys.exit(0)
853 860 trees.append(manifesttree)
854 861
855 862 if len(protodirs) > 0:
856 863 for pdir in protodirs:
857 864 prototree.adddir(pdir, exceptions)
858 865 trees.append(prototree)
859 866
860 867 if len(protolists) > 0:
861 868 for plist in protolists:
862 869 try:
863 870 protolist.addprotolist(plist, exceptions)
864 871 except IOError, exc:
865 872 sys.stderr.write("warning: %s\n" % str(exc))
866 873 trees.append(protolist)
867 874
868 875 if verbose and exceptions:
869 876 print "Entries present in exception list but missing from proto area:"
870 877 for exc in sorted(exceptions):
871 878 print "\t%s" % exc
872 879 print ""
873 880
874 881 usedexceptions = originalexceptions.difference(exceptions)
875 882 harmfulexceptions = usedexceptions.intersection(manifesttree)
876 883 if harmfulexceptions:
877 884 print "Entries present in exception list but also in manifests:"
878 885 for exc in sorted(harmfulexceptions):
879 886 print "\t%s" % exc
880 887 del manifesttree[exc]
881 888 print ""
882 889
883 890 trees[0].compare(trees[1])
884 891
885 892 if __name__ == '__main__':
886 893 try:
887 894 main(sys.argv[1:])
888 895 except KeyboardInterrupt:
889 896 sys.exit(1)
890 897 except IOError:
891 898 sys.exit(1)
↓ open down ↓ |
513 lines elided |
↑ open up ↑ |
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX