usr/src/cmd/auto-install/checkpoints/ai_configuration.py
changeset 1160 6f7e708c38ec
child 1227 4ecf733307ad
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usr/src/cmd/auto-install/checkpoints/ai_configuration.py	Tue May 31 14:21:09 2011 -0700
@@ -0,0 +1,302 @@
+#!/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.
+#
+
+""" ai_configuration.py - AI Configuration
+"""
+
+import os
+import os.path
+import shutil
+import urllib
+
+from solaris_install import ApplicationData, Popen, CalledProcessError
+from solaris_install.auto_install import TRANSFER_FILES_CHECKPOINT
+from solaris_install.configuration.configuration import Configuration
+from solaris_install.data_object import ObjectNotFoundError
+from solaris_install.data_object.data_dict import DataObjectDict
+from solaris_install.engine import InstallEngine
+from solaris_install.engine.checkpoint import AbstractCheckpoint as Checkpoint
+from solaris_install.ict import SVCCFG
+from solaris_install.target import Target
+from solaris_install.target.instantiation_zone import ALT_POOL_DATASET
+from solaris_install.target.logical import Filesystem, Zpool
+
+# Define constants
+XMLLINT = '/usr/bin/xmllint'
+
+# Checkpoint specific AI ApplicationData dictionary keys
+AI_SERVICE_LIST_FILE = "service_list_file"
+
+
+class AIConfigurationError(Exception):
+    '''Error generated when a configuration error is detected'''
+
+    def __init__(self, msg):
+        Exception.__init__(self)
+        self.msg = msg
+
+    def __str__(self):
+        return self.msg
+
+class AIConfiguration(Checkpoint):
+    """ AIConfiguration - Checkpoint to process configuration
+        specifications in the AI manifest.
+    """
+
+    def __init__(self, name):
+        super(AIConfiguration, self).__init__(name)
+
+        self.app_data = None
+        self.service_list_file = None
+        self.alt_pool_dataset = None
+        self.zone_confs = list()
+
+    def validate_zone_configuration(self, dry_run=False):
+        """ Method to download and validate all zone configurations
+            specified in the AI manifest.  This method will also query
+            the AI installation service (if provided) to download
+            AI manifest and SC profiles for the zones specified.
+        """
+
+        TMP_ZONES_DIR = os.path.join(self.app_data.work_dir, "zones")
+        TMP_ZONES_CONFIG_LIST = os.path.join(TMP_ZONES_DIR , "config_list")
+        TMP_ZONES_CONFIG_OUTPUT = os.path.join(TMP_ZONES_DIR, "config_output")
+        TMP_ZONES_INSTALL_DIR = os.path.join(TMP_ZONES_DIR , "install")
+
+        TARGET_ZONES_INSTALL_DIR = "/var/zones/install"
+
+        # Clear out the TMP_ZONES_DIR directory in case it exists.
+        shutil.rmtree(TMP_ZONES_DIR, ignore_errors=True)
+
+        # Create TMP_ZONES_DIR directory
+        os.makedirs(TMP_ZONES_DIR)
+
+        try:
+            with open(TMP_ZONES_CONFIG_LIST, 'w') as zones_config_list:
+                # Copy all 'source' entries to a local area.
+                for conf in self.zone_confs:
+                    self.logger.info("Zone name: " + conf.name)
+                    self.logger.info("   source: " + conf.source)
+
+                    # Make subdirectory to store this zone's files.  The name
+                    # of the zone is used as the name of the subdirectory.
+                    os.makedirs(os.path.join(TMP_ZONES_INSTALL_DIR, conf.name))
+
+                    # Retrieve the zone config file.
+                    try:
+                        (filename, headers) = urllib.urlretrieve(conf.source,
+                            os.path.join(TMP_ZONES_INSTALL_DIR, conf.name,
+                                         "config"))
+                    except urllib.ContentTooShortError, er:
+                        raise AIConfigurationError("Retrieval of zone config "
+                            "file (%s) failed: %s" % (conf.name, str(er)))
+
+                    # Append this zone's local config file path
+                    zones_config_list.write(filename + "\n")
+
+                    # Retrieve this zone's AI manifest and profile(s) from a
+                    # remote installation service if service_list_file is
+                    # provided.
+                    if self.service_list_file is not None:
+                        cmd = ["/usr/bin/ai_get_manifest", "-e",
+                               "-o", os.path.join(TMP_ZONES_INSTALL_DIR,
+                               conf.name, "ai_manifest.xml"),
+                               "-p", os.path.join(TMP_ZONES_INSTALL_DIR,
+                               conf.name, "profiles"),
+                               "-c", "zonename=" + conf.name,
+                               "-s", self.service_list_file]
+                        try:
+                            Popen.check_call(cmd, stdout=Popen.STORE,
+                                             stderr=Popen.STORE,
+                                             logger=self.logger)
+                        except CalledProcessError, er:
+                            raise AIConfigurationError("AI manifest query for "
+                                "zone (%s) failed: %s" % (conf.name, str(er)))
+        except IOError, er:
+            raise AIConfigurationError("IO Error during zone validation: %s" % \
+                                       str(er))
+
+        # Pass the create a file with the list of zone config files in it
+        # to "/usr/sbin/zonecfg auto-install-report", which will parse the
+        # config files, do any necessary validation of the zone configurations,
+        # and yield an output file that will contain a list of directories
+        # and datasets that are required to exist for the given zone configs
+        # to work.
+
+        cmd = ['/usr/sbin/zonecfg', 'auto-install-report',
+               '-f', TMP_ZONES_CONFIG_LIST, '-o', TMP_ZONES_CONFIG_OUTPUT]
+        try:
+            Popen.check_call(cmd, stdout=Popen.STORE, stderr=Popen.STORE,
+                             logger=self.logger)
+        except CalledProcessError, er:
+            raise AIConfigurationError("Zone configurations failed "
+                                       "to validate.")
+
+        directories = list()
+        datasets = list()
+        try:
+            with open(TMP_ZONES_CONFIG_OUTPUT, 'r') as zones_config_output:
+                for line in zones_config_output:
+                    (name, value) = line.strip().split("=", 2)
+
+                    if name == "zonepath_parent":
+                        directories.append(value)
+                    elif name == "zfs_dataset":
+                        datasets.append(value)
+                    else:
+                        raise AIConfigruationError("Failure: unknown keyword "
+                            "in %s: %s" % (TMP_ZONES_CONFIG_OUTPUT, name))
+        except IOError, er:
+            raise AIConfigurationError("Could not read zone config output "
+                                       "file (%s): %s" % \
+                                       (TMP_ZONES_CONFIG_OUTPUT, str(er)))
+
+
+        # TODO: Check that all zonepath_parent values are directories that
+        # will not exist under a filesystem dataset that is inside the BE.
+
+
+        # Ensure all zfs_dataset values are specified to be created
+        # on the installed system.  At this point, Target Selection
+        # should have already run, so we should be able to grab the
+        # DESIRED filesystems from the DOC to make sure these datasets
+        # are specified.
+        if datasets:
+            target = self.doc.get_descendants(name=Target.DESIRED,
+                class_type=Target, not_found_is_err=True)[0]
+            fs_list = target.get_descendants(class_type=Filesystem)
+
+            if fs_list:
+                for dataset in datasets:
+                    if dataset not in [fs.full_name for fs in fs_list]:
+                        raise AIConfigurationError("The following dataset is "
+                            "specified in a zone configuration but does not "
+                            "exist in the AI manifest: %s" % dataset)
+
+        # Ensure all AI manifests and SC profiles are valid.
+        for zone in os.listdir(TMP_ZONES_INSTALL_DIR):
+            zone_dir = os.path.join(TMP_ZONES_INSTALL_DIR, zone)
+
+            manifest = os.path.join(zone_dir, "ai_manifest.xml")
+            if os.path.isfile(manifest):
+                # Validate AI manifest.
+                cmd = [XMLLINT, '--valid', manifest]
+                if dry_run:
+                    self.logger.debug('Executing: %s', cmd)
+                else:
+                    Popen.check_call(cmd, stdout=Popen.STORE,
+                        stderr=Popen.STORE, logger=self.logger)
+
+            profiles_dir = os.path.join(zone_dir, "profiles")
+            if os.path.isdir(profiles_dir):
+                for profile in os.listdir(profiles_dir):
+                    profile_file = os.path.join(profiles_dir, profile)
+                    if os.path.isfile(profile_file):
+                        # Validate SC profile.
+                        cmd = [SVCCFG, 'apply', '-n ', profile_file]
+                        if dry_run:
+                            self.logger.debug('Executing: %s', cmd)
+                        else:
+                            Popen.check_call(cmd, stdout=Popen.STORE, \
+                                stderr=Popen.STORE, logger=self.logger)
+
+        # Add the zone configuration directory into the dictionary in the DOC
+        # that will be processed by the transfer-ai-files checkpoint which will
+        # copy files over to the installed root.
+        tf_doc_dict = None
+        tf_doc_dict = self.doc.volatile.get_first_child( \
+            name=TRANSFER_FILES_CHECKPOINT)
+        if tf_doc_dict is None:
+            # Initialize new 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
+
+        tf_dict[TMP_ZONES_INSTALL_DIR] = TARGET_ZONES_INSTALL_DIR
+
+    def parse_doc(self):
+        """ class method for parsing the data object cache (DOC) objects
+        for use by this checkpoint
+        """
+        self.engine = InstallEngine.get_instance()
+        self.doc = self.engine.data_object_cache
+
+        # Get a reference to ApplicationData object
+        self.app_data = self.doc.persistent.get_first_child( \
+            class_type=ApplicationData)
+
+        if self.app_data:
+            # Get the installation service list file
+            self.service_list_file = \
+                self.app_data.data_dict.get(AI_SERVICE_LIST_FILE)
+
+            # See if an alternate pool dataset is set.
+            self.alt_pool_dataset = \
+                self.app_data.data_dict.get(ALT_POOL_DATASET)
+
+        if not self.service_list_file:
+            self.logger.debug("No service list provided.")
+
+        # Get all configuration components from the DOC
+        self.conf_list = self.doc.get_descendants(class_type=Configuration)
+
+    def get_progress_estimate(self):
+        """Returns an estimate of the time this checkpoint will take
+        """
+        return 2
+
+    def execute(self, dry_run=False):
+        """ Execution method
+
+            This method will process all configuration components that are
+            in the AI manifest.
+        """
+
+        self.parse_doc()
+
+        # AI currently only supports configuration of type 'zone'.
+        # Iterate all 'zone' configuration components and process them,
+        # ignore all other types.
+        for conf in self.conf_list:
+            if conf.type is not None and \
+                    conf.type == Configuration.TYPE_VALUE_ZONE:
+                self.zone_confs.append(conf)
+            else:
+                self.logger.debug('Unsupported configuration type "' +
+                        conf.type + '".  Ignoring ...')
+
+        if self.zone_confs:
+            if not self.alt_pool_dataset:   
+                self.validate_zone_configuration(dry_run=dry_run)
+            else:
+                # If alt_pool_dataset is not none, we're installing
+                # a zone, so we ignore configuration of type 'zone'
+                self.logger.debug('Configurations of type "' +
+                    Configuration.TYPE_VALUE_ZONE + '" not supported '
+                    'when installing a zone.  Ignoring ...')