usr/src/cmd/aimanifest/aimanifest.py
author Jack Schwartz <Jack.Schwartz@Oracle.COM>
Thu, 21 Apr 2011 12:16:26 -0700
changeset 1087 96b6cc8130c5
child 1309 a58fac510fb6
permissions -rw-r--r--
6994260 Implement Auto-Installer Derived Manifests 7033335 If root is the first user of InstallLogger, only root can use 7036438 update-manifest command needed 15491 installadm needs to list default custom manifests

#!/usr/bin/python2.6
#
# 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) 2011, Oracle and/or its affiliates. All rights reserved.
#

"""
aimanifest.py: aimanifest commandline wrapper around Manifest Input Module
"""

import errno
import gettext
import logging
import os
import sys

from optparse import OptionParser
from traceback import print_exc

import solaris_install.manifest_input as milib

from solaris_install.logger import InstallLogger
from solaris_install.manifest_input.mim import ManifestInput

_ = gettext.translation('aimanifest', '/usr/share/locale',
                        fallback=True).gettext

AIM_LOGGER = None
SCHEMA_FILE = "/usr/share/install/ai.dtd"

VALIDATE = True
NO_VALIDATE = False


class AimOptionParser(OptionParser):
    '''
    OptionParser which provides error_w_errno() to return a more correct errno
    '''

    def error_w_errno(self, errnum, message):
        '''
        Log and display a message, and exit with errnum status
        '''
        AIM_LOGGER.error(message)
        print >> sys.stderr, _("Usage:\n") + self.usage + "\n"
        print >> sys.stderr, os.path.basename(sys.argv[0]) + \
                                             ": error: " + message
        sys.exit(errnum)


def usage(argv):
    '''
    Assemble command usage string.
    '''

    name = os.path.basename(argv[0])
    usage_str = _(
        "    %s subcommand cmd_options\n" +
        "    subcommands:\n" +
        "    " + name + " set [-r] <path> <value>  " +
                  "Change value or add/change attributes\n" +
        "    " + name + " add [-r] <path> <value>  Add new element\n" +
        "    " + name + " get [-r] <path>          " +
                  "Retrieve element value or attributes\n" +
        "    " + name + " load [-i] <filename>     " +
                  "Load / incrementally overlay XML file\n" +
        "    " + name + " validate                 Validate XML data\n" +
        "\n    The -r option to set/add/get displays the path of " +
                  "the returned element\n" +
        "    in terms of node IDs.  This path may be used in " +
                  "subsequent calls to\n" +
        "    %s to specify the affected element more directly.\n") % (
        name, name)
    return usage_str


def _handle_error(error):
    '''
    Display the error message, log the whole traceback and set errno

    Args:
      error: exception containing message and, if IOError, an errno.
              Assume messages are localized.

    Returns:
      errno: EINVAL unless overridden by an IOError exception's errno.
    '''
    print >> sys.stderr, str(error)
    AIM_LOGGER.exception(str(error))
    rval = getattr(error, "errno", errno.EINVAL)

    # Handle the cases where the errno field exists in the exception
    # but is None or 0
    if not rval:
        rval = errno.EIO if isinstance(error, IOError) else errno.EINVAL
    return rval


def _log_final_status(mim, path=None):
    '''
    Log status.  Usually called after running a subcommand.  Checks to see if
    the manifest validates, when this information would be logged.  Does
    nothing if logging is disabled.

    Args:
      mim: Reference to the Manifest Input Module, needed for validation.

      path: Nodepath, used for printing only.
    '''
    if AIM_LOGGER.isEnabledFor(logging.INFO):
        try:
            mim.validate()
            validated = _("Pass")
        except (milib.MimError, IOError):
            validated = _("Fail")
        out = _("cmd:success, validation:") + validated
        if path:
            out += _(", node:") + path
        AIM_LOGGER.info(out)


def _setup_logging():
    '''
    Set up logging.

    If AIM_LOGFILE is set in the environment, enable logging to the file
    specified by AIM_LOGFILE.  Enable logging at the level specified by
    the environment variable AIM_LOGLEVEL if set, else set to the INFO level
    by default.  (These two variables (and logging here in general) is to
    support the Derived Manifest Module (DMM).
    '''
    # pylint: disable-msg=W0603
    global AIM_LOGGER

    logging.setLoggerClass(InstallLogger)
    AIM_LOGGER = logging.getLogger("aimanifest")
    logfile_name = os.environ.get("AIM_LOGFILE")
    if logfile_name is not None:
        logging.basicConfig(format="%(asctime)s: %(name)s: "
                                   "%(levelname)s: %(message)s",
                            datefmt="%H:%M:%S",
                            filename=logfile_name,
                            filemode="a")
        logging_level = os.environ.get("AIM_LOGLEVEL")
        if logging_level and logging_level.isdigit():
            AIM_LOGGER.setLevel(int(logging_level))
        else:
            AIM_LOGGER.setLevel(logging.INFO)
    else:
        AIM_LOGGER.setLevel(logging.NOTSET)


def _shutdown_logging():
    '''
    Shut down logging.
    '''
    logging.shutdown()


