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