usr/src/cmd/distro_const/checkpoints/boot_archive_archive.py
author Mary Ding <mary.ding@oracle.com>
Thu, 17 May 2012 11:41:46 -0700
changeset 1680 902e7dbd8043
parent 1605 670fed6a49a9
child 1686 c09a124082fc
permissions -rw-r--r--
7155706 pylint errors in usr/src/lib/install_transfer/ips.py 7160785 pylint warnings in usr/src/cmd/distro_const/ 7153262 new pep8 error in boot_archive_archive.py due to bugid 7073683

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

""" boot_archive_archive - archive the boot archive directory
"""
import copy
import math
import os
import platform
import stat

from solaris_install import DC_LABEL, run, run_silent
from solaris_install.engine.checkpoint import AbstractCheckpoint as Checkpoint
from solaris_install.data_object.data_dict import DataObjectDict
from solaris_install.engine import InstallEngine
from osol_install.install_utils import dir_size
from solaris_install.target.logical import Lofi
from solaris_install.transfer.cpio import TransferCPIOAttr
from solaris_install.transfer.info import INSTALL

# load a table of common unix cli calls
import solaris_install.distro_const.cli as cli
cli = cli.CLI()


class BootArchiveArchive(Checkpoint):
    """ class to archive the boot archive directory
    """

    DEFAULT_ARG = {"compression_type": "gzip", "compression_level": 9,
                   "size_pad": 0, "bytes_per_inode": 0}
    DEFAULT_ARGLIST = {"uncompressed_files": []}

    MIN_PADDING_SIZE_IN_MB = 35

    def __init__(self, name, arg=DEFAULT_ARG, arglist=DEFAULT_ARGLIST):
        super(BootArchiveArchive, self).__init__(name)

        self.comp_type = arg.get("compression_type",
                                 self.DEFAULT_ARG.get("compression_type"))
        self.comp_level = int(arg.get("compression_level",
                              self.DEFAULT_ARG.get("compression_level")))
        self.size_pad = int(arg.get("size_pad",
                            self.DEFAULT_ARG.get("size_pad")))
        self.nbpi = int(arg.get("bytes_per_inode",
                        self.DEFAULT_ARG.get("bytes_per_inode")))

        self.uncompressed_files = arglist.get("uncompressed_files",
                                              self.DEFAULT_ARGLIST.get(
                                                  "uncompressed_files"))

        # instance attributes
        self.doc = None
        self.dc_dict = {}
        self.pkg_img_path = None
        self.ba_build = None
        self.tmp_dir = None

        self.lofi = None

        if platform.processor() == "i386":
            self.kernel_arch = "x86"
        else:
            self.kernel_arch = "sparc"

    def get_progress_estimate(self):
        """Returns an estimate of the time this checkpoint will take
        """
        return 20

    def parse_doc(self):
        """ class method for parsing data object cache (DOC) objects for use by
        the checkpoint.
        """
        self.doc = InstallEngine.get_instance().data_object_cache
        self.dc_dict = self.doc.volatile.get_children(name=DC_LABEL,
            class_type=DataObjectDict)[0].data_dict

        try:
            self.pkg_img_path = self.dc_dict["pkg_img_path"]
            self.ba_build = self.dc_dict["ba_build"]
            self.tmp_dir = self.dc_dict["tmp_dir"]
        except KeyError:
            raise RuntimeError("Error retrieving a value from the DOC")

    def calculate_nbpi(self, directory, size):
        """ class method to calculate exactly what the nbpi should be

        directory - root of the boot archive
        size - the size of the boot archive
        """
        file_count = 0

        # get the cwd to return to later
        cwd = os.getcwd()

        # change to the boot_archive directory
        os.chdir(directory)
        for _none, dirs, files in os.walk("."):
            file_count += len(files) + len(dirs)

        # Add inode overhead for multiple disk systems using 500 disks as a
        # target upper bound.
        #
        # For sparc we need 16 indoes per target device:
        # 8 slices * 2 (block and character device)
        #
        # For x86 we need 42 inodes
        # (5 partitions + 16 slices) * 2
        # To account for dual ported drives double the figure.

        if self.kernel_arch == "x86":
            ioverhead = 42 * 500 * 2
        else:
            ioverhead = 16 * 500 * 2

        nbpi = int(round(size * 1024 / (file_count + ioverhead)))
        # round the nbpi to the largest power of 2 which is less than or equal
        # to the calculated value
        if nbpi != 0:
            nbpi = int(pow(2, math.floor(math.log(nbpi, 2))))
            self.logger.debug("Calculated number of bytes per inode: %d" % \
                              nbpi)

        # return back to the cwd
        os.chdir(cwd)

        return nbpi

    def calculate_ba_size(self, directory):
        """ class method to calculate the size of the boot archive area

        directory - root of the boot archive
        """
        size = dir_size(directory) / 1024

        self.logger.debug("BA size before padding: %d", size)

        # For writable space, add in the minimum padding size plus whatever
        # was specified explicitly as additional padding size in the manifest.
        # Note: the space needed for the inodes eats away some of this space.
        size = size + (self.MIN_PADDING_SIZE_IN_MB * 1024) + \
               (self.size_pad * 1024)

        self.logger.debug("BA size after padding: %d", size)

        if self.nbpi == 0:
            self.nbpi = self.calculate_nbpi(directory, size)

        self.logger.info("ramdisk will be " + \
                         "%d MB in size " % (size / 1024) + \
                         "with an nbpi of %d" % self.nbpi)
        return size

    def create_ramdisks(self):
        """ class method to create the ramdisks and lofi mount them
        """
        # if the tmp_dir doesn't exist create it
        if not os.path.exists(self.tmp_dir):
            os.makedirs(self.tmp_dir)

        # create Lofi objects and store them in a list to iterate over
        size = self.calculate_ba_size(self.ba_build)
        if self.kernel_arch == "x86":
            ramdisk = os.path.join(self.pkg_img_path,
                                   "platform/i86pc/amd64/boot_archive")
            mountpoint = os.path.join(self.tmp_dir, "x86_lofimnt")
        elif self.kernel_arch == "sparc":
            ramdisk = os.path.join(self.pkg_img_path,
                                   "platform/sun4u/boot_archive")
            mountpoint = os.path.join(self.tmp_dir, "sparc_lofimnt")

            # add specific entries to /etc/system for sparc
            etc_system = os.path.join(self.ba_build, "etc/system")
            with open(etc_system, "a+") as fh:
                fh.write("set root_is_ramdisk=1\n")
                fh.write("set ramdisk_size=%d\n" % size)
                fh.write("set kernel_cage_enable=0\n")

        self.lofi = Lofi(ramdisk, mountpoint, size)
        self.lofi.nbpi = self.nbpi

    def install_bootblock(self, lofi_device):
        """ class method to install the boot blocks for a sparc distribution
        """
        cmd = [cli.INSTALLBOOT,
               os.path.join(self.pkg_img_path,
                            "usr/platform/sun4u/lib/fs/ufs/bootblk"),
               lofi_device]
        run(cmd)

    def sparc_fiocompress(self, mountpoint):
        """ class method to fiocompress majority of the files
            in the boot archive.
            Note: this method only applies to SPARC
        """
        # construct a list of exclusion files and directories
        exclude_dirs = ["usr/kernel"]
        exclude_files = copy.deepcopy(self.uncompressed_files)
        flist = os.path.join(self.ba_build, "boot/solaris/filelist.ramdisk")
        with open(flist, "r") as fh:
            lines = [line.strip() for line in fh.readlines()]
        for line in lines:
            if os.path.isdir(os.path.join(self.ba_build, line)):
                exclude_dirs.append(line)
            elif os.path.isfile(os.path.join(self.ba_build, line)):
                exclude_files.append(line)

        # get the cwd
        cwd = os.getcwd()

        os.chdir(self.ba_build)
        for root, dirs, files in os.walk("."):
            # strip off the leading . or ./
            root = root.lstrip("./")

            # check to see if root is in the exclude_dirs list
            if root in exclude_dirs:
                self.logger.debug("skipping root dir %s due to exclude list",
                                  root)
                continue

            # walk each dir and if the entry is in the exclude_dir list
            # add to to the skip list.  After looking through all
            # directories, remove the ones to be skipped from the dirs list
            skip_dirs = list()
            for d in dirs:
                if os.path.join(root, d) in exclude_dirs:
                    self.logger.debug("adding %s to directory skip list due to"
                                      " exclude list", os.path.join(root, d))
                    skip_dirs.append(d)
            for sd in skip_dirs:
                dirs.remove(sd)
            
            # walk each file and if it's in the skip_list, continue
            for f in files:
                if os.path.join(root, f) in exclude_files:
                    self.logger.debug("skipping file %s due to exclude list",
                                      os.path.join(root, f))
                    continue

                # we have a file that needs to be fiocompressed
                ba_path = os.path.join(self.ba_build, root, f)
                mp_path = os.path.join(mountpoint, root, f)

                # ensure that the file meets the following criteria:
                # - it is a regular file
                # - size > 0
                # is is NOT a hardlink
                statinfo = os.lstat(ba_path)
                if stat.S_ISREG(statinfo.st_mode) and not \
                   statinfo.st_size == 0 and \
                   statinfo.st_nlink < 2:
                    # fiocompress the file
                    cmd = [cli.FIOCOMPRESS, "-mc", ba_path, mp_path]
                    run(cmd)

        # return to the original directory
        os.chdir(cwd)

    def create_archives(self):
        """ class method to walk the list of lofi entries and create the
        archives
        """
        self.logger.info("Populating ramdisks")

        # create the ramdisk and lofi mount it
        self.lofi.create(dry_run=False)

        # create a new TransferCPIOAttr object to copy every file from
        # boot_archive to the lofi mountpoint.
        tr_attr = TransferCPIOAttr("Archive Population")

        # set the transfer src correctly
        tr_attr.src = self.ba_build
        tr_attr.dst = self.lofi.mountpoint
        tr_attr.action = INSTALL
        tr_attr.contents = ["./"]
        tr_attr.execute()

        # remove lost+found so it's not carried along to ZFS by an
        # installer
        if os.path.exists(os.path.join(self.lofi.mountpoint,
                                       "lost+found")):
            os.rmdir(os.path.join(self.lofi.mountpoint, "lost+found"))

        if self.kernel_arch == "sparc":
            # install the boot blocks.
            self.install_bootblock(self.lofi.lofi_device.replace("lofi",
                                   "rlofi"))

            # we can't use the transfer module for all of the files due to
            # needing to use fiocompress which copies the file for us.
            self.sparc_fiocompress(self.lofi.mountpoint)

        # umount the lofi device and release the boot_archive
        self.lofi.destroy(dry_run=False)

        if self.kernel_arch == "x86":
            # use gzip to compress the boot archives on x86
            cmd = [cli.CMD7ZA, "a", "-tgzip",
                   "-mx=%d" % self.comp_level,
                   self.lofi.ramdisk + ".gz", self.lofi.ramdisk]
            run_silent(cmd)

            # move the file into the proper place in the pkg image area
            os.rename(self.lofi.ramdisk + ".gz", self.lofi.ramdisk)
        else:
            # the boot_archive for sun4u/sun4v is combined into a single
            # file: sun4u/boot_archive.
            # create a symlink from sun4v/boot_archive to
            # sun4u/boot_archive
            cwd = os.getcwd()
            os.chdir(os.path.join(self.pkg_img_path, "platform/sun4v"))
            os.symlink("../../platform/sun4u/boot_archive", "boot_archive")
            os.chdir(cwd)

        # chmod the boot_archive file to 0644
        os.chmod(self.lofi.ramdisk, 0644)

    def execute(self, dry_run=False):
        """ Primary execution method used by the Checkpoint parent class.
        dry_run is not used in DC
        """
        self.logger.info("=== Executing Boot Archive Archive Checkpoint ===")

        self.parse_doc()

        # create the ramdisk entries based on platform
        self.create_ramdisks()

        # create, populate, and archive the boot archives
        self.create_archives()