def _do_aimanifest(argv):
    '''
    Main.  See usage for argv details.
    '''

    usage_str = usage(argv)

    if len(argv) <= 1:
        AIM_LOGGER.error(_("Error: Missing subcommand"))
        print >> sys.stderr, _("Usage:\n") + usage_str
        return errno.EINVAL

    parser = AimOptionParser(usage=usage_str)
    parser.add_option("-i", "--incremental", dest="is_incremental",
                      default=False, action="store_true",
                      help=_("Do not clear data before adding new data"))
    parser.add_option("-r", dest="show_path", default=False,
                      action="store_true",
                      help=_("Return unique path to affected node"))

    (options, args) = parser.parse_args(argv[1:])
    len_args = len(args)
    command = args[0]
    path = args[1] if (len_args > 1) else None
    value = args[2] if (len_args > 2) else None

    cmds_r_option = ["add", "set", "get"]
    cmds_w_value_arg = ["add", "set"]
    cmds_wo_value_arg = ["get", "load"]
    cmds_w_no_args = ["validate"]

    if ((command in cmds_w_value_arg and (len_args < 3)) or
        (command in cmds_wo_value_arg and (len_args < 2))):
        parser.error_w_errno(errno.EINVAL, _("missing argument"))
    if ((command in cmds_w_value_arg and (len_args > 3)) or
        (command in cmds_wo_value_arg and (len_args > 2)) or
        (command in cmds_w_no_args and (len_args > 1))):
        parser.error_w_errno(errno.EINVAL, _("extra arguments given"))
    if (command != "load") and options.is_incremental:
        parser.error_w_errno(errno.EINVAL,
                     _("-i is not applicable for command given"))
    if command not in cmds_r_option and options.show_path:
        parser.error_w_errno(errno.EINVAL,
                     _("-r is not applicable for command given"))

    # Pass AIM_MANIFEST as the output file.
    try:
        mim = ManifestInput(os.environ.get("AIM_MANIFEST"), SCHEMA_FILE)
    except (milib.MimError, IOError) as err:
        return (_handle_error(err))

    # Note: warnings already localized.
    warnings = mim.get_warnings()
    if warnings is not None:
        for msg in warnings:
            print >> sys.stderr, msg
            AIM_LOGGER.error(msg)

    if (command == "set") or (command == "add"):
        AIM_LOGGER.info(_("command:%(mcommand)s, path:%(mpath)s, "
                "value:%(mvalue)s") %
                {"mcommand": command, "mpath": path, "mvalue": value})
        try:
            if command == "set":
                path = mim.set(path, value)
            else:
                path = mim.add(path, value)
            mim.commit(NO_VALIDATE)
        except (milib.MimError, IOError) as err:
            return (_handle_error(err))

        _log_final_status(mim, path)
        if options.show_path:
            # Localization not needed here.
            print path

    elif command == "get":
        AIM_LOGGER.info(_("command:%(mcommand)s, path:%(mpath)s") %
                {"mcommand": command, "mpath": path})
        try:
            (value, path) = mim.get(path)
        except (milib.MimError, IOError) as err:
            return (_handle_error(err))

        if value is None:
            value = "\"\""

        AIM_LOGGER.info(_("successful: returns value:%(mvalue)s, "
                      "path:%(mpath)s") % {"mvalue": value, "mpath": path})

        if not options.show_path:
            path = ""

        # Localization not needed here.
        print "%s %s" % (value, path)

    elif (command == "load"):
        # path arg holds the filename
        AIM_LOGGER.info(_("command:%(mcommand)s, "
                      "incremental:%(mincr)s, file:%(mfile)s") %
                    {"mcommand": command, "mincr": str(options.is_incremental),
                     "mfile": path})
        try:
            mim.load(path, options.is_incremental)
            mim.commit(NO_VALIDATE)
        except (milib.MimError, IOError) as err:
            return (_handle_error(err))

        # Note: warnings already localized.
        warnings = mim.get_warnings()
        if warnings is not None:
            for msg in warnings:
                print >> sys.stderr, msg
                AIM_LOGGER.error(msg)

        _log_final_status(mim, path)

    elif (command == "validate"):
        AIM_LOGGER.info(_("Command:%s") % command)
        try:
            mim.validate()
            AIM_LOGGER.info(_("Validation successful"))
        except milib.MimDTDInvalid as err:
            errorlist = err.errors
            for error in errorlist:
                # These messages come already localized.
                print >> sys.stderr, error
                AIM_LOGGER.error(error)
            return errno.EINVAL
        except (milib.MimError, IOError) as err:
            return (_handle_error(err))

    else:
        AIM_LOGGER.error(_("Invalid subcommand \"%s\"") % command)
        print >> sys.stderr, _("Usage:\n") + usage_str
        return errno.EINVAL

    return 0  # No errors


def main(argv):
    '''
    Main program.
    '''
    _setup_logging()
    rval = 1
    try:
        rval = _do_aimanifest(argv)
    except StandardError as err:
        # Catch unexpected exceptions, logging and displaying a traceback.
        print >> sys.stderr, str(err)
        AIM_LOGGER.exception(str(err))
        print_exc()
    _shutdown_logging()
    return rval


if __name__ == "__main__":
    sys.exit(main(sys.argv))