usr/src/cmd/auto-install/auto_install.py
author Ethan Quach <Ethan.Quach@sun.com>
Tue, 31 May 2011 14:21:09 -0700
changeset 1160 6f7e708c38ec
parent 1154 8cb36a37e75d
child 1162 a4dac96d1eaa
permissions -rw-r--r--
16257 Support for zones configuration and installation should be included in AI 7041915 TransferFiles ICT should support transferring a directory that is more than one level deep. 7049824 System installed via AI ends up with incorrect mountpoints for shared ZFS datasets

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

"""AutoInstall main class and progress logging support."""

import linecache
import logging
import optparse
import os
import os.path
import random
import socket
import struct
import sys
import thread
import time
import traceback

import osol_install.errsvc as errsvc

from osol_install.liberrsvc import ES_DATA_EXCEPTION

from solaris_install import \
    ApplicationData, system_temp_path, post_install_logs_path, Popen
from solaris_install.auto_install import TRANSFER_FILES_CHECKPOINT
from solaris_install.auto_install.ai_instance import AIInstance
from solaris_install.auto_install.checkpoints.dmm import \
    DERIVED_MANIFEST_DATA, DerivedManifestData
from solaris_install.auto_install.checkpoints.target_selection import \
    SelectionError, TargetSelection
from solaris_install.auto_install.checkpoints.target_selection_zone import \
    TargetSelectionZone
from solaris_install.auto_install.checkpoints.ai_configuration import \
    AI_SERVICE_LIST_FILE, AIConfigurationError
from solaris_install.auto_install.utmpx import users_on_console
from solaris_install.boot import boot
from solaris_install.data_object import ParsingError, \
    DataObject, ObjectNotFoundError
from solaris_install.data_object.data_dict import DataObjectDict
from solaris_install.engine import InstallEngine
from solaris_install.engine import UnknownChkptError, UsageError, \
    RollbackError
from solaris_install.ict import initialize_smf, update_dumpadm, ips, \
    device_config, apply_sysconfig, boot_archive, transfer_files, \
    create_snapshot, setup_swap
from solaris_install.ict.apply_sysconfig import APPLY_SYSCONFIG_DICT, \
    APPLY_SYSCONFIG_PROFILE_KEY
from solaris_install.logger import FileHandler, ProgressHandler, MAX_INT
from solaris_install.logger import INSTALL_LOGGER_NAME
from solaris_install.manifest.parser import ManifestError, \
    MANIFEST_PARSER_DATA
from solaris_install.target import Target, discovery, instantiation
from solaris_install.target.instantiation_zone import ALT_POOL_DATASET
from solaris_install.target.logical import BE, Logical
from solaris_install.transfer import create_checkpoint
from solaris_install.transfer.info import Software, Destination, Image, \
    ImType, Dir, INSTALL, IPSSpec, CPIOSpec, SVR4Spec
from solaris_install.transfer.ips import AbstractIPS

ZPOOL = "/usr/sbin/zpool"

