普通文本  |  269行  |  11.81 KB

# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import logging
import os

import common
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.site_utils.lxc import Container
from autotest_lib.site_utils.lxc import constants
from autotest_lib.site_utils.lxc import lxc
from autotest_lib.site_utils.lxc import utils as lxc_utils


class Zygote(Container):
    """A Container that implements post-bringup configuration.
    """

    def __init__(self, container_path, name, attribute_values, src=None,
                 snapshot=False, host_path=None):
        """Initialize an object of LXC container with given attribute values.

        @param container_path: Directory that stores the container.
        @param name: Name of the container.
        @param attribute_values: A dictionary of attribute values for the
                                 container.
        @param src: An optional source container.  If provided, the source
                    continer is cloned, and the new container will point to the
                    clone.
        @param snapshot: Whether or not to create a snapshot clone.  By default,
                         this is false.  If a snapshot is requested and creating
                         a snapshot clone fails, a full clone will be attempted.
        @param host_path: If set to None (the default), a host path will be
                          generated based on constants.DEFAULT_SHARED_HOST_PATH.
                          Otherwise, this can be used to override the host path
                          of the new container, for testing purposes.
        """
        # Check if this is a pre-existing LXC container.  Do this before calling
        # the super ctor, because that triggers container creation.
        exists = lxc.get_container_info(container_path, name=name)

        super(Zygote, self).__init__(container_path, name, attribute_values,
                                     src, snapshot)

        logging.debug(
                'Creating Zygote (lxcpath:%s name:%s)', container_path, name)

        # host_path is a directory within a shared bind-mount, which enables
        # bind-mounts from the host system to be shared with the LXC container.
        if host_path is not None:
            # Allow the host_path to be injected, for testing.
            self.host_path = host_path
        else:
            if exists:
                # Pre-existing Zygotes must have a host path.
                self.host_path = self._find_existing_host_dir()
                if self.host_path is None:
                    raise error.ContainerError(
                            'Container %s has no host path.' %
                            os.path.join(container_path, name))
            else:
                # New Zygotes use a predefined template to generate a host path.
                self.host_path = os.path.join(
                        os.path.realpath(constants.DEFAULT_SHARED_HOST_PATH),
                        self.name)

        # host_path_ro is a directory for holding intermediate mount points,
        # which are necessary when creating read-only bind mounts.  See the
        # mount_dir method for more details.
        #
        # Generate a host_path_ro based on host_path.
        ro_dir, ro_name = os.path.split(self.host_path.rstrip(os.path.sep))
        self.host_path_ro = os.path.join(ro_dir, '%s.ro' % ro_name)

        # Remember mounts so they can be cleaned up in destroy.
        self.mounts = []

        if exists:
            self._find_existing_bind_mounts()
        else:
            # Creating a new Zygote - initialize the host dirs.  Don't use sudo,
            # so that the resulting directories can be accessed by autoserv (for
            # SSP installation, etc).
            if not lxc_utils.path_exists(self.host_path):
                os.makedirs(self.host_path)
            if not lxc_utils.path_exists(self.host_path_ro):
                os.makedirs(self.host_path_ro)

            # Create the mount point within the container's rootfs.
            # Changes within container's rootfs require sudo.
            utils.run('sudo mkdir %s' %
                      os.path.join(self.rootfs,
                                   constants.CONTAINER_HOST_DIR.lstrip(
                                           os.path.sep)))
            self.mount_dir(self.host_path, constants.CONTAINER_HOST_DIR)


    def destroy(self, force=True):
        """Destroy the Zygote.

        This destroys the underlying container (see Container.destroy) and also
        cleans up any host mounts associated with it.

        @param force: Force container destruction even if it's running.  See
                      Container.destroy.
        """
        logging.debug('Destroying Zygote %s', self.name)
        super(Zygote, self).destroy(force)
        self._cleanup_host_mount()


    def install_ssp(self, ssp_url):
        """Downloads and installs the given server package.

        @param ssp_url: The URL of the ssp to download and install.
        """
        # The host dir is mounted directly on /usr/local/autotest within the
        # container.  The SSP structure assumes it gets untarred into the
        # /usr/local directory of the container's rootfs.  In order to unpack
        # with the correct directory structure, create a tmpdir, mount the
        # container's host dir as ./autotest, and unpack the SSP.
        if not self.is_running():
            super(Zygote, self).install_ssp(ssp_url)
            return

        usr_local_path = os.path.join(self.host_path, 'usr', 'local')
        os.makedirs(usr_local_path)

        with lxc_utils.TempDir(dir=usr_local_path) as tmpdir:
            download_tmp = os.path.join(tmpdir,
                                        'autotest_server_package.tar.bz2')
            lxc.download_extract(ssp_url, download_tmp, usr_local_path)

        container_ssp_path = os.path.join(
                constants.CONTAINER_HOST_DIR,
                constants.CONTAINER_AUTOTEST_DIR.lstrip(os.path.sep))
        self.attach_run('mkdir -p %s && mount --bind %s %s' %
                        (constants.CONTAINER_AUTOTEST_DIR,
                         container_ssp_path,
                         constants.CONTAINER_AUTOTEST_DIR))


    def copy(self, host_path, container_path):
        """Copies files into the Zygote.

        @param host_path: Path to the source file/dir to be copied.
        @param container_path: Path to the destination dir (in the container).
        """
        if not self.is_running():
            return super(Zygote, self).copy(host_path, container_path)

        logging.debug('copy %s to %s', host_path, container_path)

        # First copy the files into the host mount, then move them from within
        # the container.
        self._do_copy(src=host_path,
                      dst=os.path.join(self.host_path,
                                       container_path.lstrip(os.path.sep)))

        src = os.path.join(constants.CONTAINER_HOST_DIR,
                           container_path.lstrip(os.path.sep))
        dst = container_path

        # In the container, bind-mount from host path to destination.
        # The mount destination must have the correct type (file vs dir).
        if os.path.isdir(host_path):
            self.attach_run('mkdir -p %s' % dst)
        else:
            self.attach_run(
                'mkdir -p %s && touch %s' % (os.path.dirname(dst), dst))
        self.attach_run('mount --bind %s %s' % (src, dst))


    def mount_dir(self, source, destination, readonly=False):
        """Mount a directory in host to a directory in the container.

        @param source: Directory in host to be mounted.
        @param destination: Directory in container to mount the source directory
        @param readonly: Set to True to make a readonly mount, default is False.
        """
        if not self.is_running():
            return super(Zygote, self).mount_dir(source, destination, readonly)

        # Destination path in container must be absolute.
        if not os.path.isabs(destination):
            destination = os.path.join('/', destination)

        # Create directory in container for mount.
        self.attach_run('mkdir -p %s' % destination)

        # Creating read-only shared bind mounts is a two-stage process.  First,
        # the original file/directory is bind-mounted (with the ro option) to an
        # intermediate location in self.host_path_ro.  Then, the intermediate
        # location is bind-mounted into the shared host dir.
        # Replace the original source with this intermediate read-only mount,
        # then continue.
        if readonly:
            source_ro = os.path.join(self.host_path_ro,
                                     source.lstrip(os.path.sep))
            self.mounts.append(lxc_utils.BindMount.create(
                    source, self.host_path_ro, readonly=True))
            source = source_ro

        # Mount the directory into the host dir, then from the host dir into the
        # destination.
        self.mounts.append(
                lxc_utils.BindMount.create(source, self.host_path, destination))

        container_host_path = os.path.join(constants.CONTAINER_HOST_DIR,
                                           destination.lstrip(os.path.sep))
        self.attach_run('mount --bind %s %s' %
                        (container_host_path, destination))


    def _cleanup_host_mount(self):
        """Unmounts and removes the host dirs for this container."""
        # Clean up all intermediate bind mounts into host_path and host_path_ro.
        for mount in self.mounts:
            mount.cleanup()
        # The SSP and other "real" content gets copied into the host dir.  Use
        # rm -r to clear it out.
        if lxc_utils.path_exists(self.host_path):
            utils.run('sudo rm -r "%s"' % self.host_path)
        # The host_path_ro directory only contains intermediate bind points,
        # which should all have been cleared out.  Use rmdir.
        if lxc_utils.path_exists(self.host_path_ro):
            utils.run('sudo rmdir "%s"' % self.host_path_ro)


    def _find_existing_host_dir(self):
        """Finds the host mounts for a pre-existing Zygote.

        The host directory is passed into the Zygote constructor when creating a
        new Zygote.  However, when a Zygote is instantiated on top of an already
        existing LXC container, it has to reconnect to the existing host
        directory.

        @return: The host-side path to the host dir.
        """
        # Look for the mount that targets the "/host" dir within the container.
        for mount in self._get_lxc_config('lxc.mount.entry'):
            mount_cfg = mount.split(' ')
            if mount_cfg[1] == 'host':
                return mount_cfg[0]
        return None


    def _find_existing_bind_mounts(self):
        """Locates bind mounts associated with an existing container.

        When a Zygote object is instantiated on top of an existing LXC
        container, this method needs to be called so that all the bind-mounts
        associated with the container can be reconstructed.  This enables proper
        cleanup later.
        """
        for info in utils.get_mount_info():
            # Check for bind mounts in the host and host_ro directories, and
            # re-add them to self.mounts.
            if lxc_utils.is_subdir(self.host_path, info.mount_point):
                logging.debug('mount: %s', info.mount_point)
                self.mounts.append(lxc_utils.BindMount.from_existing(
                        self.host_path, info.mount_point))
            elif lxc_utils.is_subdir(self.host_path_ro, info.mount_point):
                logging.debug('mount_ro: %s', info.mount_point)
                self.mounts.append(lxc_utils.BindMount.from_existing(
                        self.host_path_ro, info.mount_point))