src/modules/lint/opensolaris.py
author Tim Foster <tim.s.foster@oracle.com>
Wed, 29 Sep 2010 11:55:47 +1300
changeset 2090 d84a7b3cafa3
parent 2085 10a27146ebc2
child 2184 48031491a616
child 2572 20cf41d565de
permissions -rw-r--r--
17061 pkglint -L output could be improved 17072 pkglint print_fmri check is insufficient for debugging 17086 pkglint -c has different behavior with relative and absolute paths

#!/usr/bin/python
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#

#
# Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
#

# Some opensolaris distribution specific lint checks

import pkg.lint.base as base
import re
import os.path
import ConfigParser

class OpenSolarisActionChecker(base.ActionChecker):
        """An opensolaris.org-specific class to check actions."""

        name = "opensolaris.action"

        def __init__(self, config):
                self.description = _(
                    "checks OpenSolaris packages for common action errors")
                super(OpenSolarisActionChecker, self).__init__(config)

        def username_format(self, action, manifest, engine, pkglint_id="001"):
                """Checks username length, and format."""

                if action.name is not "user":
                        return

                username = action.attrs["username"]
                if len(username) > 8:
                        engine.error(
                            _("Username %(name)s in %(pkg)s > 8 chars") %
                            {"name": username,
                            "pkg": manifest.fmri},
                            msgid="%s%s.1" % (self.name, pkglint_id))

                if len(username) == 0 or not re.match("[a-z]", username[0]):
                        engine.error(
                            _("Username %(name)s in %(pkg)s does not have an "
                            "initial lower-case alphabetical character") %
                            {"name": username,
                            "pkg": manifest.fmri},
                            msgid="%s%s.2" % (self.name, pkglint_id))

                if not re.match("^[a-z]([a-zA-Z1-9._-])*$", username):
                        engine.error(
                            _("Username %(name)s in %(pkg)s is invalid - see "
                            "passwd(4)") %
                            {"name": username,
                            "pkg": manifest.fmri},
                            msgid="%s%s.3" % (self.name, pkglint_id))

        username_format.pkglint_desc = _("User names should be valid.")


class OpenSolarisManifestChecker(base.ManifestChecker):
        """An opensolaris.org-specific class to check manifests."""

        name = "opensolaris.manifest"

        def __init__(self, config):
                self.description = _(
                    "checks OpenSolaris packages for common errors")
                self.classification_data = None

                self.classification_path = config.get(
                    "pkglint", "info_classification_path")
                self.skip_classification_check = False

                # a default error message used if we've parsed the
                # data file, but haven't thrown any exceptions
                self.bad_classification_data = _("no sections found in data "
                    "file %s") % self.classification_path

                if os.path.exists(self.classification_path):
                        try:
                                self.classification_data = \
                                    ConfigParser.SafeConfigParser()
                                self.classification_data.readfp(
                                    open(self.classification_path))
                        except Exception, err:
                                # any exception thrown here results in a null
                                # classification_data object.  We deal with that
                                # later.
                                self.bad_classification_data = _(
                                    "unable to parse data file %(path)s: "
                                    "%(err)s") % \
                                    {"path": self.classification_path,
                                    "err": err}
                                pass
                else:
                        self.bad_classification_data = _("missing file %s") % \
                            self.classification_path

                super(OpenSolarisManifestChecker, self).__init__(config)

        def missing_attrs(self, manifest, engine, pkglint_id="001"):
                """Various checks for missing attributes
                * warn when a package doesn't have a pkg.description
                * error when a package doesn't have a pkg.summary
                * warn when a package doesn't have an org.opensolaris.consolidation
                * warn when a package doesn't have an info.classification'
                """
                if "pkg.renamed" in manifest:
                        return

                if "pkg.obsolete" in manifest:
                        return

                for key in ["pkg.description", "org.opensolaris.consolidation",
                    "info.classification"]:
                        if key not in manifest:
                                engine.warning(
                                    _("Missing key %(key)s from %(pkg)s") %
                                    {"key": key,
                                    "pkg": manifest.fmri},
                                    msgid="%s%s.1" % (self.name, pkglint_id))

                if "pkg.summary" not in manifest:
                        engine.error(_("Missing key pkg.summary from %s") %
                            manifest.fmri,
                            msgid="%s%s.2" % (self.name, pkglint_id))

        missing_attrs.pkglint_desc = _(
            "Standard package attributes should be present.")

        def info_classification(self, manifest, engine, pkglint_id="003"):
                """Checks that the info.classification attribute is valid."""

                if (not "info.classification" in manifest) or \
                    self.skip_classification_check:
                        return

                if not self.classification_data or \
                    not self.classification_data.sections():
                        engine.error(_("Unable to perform manifest checks "
                            "for info.classification attribute: %s") %
                            self.bad_classification_data,
                            msgid="%s%s.1" % (self.name, pkglint_id))
                        self.skip_classification_check = True
                        return

                value = manifest["info.classification"]

                # we allow multiple values for info.classification
                if isinstance(value, list):
                        for item in value:
                                self._check_info_classification_value(
                                    engine, item, manifest.fmri,
                                    "%s%s" % (self.name, pkglint_id))
                else:
                        self._check_info_classification_value(engine, value,
                            manifest.fmri, "%s%s" % (self.name, pkglint_id))

        info_classification.pkglint_desc = _(
            "info.classification attribute should be valid.")

        def _check_info_classification_value(self, engine, value, fmri, msgid):

                prefix = "org.opensolaris.category.2008:"

                if not prefix in value:
                        engine.error(_("info.classification attribute "
                            "does not contain '%(prefix)s' for %(fmri)s") %
                            locals(), msgid="%s.2" % msgid)
                        return

                classification = value.replace(prefix, "")

                components = classification.split("/", 1)
                if len(components) != 2:
                        engine.error(_("info.classification value %(value)s "
                            "does not match "
                            "%(prefix)s<Section>/<Category> for %(fmri)s") %
                            locals(), msgid="%s.3" % msgid)
                        return

                # the data file looks like:
                # [Section]
                # categtory = Cat1,Cat2,Cat3
                #
                # We expect the info.classification action to look like:
                # org.opensolaris.category.2008:Section/Cat2
                #
                section, category = components
                valid_value = True
                ref_categories = []
                try:
                        ref_categories = self.classification_data.get(section,
                            "category").split(",")
                        if category not in ref_categories:
                                valid_value = False
                except ConfigParser.NoSectionError:
                        engine.error(_("info.classification value %(value)s "
                            "does not contain one of the valid sections "
                            "%(ref_sections)s for %(fmri)s.") %
                            {"value": value,
                            "ref_sections":
                            ", ".join(self.classification_data.sections()),
                            "fmri": fmri},
                            msgid="%s.4" % msgid)
                        return

                except ConfigParser.NoOptionError:
                        engine.error(_("Invalid info.classification value for "
                            "%(fmri)s: data file %(file)s does not have a "
                            "'category' key for section %(section)s.") %
                            {"file": self.classification_path,
                            "section": section,
                            "fmri": fmri},
                             msgid="%s.5" % msgid)
                        return

                if valid_value:
                        return

                ref_cats = self.classification_data.get(section,"category")
                engine.error(_("info.classification attribute in %(fmri)s "
                    "does not contain one of the values defined for the "
                    "section %(section)s: %(ref_cats)s from %(path)s") %
                    {"section": section,
                    "fmri": fmri,
                    "path": self.classification_path,
                    "ref_cats": ref_cats },
                    msgid="%s.6" % msgid)