class AutoInstall(object):
    """
    AutoInstall master class
    """

    BE_LOG_DIR = post_install_logs_path("")
    INSTALL_LOG = "install_log"
    AI_EXIT_SUCCESS = 0
    AI_EXIT_FAILURE = 1
    AI_EXIT_AUTO_REBOOT = 64
    TARGET_INSTANTIATION_CHECKPOINT = 'target-instantiation'
    FIRST_TRANSFER_CHECKPOINT = 'first-transfer'
    MANIFEST_CHECKPOINTS = ["derived-manifest", "manifest-parser"]
    CHECKPOINTS_BEFORE_TI = ["target-discovery", "target-selection", \
        "ai-configuration", TARGET_INSTANTIATION_CHECKPOINT]
    CHECKPOINTS_BEFORE_TI.extend(MANIFEST_CHECKPOINTS)
    CHECKPOINTS_BEFORE_IPS = list(CHECKPOINTS_BEFORE_TI)
    INSTALLED_ROOT_DIR = "/a"

    def __init__(self, args=None):
        """
        Class constructor
        """
        self.installed_root_dir = self.INSTALLED_ROOT_DIR
        self.auto_reboot = False
        self.doc = None
        self.exitval = self.AI_EXIT_SUCCESS
        self.derived_script = None
        self.manifest = None

        # To remember the BE when we find it.
        self._be = None

        # Parse command line arguments
        self.options, self.args = self.parse_args(args)

        # Initialize Install Engine
        self.engine = InstallEngine(debug=True, stop_on_error=True)
        self.doc = self.engine.data_object_cache

        if self.options.zone_pool_dataset is not None:
            # If we're installing a zone root, generate a work_dir
            # location based on the current PID.
            work_dir = "/system/volatile/install." + str(os.getpid())

            # Add ApplicationData to the DOC
            self._app_data = ApplicationData("auto-install", work_dir=work_dir)
            self._app_data.data_dict[ALT_POOL_DATASET] = \
                self.options.zone_pool_dataset

            # Set installed_root_dir to be based off work_dir
            self.installed_root_dir = work_dir + self.INSTALLED_ROOT_DIR
        else:
            # Add ApplicationData to the DOC
            self._app_data = ApplicationData("auto-install")

        # Add profile location to the ApplySysconfig checkpoint's data dict.
        if self.options.profile is not None:
            # Try to find the ApplySysconfig data dict from
            # the DOC in case it already exists.
            as_doc_dict = None
            as_doc_dict = self.doc.volatile.get_first_child( \
                name=APPLY_SYSCONFIG_DICT)

            if as_doc_dict is None:
                # Initialize new dictionary in DOC
                as_dict = {APPLY_SYSCONFIG_PROFILE_KEY : self.options.profile}
                as_doc_dict = DataObjectDict(APPLY_SYSCONFIG_DICT, as_dict)
                self.doc.volatile.insert_children(as_doc_dict)
            else:
                # Add to existing dictionary in DOC
                as_doc_dict.data_dict[APPLY_SYSCONFIG_PROFILE_KEY] = \
                    self.options.profile

        # Add service list file to ApplicationData
        if self.options.service_list_file is not None:
            self._app_data.data_dict[AI_SERVICE_LIST_FILE] = \
                self.options.service_list_file

        self.doc.persistent.insert_children(self._app_data)

        # Clear error service
        errsvc.clear_error_list()

        # Create Logger and setup logfiles
        self.install_log_fh = None
        self.logger = None
        self.progress_ph = None
        self.setup_logs()

        if not self.options.list_checkpoints:
            self.logger.info("Starting Automated Installation Service")

        if self.options.stop_checkpoint:
            self.logger.debug("Pausing AI install before checkpoint: %s" % \
                (self.options.stop_checkpoint))

        if not self.options.list_checkpoints:
            if self.manifest:
                self.logger.info("Using XML Manifest: %s" % (self.manifest))

            if self.derived_script:
                self.logger.info("Using Derived Script: %s" % \
                    (self.derived_script))

            if self.options.profile:
                self.logger.info("Using profile specification: %s" % \
                    (self.options.profile))

            if self.options.service_list_file:
                self.logger.info("Using service list file: %s" % \
                    (self.options.service_list_file))

            if self.options.zone_pool_dataset:
                self.logger.info("Installing zone under dataset: %s" % \
                    (self.options.zone_pool_dataset))

            if self.options.dry_run:
                self.logger.info("Dry Run mode enabled")

    def parse_args(self, args):
        """
        Method to parse command line arguments
        """

        usage = "%prog -m <manifest> [-c <profile/dir>]\n"\
            "\t[-i - Stop installation before Target Instantiation |\n" + \
            "\t -I - Stop installation after Target Instantiation]\n" + \
            "\t[-n - Enable dry run mode]\n" + \
            "\t[-r <service_list_file>]" + \
            "\t[-Z <zone_pool_dataset]"

        parser = optparse.OptionParser(usage=usage)

        parser.add_option("-m", "--manifest", dest="manifest",
            help="Specify script or XML manifest to use")

        parser.add_option("-c", "--profile", dest="profile",
            help="Specify a profile or directory of profiles")

        parser.add_option("-i", "--break-before-ti", dest="break_before_ti",
            action="store_true", default=False,
            help="Break execution before Target Instantiation, testing only")

        parser.add_option("-I", "--break-after-ti", dest="break_after_ti",
            action="store_true", default=False,
            help="Break execution after Target Instantiation, testing only")

        parser.add_option("-n", "--dry-run", dest="dry_run",
            action="store_true", default=False,
            help="Enable dry-run mode for testing")

        parser.add_option("-l", "--list-checkpoints", dest="list_checkpoints",
            action="store_true", default=False,
            help=optparse.SUPPRESS_HELP)

        parser.add_option("-s", "--stop-checkpoint", dest="stop_checkpoint",
            help=optparse.SUPPRESS_HELP)

        parser.add_option("-r", "--service-list-file",
            dest="service_list_file", help="Specify service list file")

        parser.add_option("-Z", "--zone-pool-dataset",
            dest="zone_pool_dataset",
            help="Specify zone pool dataset to install into")

        (options, args) = parser.parse_args(args)

        # If manifest argument provided, determine if script or XML manifest
        if options.manifest:
            (self.derived_script, self.manifest) =  \
                self.determine_manifest_type(options.manifest)
            if not self.derived_script and not self.manifest:
                parser.error("Must specify manifest with -m option")

        # If specifying to list checkpoints, we can ignore all other semantics
        if not options.list_checkpoints:
            # Perform some parsing semantic validation
            # Must specify one of disk or manifest
            if options.manifest is None:
                parser.error("Must specify a manifest to use for installation")

        if options.break_before_ti and options.break_after_ti:
            parser.error("Cannot specify to stop installation before " + \
                "and after Target Installation")

        if (options.break_before_ti or options.break_after_ti) and \
            options.stop_checkpoint:
            parser.error("Cannot specify a stop checkpoint and to stop " + \
                "before/after target instantiation at same time")

        # Set stop_breakpoint to be before or after TI if requested
        if options.break_before_ti:
            options.stop_checkpoint = self.TARGET_INSTANTIATION_CHECKPOINT
        elif options.break_after_ti:
            options.stop_checkpoint = self.FIRST_TRANSFER_CHECKPOINT

        return (options, args)

    @staticmethod
    def determine_manifest_type(manifest):
        """
        Determine if manifest file argument is a script or xml manifest.
        Simply check reading first two characters of file for #!
        """
        derived_script = None
        xml_manifest = None

        if manifest is None:
            return None, None
        else:
            if linecache.getline(manifest, 1)[:2] == "#!":
                derived_script = manifest
            else:
                xml_manifest = manifest

        return derived_script, xml_manifest

    def validate_stop_checkpoint(self):
        """
        Validate stop checkpoint argument is valid by comparing
        against list of registered checkpoints.
        """
        cp_data_list = self.engine.get_exec_list()

        if len(cp_data_list) == 0:
            self.logger.debug("No Checkpoints have been registered to run")
        else:
            for cp in cp_data_list:
                if str(cp) == self.options.stop_checkpoint:
                    return True
        return False

    def list_checkpoints(self):
        """
        print to stdout the current list of checkpoints registered to run.
        """
        cp_data_list = self.engine.get_exec_list()

        if len(cp_data_list) == 0:
            print "No Checkpoints have been registered to run."
        else:
            print "Checkpoints will be run in the following order:"
            if self.derived_script:
                print "	%s" % (str(self.MANIFEST_CHECKPOINTS[0]))
                print "	%s" % (str(self.MANIFEST_CHECKPOINTS[1]))
            elif self.manifest:
                print "	%s" % (str(self.MANIFEST_CHECKPOINTS[1]))
            for cp in cp_data_list:
                if self.options.stop_checkpoint and \
                    str(cp) == self.options.stop_checkpoint:
                    break
                print "	%s" % (str(cp))

    def setup_logs(self):
        """
        Create the logger instanace for AI and create simple and
        detailed log files to use.
        """

        # Create logger for AI
        self.logger = logging.getLogger(INSTALL_LOGGER_NAME)

        # Log progress and info messages to the console.
        self.progress_ph = AIProgressHandler(self.logger,
            skip_console_msg=(self.options.list_checkpoints or \
                              self.options.zone_pool_dataset))
        self.progress_ph.start_progress_server()
        self.logger.addHandler(self.progress_ph)

        # Only ever send debug info to the logs, use INFO for console
        self.progress_ph.removeFilter(self.logger._prog_filter)
        self.progress_ph.setLevel(logging.INFO)
        datefmt = "%H:%M:%S"
        formatter = AIScreenFormatter(datefmt=datefmt,
                hide_progress=self.options.list_checkpoints)
        self.progress_ph.setFormatter(formatter)

        # create a install_log file handler and add it to the ai_logger

        # set the logfile names
        install_log = os.path.join(self._app_data.work_dir, self.INSTALL_LOG)
        self.install_log_fh = FileHandler(install_log)

        self.install_log_fh.setLevel(logging.DEBUG)
        if not self.options.list_checkpoints:
            self.logger.info("Install Log: %s" % install_log)
        self.logger.addHandler(self.install_log_fh)

    @property
    def be(self):
        if self._be is not None:
            return self._be

        new_be = None
        desired = self.doc.persistent.get_first_child(Target.DESIRED,
                                                      class_type=Target)

        if desired:
            try:
                new_be = desired.get_descendants(class_type=BE, max_count=1,
                                                 not_found_is_err=True)[0]
                self._be = new_be
            except ObjectNotFoundError:
                self.logger.error("Unable to locate new BE definition")

        return new_be

    def __transfer_install_log(self):
        """If BE exists, then transfer Log Files to New BE"""
        new_be = self.be

        if new_be is not None:
            # Assumes BE is still mounted, should be, if it exists.
            self.logger.debug("Transferring log to %s" %
                new_be.mountpoint + self.BE_LOG_DIR)
            self.install_log_fh.transfer_log(
                new_be.mountpoint + self.BE_LOG_DIR, isdir=True)
        else:
            self.logger.error(
                "Unable to determine location to transfer logs to")
            return False

        return True

    def __cleanup_before_exit(self, error_val):
        """Do some clean up and set exit code.
        """

        unmount_be = False

        self.exitval = error_val
        if not self.options.list_checkpoints:
            if error_val in [self.AI_EXIT_SUCCESS, self.AI_EXIT_AUTO_REBOOT]:
                self.logger.info("Automated Installation succeeded.")
                if self.options.zone_pool_dataset is None:
                    if error_val == self.AI_EXIT_AUTO_REBOOT:
                        self.logger.info("System will be rebooted now")
                    else:
                        self.logger.info("You may wish to reboot the system at "
                                         "this time.")
                unmount_be = True
            else:
                # error_val == self.AI_EXIT_FAILURE:
                self.logger.info("Automated Installation Failed")
                self.logger.info("Please see logs for more information")

        # Close logger now since it holds a handle to the log on the BE, which
        # makes it impossible to unmount the BE
        self.progress_ph.stop_progress_server()
        self.logger.close()

        # Only attempt to unmount BE if Target Instantiation has completed
        if self.options.stop_checkpoint not in self.CHECKPOINTS_BEFORE_TI:
            # If we didn't fail unmount the BE now.
            if unmount_be:
                if self.be is not None:
                    try:
                        self.be.unmount(self.options.dry_run,
                            altpool=self.options.zone_pool_dataset)
                    except RuntimeError as ex:
                        # Use print since logger is now closed.
                        print >> sys.stderr, str(ex)
                        self.exitval = self.AI_EXIT_FAILURE        

    def import_preserved_zpools(self):
        '''
        Check if we are preserving Zpools in manifest.
        If we are ensure any referenced zpools are imported.
        If importing fails, exit
        '''
        from_manifest = self.doc.find_path(
            "//[@solaris_install.auto_install.ai_instance.AIInstance?2]"
            "//[@solaris_install.target.Target?2]")

        cmd = [ZPOOL, "list", "-H", "-o", "name"]
        p = Popen.check_call(cmd, stdout=Popen.STORE, stderr=Popen.STORE,
                             logger=INSTALL_LOGGER_NAME)

        zpool_list = p.stdout.splitlines()

        if from_manifest:
            # Check if all Targets have children
            targets_have_children = False
            if from_manifest:
                for target in from_manifest:
                    if target.has_children:
                        targets_have_children = True
                        break

            if targets_have_children:
                target = from_manifest[0]
                logical = target.get_first_child(class_type=Logical)

                # Should only every be one logical
                if logical:
                    for zpool in logical.children:
                        if zpool.action in TargetSelection.PRESERVED and \
                            zpool.name not in zpool_list:
                            # Zpool being preserved but not imported
                            # Attempt to import.
                            cmd = [ZPOOL, "import", "-f", zpool.name]
                            p = Popen.check_call(cmd, stdout=Popen.STORE,
                                                 stderr=Popen.STORE,
                                                 logger=INSTALL_LOGGER_NAME,
                                                 check_result=Popen.ANY,
                                                 stderr_loglevel=logging.DEBUG)
                            if p.returncode != 0:
                                # Import failed cannot preserve, so fail AI
                                self.logger.error("Zpool '%s' with action '%s' "
                                    "failed to import. AI is unable to "
                                    "preserve unavailable zpools." % \
                                    (zpool.name, zpool.action))
                                return False
        return True

    def perform_autoinstall(self):
        """
        Main control method for performing an Automated Installation
        """

        # Check if we need to register/run derived manifest/parser checkpoints
        # If manifest argument set or derived script argument set then
        # need to parse the manifest.
        if self.manifest or self.derived_script:
            if not self.register_parse_manifest():
                self.logger.error("Derived/Parse Manifest " + \
                                  "registration failed")
                self.__cleanup_before_exit(self.AI_EXIT_FAILURE)
                return

            if not self.execute_parse_manifest():
                self.logger.error("Derived/Parse Manifest checkpoint failed")
                self.__cleanup_before_exit(self.AI_EXIT_FAILURE)
                return

            if self.options.stop_checkpoint is not None:
                if self.options.stop_checkpoint in self.MANIFEST_CHECKPOINTS:
                    self.logger.debug("DOC: %s" % (str(self.doc)))
                    self.logger.debug("DOC XML: %s" % \
                        (str(self.doc.get_xml_tree_str())))
                    self.logger.info("Automated Installation paused at " + \
                        "checkpoint: %s" % (self.options.stop_checkpoint))
                    self.__cleanup_before_exit(self.AI_EXIT_SUCCESS)

                    return

        errors = errsvc.get_all_errors()
        if errors:
            errstr = "Following errors occured parsing manifest :\n %s" % \
                (str(errors[0]))
            self.logger.error(errstr)
            self.__cleanup_before_exit(self.AI_EXIT_FAILURE)
            return

        # If we are to stop before target-discovery, then stop here
        # As target-discovery is the first checkpoint in the next
        # engine run. No need to call the engine.
        if self.options.stop_checkpoint is not None:
            if self.options.stop_checkpoint == "target-discovery":
                self.logger.debug("DOC: %s" % (str(self.doc)))
                self.logger.debug("DOC XML: %s" % \
                    (str(self.doc.get_xml_tree_str())))
                self.logger.info("Automated Installation paused at " + \
                    "checkpoint: %s" % (self.options.stop_checkpoint))
                self.__cleanup_before_exit(self.AI_EXIT_SUCCESS)
                return

        # Need to register all checkpoints
        if not self.configure_checkpoints():
            self.logger.error("Registering of checkpoints failed")
            self.__cleanup_before_exit(self.AI_EXIT_FAILURE)
            return

        # Validate stop checkpoint if specified
        if self.options.stop_checkpoint:
            if not self.validate_stop_checkpoint():
                self.logger.error("Invalid stop checkpoint specified: %s" % \
                    self.options.stop_checkpoint)
                self.__cleanup_before_exit(self.AI_EXIT_FAILURE)
                return

        # If specifying to list checkpoints, do so then exit
        # List of checkpoints available depend on what has just been
        # registered.
        if self.options.list_checkpoints:
            self.list_checkpoints()
            self.__cleanup_before_exit(self.AI_EXIT_SUCCESS)
            return

        # Check auto_reboot and proxy in DOC and set local definition
        ai_instance = self.doc.volatile.get_first_child(class_type=AIInstance)

        if ai_instance:
            self.auto_reboot = ai_instance.auto_reboot

            if ai_instance.http_proxy is not None and \
               len(ai_instance.http_proxy) > 0:
                # Set the HTTP Proxy environment variable
                os.environ["http_proxy"] = ai_instance.http_proxy

        self.logger.debug("Auto Reboot set to: %s" % (self.auto_reboot))

        # Ensure preserved zpools are online (imported)
        if not self.import_preserved_zpools():
            self.__cleanup_before_exit(self.AI_EXIT_FAILURE)
            return

        # Set resume_checkpoint to None, Engine will simply resume from
        # the next checkpoint that has not been run yet.
        # Specifying a resume_checkpoint you need to have ZFS dataset
        # containing a snapshot of where resumable checkpoint was paused.
        if self.execute_checkpoints(resume_checkpoint=None,
            pause_checkpoint=self.options.stop_checkpoint,
            dry_run=self.options.dry_run):

            if self.options.stop_checkpoint not in self.CHECKPOINTS_BEFORE_IPS:
                if not self.__transfer_install_log():
                    self.__cleanup_before_exit(self.AI_EXIT_FAILURE)
                else:
                    if self.auto_reboot and \
                        self.options.zone_pool_dataset is None:
                        self.__cleanup_before_exit(self.AI_EXIT_AUTO_REBOOT)
                    else:
                        self.__cleanup_before_exit(self.AI_EXIT_SUCCESS)

            # Successful Execution
            elif self.options.stop_checkpoint:
                self.logger.debug("DOC: %s" % (str(self.doc)))
                self.logger.debug("DOC XML: %s" % \
                    (str(self.doc.get_xml_tree_str())))
                self.logger.info("Automated Installation paused at " + \
                    "checkpoint: %s" % (self.options.stop_checkpoint))
                self.__cleanup_before_exit(self.AI_EXIT_SUCCESS)
            else:
                self.__cleanup_before_exit(self.AI_EXIT_SUCCESS)
        else:
            self.__cleanup_before_exit(self.AI_EXIT_FAILURE)

    def register_parse_manifest(self):
        """
        Method to parse the manifest

        If derived_script argument is provided, then Use Derived Manifest
        checkpoint. Derive the manifest to use for this automated install
        and parse it.  Otherwise just parse the manifest.

        Path of execution for Derived Manifest
        - Store Derived Script in DOC for DM checkpoint to read.
        - Register Derived Manifest Checkpoint (DM)
        - Register Manifest Parser Checkpoint (MP)
        """

        try:
            if self.derived_script:
                if not self.options.list_checkpoints:
                    self.logger.info("Deriving manifest from: %s" % \
                        (self.derived_script))

                # Store Derived Script name into DOC for DM checkpoint to read.
                # Check if derived script path is on volatile doc
                dm = self.doc.volatile.get_first_child(
                    name=DERIVED_MANIFEST_DATA)

                if dm is not None:
                    # Just change it's value
                    dm.script = self.derived_script
                else:
                    # Insert a new child
                    dm = DerivedManifestData(DERIVED_MANIFEST_DATA, \
                        script=self.derived_script)
                    self.doc.volatile.insert_children(dm)

                if not self.options.list_checkpoints:
                    self.logger.info("Derived %s stored" % (dm.script))

                    # Register Derived Manifest checkpoint
                    self.logger.info("Registering Derived Manifest " \
                        "Module Checkpoint")

                self.engine.register_checkpoint("derived-manifest",
                    "solaris_install.auto_install.checkpoints.dmm",
                    "DerivedManifestModule", args=None, kwargs=None)

                # Set arguments for Manifest Parser Checkpoint
                kwargs = dict()
                kwargs["call_xinclude"] = True
                args = None

            elif self.manifest:
                # Set arguments for Manifest Parser Checkpoint
                kwargs = dict()
                kwargs["call_xinclude"] = True
                args = [self.manifest]
            else:
                # No manifest specified to parse
                return True

            if not self.options.list_checkpoints:
                self.logger.debug("Registering Manifest Parser Checkpoint")

            self.engine.register_checkpoint("manifest-parser",
                                    "solaris_install.manifest.parser",
                                    "ManifestParser", args=args, kwargs=kwargs)
            return True
        except Exception as ex:
            self.logger.debug("Uncaught exception parsing manifest: %s" % \
                str(ex))
            return False

    def execute_parse_manifest(self):
        """
        Execute derived or/and manifest pareser checkpoints
          - DM checkpoint will read script from DOC
          - DM will derive manifest and store final location in DOC
          - MP will read DOC for manifest to parse, if not passed any
            manifest as an explicit argument. (This is not implemented yet)
        """
        # Execute Checkpoints
        if not self.options.list_checkpoints:
            if self.derived_script:
                self.logger.debug("Executing Derived Manifest and Manifest " \
                        "Parser Checkpoints")
            else:
                self.logger.debug("Executing Manifest Parser Checkpoint")

        if self.options.stop_checkpoint in self.MANIFEST_CHECKPOINTS:
            pause_cp = self.options.stop_checkpoint
        else:
            pause_cp = None

        if not self.execute_checkpoints(pause_checkpoint=pause_cp, \
            dry_run=False):
            return False

        # If derived manifest run, read the stored manifest location
        # from pm.manifest.
        if self.derived_script:
            pm = self.doc.volatile.get_first_child(name=MANIFEST_PARSER_DATA)
            if pm is None or pm.manifest is None:
                self.logger.error("Derived Manifest Failed, manifest not set")
                return False

            # Ideal Path - We have a parsed manifest at this point
            self.logger.info("DM set manifest to: %s" % (pm.manifest))
            self.manifest = pm.manifest

        self.logger.info("Manifest %s successfully parsed" % (self.manifest))
        self.logger.debug("DOC (tree format):\n%s\n\n\n" %
            str(self.engine.data_object_cache))
        self.logger.debug("DOC (xml_format):\n%s\n\n\n" %
            str(self.engine.data_object_cache.get_xml_tree_str()))

        return True

    def execute_checkpoints(self, resume_checkpoint=None,
        pause_checkpoint=None, dry_run=False):
        """
        Wrapper to the execute_checkpoint method
        """

        # Get execution list from engine to determine of any checkpoints
        # to run, if not return false
        if len(self.engine.get_exec_list(None, None)) == 0:
            self.logger.warning("No checkpoints to execute")
            return False

        self.logger.debug("Executing Engine Checkpoints...")
        if resume_checkpoint is not None:
            self.logger.debug("Resuming at checkpoint: %s" % \
                (resume_checkpoint))

        if pause_checkpoint is not None:
            self.logger.debug("Pausing before checkpoint: %s" %
                (pause_checkpoint))

        try:
            if resume_checkpoint is not None:
                (status, failed_cps) = self.engine.resume_execute_checkpoints(
                    resume_checkpoint, pause_before=pause_checkpoint,
                    dry_run=dry_run, callback=None)
            else:
                (status, failed_cps) = self.engine.execute_checkpoints(
                    start_from=None, pause_before=pause_checkpoint,
                    dry_run=dry_run, callback=None)
        except (ManifestError, ParsingError) as ex:
            self.logger.error("Manifest parser checkpoint error :")
            print "\t\t%s" % str(ex)
            return False
        except (SelectionError) as ex:
            self.logger.error("Target selection checkpoint error :")
            print "\t\t%s" % str(ex)
            return False
        except (ValueError) as ex:
            self.logger.error("Value errors occured :")
            print "\t\t%s" % str(ex)
            return False
        except (AIConfigurationError) as ex:
            self.logger.error("AI Configuration checkpoint error :")
            print "\t\t%s" % str(ex)
            return False
        except (RollbackError, UnknownChkptError, UsageError) as ex:
            self.logger.error("RollbackError, UnknownChkptError, UsageError :")
            print "\t\t%s" % str(ex)
            raise RuntimeError(str(ex))
        except Exception, ex:
            self.logger.debug("%s" % (traceback.format_exc()))
            raise RuntimeError(str(ex))

        self.logger.debug("Checkpoints Completed : DOC : \n%s\n\n", self.doc)
        self.logger.debug("Checkpoints Completed : "
                          "DOC (xml_format):\n%s\n\n\n" %
            str(self.engine.data_object_cache.get_xml_tree_str()))

        if status != InstallEngine.EXEC_SUCCESS:
            self.logger.critical("Failed Checkpoints:")
            for failed_cp in failed_cps:
                err_data = errsvc.get_errors_by_mod_id(failed_cp)[0]
                self.logger.critical("\t%s" % failed_cp)
                self.logger.exception(err_data.error_data[ES_DATA_EXCEPTION])
            return False
        else:
            return True

    def configure_checkpoints(self):
        """
        Wrapper to configure required checkpoints for performing an
        automated installation
        """
        # Need to set following Checkpoints for installation.  Checkpoints
        # marked with a 'G' are applicable when installing a global zone.
        # Checkpoings marked with a 'N' are application when installing a
        # non-global zone.
        #   G- -- Derived Manifest (If script passed as argument)
        #   GN -- Manifest Parser (If manifest passed or derived)
        #   G- -- Target Discovery
        #   G- -- Target Selection
        #   -N -- Target Selection Zone
        #   GN -- AI Configuration
        #   G- -- Device Driver Update - Install Root
        #   G- -- Target Instantiation
        #   -N -- Target Instantiation Zone
        #   GN -- Transfer
        #   GN -- Target Configuration
        #   G- -- Device Driver Update - New BE

        try:
            if not self.options.list_checkpoints:
                self.logger.info("Configuring Checkpoints")

            # Register TargetDiscovery
            if self.options.zone_pool_dataset is None:
                self.engine.register_checkpoint("target-discovery",
                                "solaris_install.target.discovery",
                                "TargetDiscovery", args=None, kwargs=None)

            # Register TargetSelection
            if self.options.zone_pool_dataset is None:
                self.logger.debug("Adding Target Selection Checkpoint")
                self.engine.register_checkpoint("target-selection",
                    "solaris_install.auto_install.checkpoints."
                    "target_selection", "TargetSelection", args=None,
                    kwargs=None)
            else:
                self.logger.debug("Adding Target Selection Zone Checkpoint")
                self.engine.register_checkpoint("target-selection",
                    "solaris_install.auto_install.checkpoints."
                    "target_selection_zone", "TargetSelectionZone", args=None,
                    kwargs={"be_mountpoint": self.installed_root_dir})

            # Register AIConfiguration
            self.logger.debug("Adding AI Configuration Checkpoint")
            self.engine.register_checkpoint("ai-configuration",
                "solaris_install.auto_install.checkpoints.ai_configuration",
                "AIConfiguration", args=None, kwargs=None)

            # Register TargetInstantiation
            if self.options.zone_pool_dataset is None:
                self.logger.debug("Adding Target Instantiation Checkpoint")
                self.engine.register_checkpoint(self.TARGET_INSTANTIATION_CHECKPOINT,
                                "solaris_install.target.instantiation",
                                "TargetInstantiation", args=None, kwargs=None)
            else:
                self.logger.debug("Adding Target Instantiation Zone "
                    "Checkpoint")
                self.engine.register_checkpoint("target-instantiation",
                                "solaris_install/target/instantiation_zone",
                                "TargetInstantiationZone", args=None,
                                kwargs=None)

            # Add destination for transfer nodes, and register checkpoints.
            sw_nodes = self.doc.volatile.get_descendants(class_type=Software)
            image_action = AbstractIPS.CREATE  # For first IPS only
            transfer_count = 0  # For generating names if none provided
            for sw in sw_nodes:
                transfer_count += 1
                if sw.name is None or len(sw.name) == 0:
                    # Generate a name, setting internal attribute
                    sw._name = "generated-transfer-%d-%d" % \
                        (os.getpid(), transfer_count)

                # Add first transfer checkpoint name to list of checkpoints
                # to ensure -I option succeeds.
                if transfer_count == 1:
                    self.CHECKPOINTS_BEFORE_IPS.append(sw.name)
                    if self.options.stop_checkpoint is not None:
                        if self.options.stop_checkpoint == \
                            self.FIRST_TRANSFER_CHECKPOINT:
                            self.options.stop_checkpoint = sw.name

                # Ensure there is at least one software_data element with
                # Install action exists, and that all software_data elements
                # contain at least one 'name' sub element.
                found_install_sw_data = False
                tran_type = sw.tran_type.upper()
                for sw_child in sw.children:
                    found_sw_data = False
                    if tran_type == "IPS" and isinstance(sw_child, IPSSpec):
                        found_sw_data = True
                        if sw_child.action == IPSSpec.INSTALL:
                            found_install_sw_data = True
                    elif tran_type == "CPIO" and \
                        isinstance(sw_child, CPIOSpec):
                        found_sw_data = True
                        if sw_child.action == CPIOSpec.INSTALL:
                            found_install_sw_data = True
                    elif tran_type == "SVR4" and \
                        isinstance(sw_child, SVR4Spec):
                        found_sw_data = True
                        if sw_child.action == SVR4Spec.INSTALL:
                            found_install_sw_data = True

                    if found_sw_data and len(sw_child.contents) == 0:
                        self.logger.error("Invalid manifest specification "
                            "for <software_data> element. Must specify at "
                            "least one package to install/uninstall.")
                        return False

                if not found_install_sw_data:
                    self.logger.error("No packages specified to install. "
                        "Manifest must contain at least one <software_data> "
                        "element with 'install' action.")
                    return False

                self.logger.debug("Setting destination for transfer: %s to %s"
                    % (sw.name, self.installed_root_dir))
                dst = sw.get_first_child(class_type=Destination)
                if dst is None:
                    dst = Destination()
                    if sw.tran_type.upper() == "IPS":
                        image = Image(self.installed_root_dir, image_action)

                        if self.options.zone_pool_dataset is None:
                            img_type = ImType("full", zone=False)
                        else:
                            img_type = ImType("full", zone=True)

                        image.insert_children(img_type)
                        dst.insert_children(image)
                        image_action = AbstractIPS.EXISTING
                    else:
                        directory = Dir(self.installed_root_dir)
                        dst.insert_children(directory)
                    sw.insert_children(dst)
                    # Next images are use_existing, not create.
                else:
                    raise RuntimeError(
                        "Unexpected destination in software node: %s" % \
                        (sw.name))

                # Register a Transfer checkpoint suitable for the selected
                # Software node
                ckpt_info = create_checkpoint(sw)
                if ckpt_info is not None:
                    self.logger.debug("Adding Target Instantation Checkpoint: "
                        "%s, %s, %s" % ckpt_info)
                    self.engine.register_checkpoint(*ckpt_info)

            # Register ICT Checkpoints
            #=========================
            # 1. Initialize SMF Repository
            if self.options.zone_pool_dataset is None:
                self.engine.register_checkpoint("initialize-smf",
                    "solaris_install.ict.initialize_smf",
                    "InitializeSMF", args=None, kwargs=None)
            else:
                self.engine.register_checkpoint("initialize-smf-zone",
                    "solaris_install.ict.initialize_smf",
                    "InitializeSMFZone", args=None, kwargs=None)

            # 2. Boot Configuration
            if self.options.zone_pool_dataset is None:
                self.engine.register_checkpoint("boot-configuration",
                    "solaris_install.boot.boot",
                    "SystemBootMenu", args=None, kwargs=None)

            # 3. Update dumpadm / Dump Configuration
            if self.options.zone_pool_dataset is None:
                self.engine.register_checkpoint("update-dump-adm",
                    "solaris_install.ict.update_dumpadm",
                    "UpdateDumpAdm", args=None, kwargs=None)

            # 4. Setup Swap in Vfstab
            self.engine.register_checkpoint("setup-swap",
                "solaris_install.ict.setup_swap",
                "SetupSwap", args=None, kwargs=None)

            # 5. Set Flush IPS Content Flag
            self.engine.register_checkpoint("set-flush-ips-content-cache",
                "solaris_install.ict.ips",
                "SetFlushContentCache", args=None, kwargs=None)

            # 6. Device Configuration / Create Device Namespace
            if self.options.zone_pool_dataset is None:
                self.engine.register_checkpoint("device-config",
                    "solaris_install.ict.device_config",
                    "DeviceConfig", args=None, kwargs=None)

            # 7. Transfer System Configuration To BE / ApplySysConfig
            if self.options.profile is not None:
                self.engine.register_checkpoint("apply-sysconfig",
                    "solaris_install.ict.apply_sysconfig",
                    "ApplySysConfig", args=None, kwargs=None)

            # 8. Boot Archive
            if self.options.zone_pool_dataset is None:
                self.engine.register_checkpoint("boot-archive",
                    "solaris_install.ict.boot_archive",
                    "BootArchive", args=None, kwargs=None)

            # 9. Transfer Files to New BE
            self.add_transfer_files()
            self.engine.register_checkpoint(TRANSFER_FILES_CHECKPOINT,
                "solaris_install.ict.transfer_files",
                "TransferFiles", args=None, kwargs=None)

            # 10. CreateSnapshot before reboot
            self.engine.register_checkpoint("create-snapshot",
                "solaris_install.ict.create_snapshot",
                "CreateSnapshot", args=None, kwargs=None)

        except Exception as ex:
            self.logger.debug(
                "An execption occurred registering checkpoints: %s\n%s" %
                str(ex), traceback.format_exc())
            return False

        return True

    def add_transfer_files(self):
        """
            Create dataobjectdict dictionary of containing src/dest
            pairs for files that are to be transferred to the new
            boot environment.
        """
        # Check for existence of transfer-ai-files data object dictionary,
        # insert if not found
        tf_doc_dict = None
        tf_doc_dict = self.doc.volatile.get_first_child( \
            name=TRANSFER_FILES_CHECKPOINT)

        if tf_doc_dict is None:
            # Initialize dictionary in DOC
            tf_dict = dict()
            tf_doc_dict = DataObjectDict(TRANSFER_FILES_CHECKPOINT,
                tf_dict)
            self.doc.volatile.insert_children(tf_doc_dict)
        else:
            tf_dict = tf_doc_dict.data_dict

        # If using dmm, transfer script and derived manifest
        if self.derived_script:
            dm = self.doc.volatile.get_first_child(
                    name=DERIVED_MANIFEST_DATA)
            if dm is not None and dm.script is not None:
                tf_dict[dm.script] = \
                    post_install_logs_path('derived/manifest_script')

            mp = self.doc.volatile.get_first_child(name=MANIFEST_PARSER_DATA)
            if mp is not None and mp.manifest is not None:
                tf_dict[mp.manifest] = \
                    post_install_logs_path('derived/manifest.xml')
        # Else transfer the XML manifest passed in
        else:
            tf_dict[self.manifest] = post_install_logs_path('ai.xml')

        # Transfer smf logs
        tf_dict['/var/svc/log/application-auto-installer:default.log'] = \
            post_install_logs_path('application-auto-installer:default.log')
        tf_dict['/var/svc/log/application-manifest-locator:default.log'] = \
            post_install_logs_path('application-manifest-locator:default.log')

        # Transfer AI Service Discovery Log
        tf_dict[system_temp_path('ai_sd_log')] = \
            post_install_logs_path('ai_sd_log')

        # Transfer /var/adm/messages
        tf_dict['/var/adm/messages'] = post_install_logs_path('messages')

        # Possibly copy contents of ApplicationData.work_dir, however
        # for standard AI install, this is /system/volatile, so not feasable
        # However for zones install, it would makes sense as work_dir in
        # that scenario would be unique to each AI instance and would have
        # limited number of files.
        # e.g.
        #  dest = post.install_logs_path(\
        #      os.path.basename(self._app_data.work_dir))
        #  tf_dict[self._app_data.work_dir] = dest


