blob: b24a745c42c71ff951eb6095fb281c94b23c84ce [file] [log] [blame]
# 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
@param src: An optional source container. If provided, the source
continer is cloned, and the new container will point to the
@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)
'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
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))
# New Zygotes use a predefined template to generate a host path.
self.host_path = os.path.join(
# 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, '' % ro_name)
# Remember mounts so they can be cleaned up in destroy.
self.mounts = []
if exists:
# 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):
if not lxc_utils.path_exists(self.host_path_ro):
# Create the mount point within the container's rootfs.
# Changes within container's rootfs require sudo.'sudo mkdir %s' %
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
logging.debug('Destroying Zygote %s',
super(Zygote, self).destroy(force)
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)
usr_local_path = os.path.join(self.host_path, 'usr', 'local')
with lxc_utils.TempDir(dir=usr_local_path) as tmpdir:
download_tmp = os.path.join(tmpdir,
lxc.download_extract(ssp_url, download_tmp, usr_local_path)
container_ssp_path = os.path.join(
self.attach_run('mkdir -p %s && mount --bind %s %s' %
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.
src = os.path.join(constants.CONTAINER_HOST_DIR,
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)
'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, self.host_path_ro, readonly=True))
source = source_ro
# Mount the directory into the host dir, then from the host dir into the
# destination.
lxc_utils.BindMount.create(source, self.host_path, destination))
container_host_path = os.path.join(constants.CONTAINER_HOST_DIR,
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:
# 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):'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):'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
@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 lxc_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.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.host_path_ro, info.mount_point))