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