| # 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)) |