class AIScreenFormatter(logging.Formatter):
    """ AI-Specific Formatter class. Suppresses traceback printing to
    the screen by overloading the format() method.

    Checks if log message is MAX_INT (Progress Log) or Normal log and
    formats message appropriately.
    """

    def __init__(self, fmt=None, datefmt=None, hide_progress=True):
        """Initialize formatter class.

           Consume hide_progress boolean for local processing.
        """
        self.hide_progress = hide_progress

        logging.Formatter.__init__(self, fmt, datefmt)

    def format(self, record):
        """ Overload method to prevent the traceback from being printed.
        """
        record.message = record.getMessage()
        record.asctime = self.formatTime(record, self.datefmt)

        formatted_str = ""
        fmt = None

        if self.hide_progress:
            # Don't output progress information for -l option
            if record.levelno != MAX_INT:
                fmt = "%(message)s"
        else:
            if record.levelno == MAX_INT:
                fmt = "%(asctime)-11s %(progress)s%% %(message)s"
            else:
                fmt = "%(asctime)-11s %(message)s"

        if fmt is not None:
            formatted_str = fmt % record.__dict__

        return formatted_str


class AIProgressHandler(ProgressHandler):
    """ AI-Specific ProgressHandler. """
    def __init__(self, logger, hostname=None, portno=None,
                 skip_console_msg=False):
        if hostname is not None:
            self.hostname = hostname
        else:
            self.hostname = 'localhost'

        self.engine_skt = None
        self.server_up = False
        self.logger = logger
        self.skip_console_msg = skip_console_msg

        # Get a port number
        self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        if portno is not None:
            self.portno = portno
            try:
                self.skt.bind((self.hostname, self.portno))
                self.skt.listen(5)
            except socket.error:
                self.logger.error("AIProgresHandler init failed")
                self.logger.debug("%s" % (traceback.format_exc()))
                return None
        else:
            random.seed()
            # Continue looping until skt.listen(5) does not cause socket.error
            while True:
                try:
                    # Get a random port between 10000 and 30000
                    self.portno = random.randint(10000, 30000)
                    self.skt.bind((self.hostname, self.portno))
                    self.skt.listen(5)
                    break
                except socket.error, msg:
                    self.skt.close()
                    self.skt = None
                    continue

        ProgressHandler.__init__(self, self.hostname, self.portno)

    def start_progress_server(self):
        """ Starts the socket server stream to receive progress messages. """
        if not self.server_up:
            self.logger.debug("Starting up Progress Handler")
            self.server_up = True
            self.engine_skt, address = self.skt.accept()
            thread.start_new_thread(self.progress_server,
                (self.progress_receiver, ))
            time.sleep(1)

    def stop_progress_server(self):
        """ Stop the socket server stream. """
        if self.server_up:
            self.server_up = False
            self.logger.debug("Shutting down Progress Handler")

    def progress_server(self, cb):
        """ Actual spawned progress_server process. """
        try:
            while self.server_up:
                percentage, msg = self.parse_progress_msg(self.engine_skt, cb)
            self.engine_skt.close()
        except Exception, ex:
            self.logger.error("Progress Server Error")
            self.logger.debug("%s" % (str(ex)))

    @staticmethod
    def parse_progress_msg(skt, cb):
        """Parse the messages sent by the client."""
        total_len = 0
        total_data = list()
        size = sys.maxint
        size_data = sock_data = ''
        recv_size = 8192
        percent = None
        msg = None

        while total_len < size:
            sock_data = skt.recv(recv_size)
            if not total_data:
                if len(sock_data) > 4:
                    size_data += sock_data
                    size = struct.unpack('@i', size_data[:4])[0]
                    recv_size = size
                    if recv_size > 524288:
                        recv_size = 524288
                    total_data.append(size_data[4:])
                else:
                    size_data += sock_data
            else:
                total_data.append(sock_data)
            total_len = sum([len(i) for i in total_data])
            message = ''.join(total_data)
            if message:
                # This is a callback function that sends the message to
                # the receiver
                cb(message)
                percent, msg = message.split(' ', 1)
            break
        return percent, msg

    def progress_receiver(self, msg):
        """Receive a message, show on screen and/or console"""

        # Default to showing on stdout
        print "%s" % (msg)

        if not self.skip_console_msg and not users_on_console():
            # Also log to console if no-one is logged in there.
            try:
                with open("/dev/sysmsg", "w+") as fh:
                    fh.write("%s\n" % (msg))
            except IOError:
                # Quietly fail - can't log or we cause a repeating loop
                pass