blob: 6f7d73f8cf51e6472b579a2cffc980959aaeaedc [file] [log] [blame]
# Copyright 2015 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 socket
import time
import common
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib.cros.graphite import autotest_es
from autotest_lib.site_utils.lxc import Container
from autotest_lib.site_utils.lxc import config as lxc_config
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
from autotest_lib.site_utils.lxc.cleanup_if_fail import cleanup_if_fail
try:
from chromite.lib import metrics
except ImportError:
metrics = utils.metrics_mock
class ContainerBucket(object):
"""A wrapper class to interact with containers in a specific container path.
"""
def __init__(self, container_path=constants.DEFAULT_CONTAINER_PATH):
"""Initialize a ContainerBucket.
@param container_path: Path to the directory used to store containers.
Default is set to AUTOSERV/container_path in
global config.
"""
self.container_path = os.path.realpath(container_path)
def get_all(self):
"""Get details of all containers.
@return: A dictionary of all containers with detailed attributes,
indexed by container name.
"""
info_collection = lxc.get_container_info(self.container_path)
containers = {}
for info in info_collection:
container = Container(self.container_path, info)
containers[container.name] = container
return containers
def get(self, name):
"""Get a container with matching name.
@param name: Name of the container.
@return: A container object with matching name. Returns None if no
container matches the given name.
"""
return self.get_all().get(name, None)
def exist(self, name):
"""Check if a container exists with the given name.
@param name: Name of the container.
@return: True if the container with the given name exists, otherwise
returns False.
"""
return self.get(name) != None
def destroy_all(self):
"""Destroy all containers, base must be destroyed at the last.
"""
containers = self.get_all().values()
for container in sorted(
containers, key=lambda n: 1 if n.name == constants.BASE else 0):
logging.info('Destroy container %s.', container.name)
container.destroy()
@metrics.SecondsTimerDecorator(
'%s/create_from_base_duration' % constants.STATS_KEY)
def create_from_base(self, name, disable_snapshot_clone=False,
force_cleanup=False):
"""Create a container from the base container.
@param name: Name of the container.
@param disable_snapshot_clone: Set to True to force to clone without
using snapshot clone even if the host supports that.
@param force_cleanup: Force to cleanup existing container.
@return: A Container object for the created container.
@raise ContainerError: If the container already exist.
@raise error.CmdError: If lxc-clone call failed for any reason.
"""
if self.exist(name) and not force_cleanup:
raise error.ContainerError('Container %s already exists.' % name)
use_snapshot = (constants.SUPPORT_SNAPSHOT_CLONE and not
disable_snapshot_clone)
try:
return self.clone_container(path=self.container_path,
name=constants.BASE,
new_path=self.container_path,
new_name=name,
snapshot=use_snapshot,
cleanup=force_cleanup)
except error.CmdError:
if not use_snapshot:
raise
else:
# Snapshot clone failed, retry clone without snapshot.
container = self.clone_container(path=self.container_path,
name=constants.BASE,
new_path=self.container_path,
new_name=name,
snapshot=False,
cleanup=force_cleanup)
# Report metadata about retry success.
autotest_es.post(
use_http=True,
type_str=constants.CONTAINER_CREATE_RETRY_METADB_TYPE,
metadata={'drone': socket.gethostname(),
'name': name,
'success': True})
return container
def clone_container(self, path, name, new_path, new_name, snapshot=False,
cleanup=False):
"""Clone one container from another.
@param path: LXC path for the source container.
@param name: Name of the source container.
@param new_path: LXC path for the cloned container.
@param new_name: Name for the cloned container.
@param snapshot: Whether to snapshot, or create a full clone.
@param cleanup: If a container with the given name and path already
exist, clean it up first.
@return: A Container object for the created container.
@raise ContainerError: If the container already exists.
@raise error.CmdError: If lxc-clone call failed for any reason.
"""
# Cleanup existing container with the given name.
container_folder = os.path.join(new_path, new_name)
if lxc_utils.path_exists(container_folder):
if not cleanup:
raise error.ContainerError('Container %s already exists.' %
new_name)
container = Container(new_path, {'name': name})
try:
container.destroy()
except error.CmdError as e:
# The container could be created in a incompleted state. Delete
# the container folder instead.
logging.warn('Failed to destroy container %s, error: %s',
name, e)
utils.run('sudo rm -rf "%s"' % container_folder)
snapshot_arg = '-s' if snapshot else ''
# overlayfs is the default clone backend storage. However it is not
# supported in Ganeti yet. Use aufs as the alternative.
aufs_arg = '-B aufs' if utils.is_vm() and snapshot else ''
cmd = (('sudo lxc-clone --lxcpath %s --newpath %s '
'--orig %s --new %s %s %s') %
(path, new_path, name, new_name, snapshot_arg, aufs_arg))
utils.run(cmd)
return self.get(new_name)
@cleanup_if_fail()
def setup_base(self, name=constants.BASE, force_delete=False):
"""Setup base container.
@param name: Name of the base container, default to base.
@param force_delete: True to force to delete existing base container.
This action will destroy all running test
containers. Default is set to False.
"""
if not self.container_path:
raise error.ContainerError(
'You must set a valid directory to store containers in '
'global config "AUTOSERV/ container_path".')
if not os.path.exists(self.container_path):
os.makedirs(self.container_path)
base_path = os.path.join(self.container_path, name)
if self.exist(name) and not force_delete:
logging.error(
'Base container already exists. Set force_delete to True '
'to force to re-stage base container. Note that this '
'action will destroy all running test containers')
# Set proper file permission. base container in moblab may have
# owner of not being root. Force to update the folder's owner.
# TODO(dshi): Change root to current user when test container can be
# unprivileged container.
utils.run('sudo chown -R root "%s"' % base_path)
utils.run('sudo chgrp -R root "%s"' % base_path)
return
# Destroy existing base container if exists.
if self.exist(name):
# TODO: We may need to destroy all snapshots created from this base
# container, not all container.
self.destroy_all()
# Download and untar the base container.
tar_path = os.path.join(self.container_path, '%s.tar.xz' % name)
path_to_cleanup = [tar_path, base_path]
for path in path_to_cleanup:
if os.path.exists(path):
utils.run('sudo rm -rf "%s"' % path)
container_url = constants.CONTAINER_BASE_URL_FMT % name
lxc.download_extract(container_url, tar_path, self.container_path)
# Remove the downloaded container tar file.
utils.run('sudo rm "%s"' % tar_path)
# Set proper file permission.
# TODO(dshi): Change root to current user when test container can be
# unprivileged container.
utils.run('sudo chown -R root "%s"' % base_path)
utils.run('sudo chgrp -R root "%s"' % base_path)
# Update container config with container_path from global config.
config_path = os.path.join(base_path, 'config')
utils.run('sudo sed -i "s|container_dir|%s|g" "%s"' %
(self.container_path, config_path))
@metrics.SecondsTimerDecorator(
'%s/setup_test_duration' % constants.STATS_KEY)
@cleanup_if_fail()
def setup_test(self, name, job_id, server_package_url, result_path,
control=None, skip_cleanup=False, job_folder=None,
dut_name=None):
"""Setup test container for the test job to run.
The setup includes:
1. Install autotest_server package from given url.
2. Copy over local shadow_config.ini.
3. Mount local site-packages.
4. Mount test result directory.
TODO(dshi): Setup also needs to include test control file for autoserv
to run in container.
@param name: Name of the container.
@param job_id: Job id for the test job to run in the test container.
@param server_package_url: Url to download autotest_server package.
@param result_path: Directory to be mounted to container to store test
results.
@param control: Path to the control file to run the test job. Default is
set to None.
@param skip_cleanup: Set to True to skip cleanup, used to troubleshoot
container failures.
@param job_folder: Folder name of the job, e.g., 123-debug_user.
@param dut_name: Name of the dut to run test, used as the hostname of
the container. Default is None.
@return: A Container object for the test container.
@raise ContainerError: If container does not exist, or not running.
"""
start_time = time.time()
if not os.path.exists(result_path):
raise error.ContainerError('Result directory does not exist: %s',
result_path)
result_path = os.path.abspath(result_path)
# Save control file to result_path temporarily. The reason is that the
# control file in drone_tmp folder can be deleted during scheduler
# restart. For test not using SSP, the window between test starts and
# control file being picked up by the test is very small (< 2 seconds).
# However, for tests using SSP, it takes around 1 minute before the
# container is setup. If scheduler is restarted during that period, the
# control file will be deleted, and the test will fail.
if control:
control_file_name = os.path.basename(control)
safe_control = os.path.join(result_path, control_file_name)
utils.run('cp %s %s' % (control, safe_control))
# Create test container from the base container.
container = self.create_from_base(name)
# Update the hostname of the test container to be `dut-name`.
# Some TradeFed tests use hostname in test results, which is used to
# group test results in dashboard. The default container name is set to
# be the name of the folder, which is unique (as it is composed of job
# id and timestamp. For better result view, the container's hostname is
# set to be a string containing the dut hostname.
if dut_name:
config_file = os.path.join(container.container_path, name, 'config')
lxc_utsname_setting = (
'lxc.utsname = ' +
(constants.CONTAINER_UTSNAME_FORMAT %
dut_name.replace('.', '-')))
utils.run(
constants.APPEND_CMD_FMT % {'content': lxc_utsname_setting,
'file': config_file})
# Deploy server side package
usr_local_path = os.path.join(container.rootfs, 'usr', 'local')
autotest_pkg_path = os.path.join(usr_local_path,
'autotest_server_package.tar.bz2')
autotest_path = os.path.join(usr_local_path, 'autotest')
# sudo is required so os.makedirs may not work.
utils.run('sudo mkdir -p %s'% usr_local_path)
lxc.download_extract(
server_package_url, autotest_pkg_path, usr_local_path)
deploy_config_manager = lxc_config.DeployConfigManager(container)
deploy_config_manager.deploy_pre_start()
# Copy over control file to run the test job.
if control:
container_drone_temp = os.path.join(autotest_path, 'drone_tmp')
utils.run('sudo mkdir -p %s'% container_drone_temp)
container_control_file = os.path.join(
container_drone_temp, control_file_name)
# Move the control file stored in the result folder to container.
utils.run('sudo mv %s %s' % (safe_control, container_control_file))
if constants.IS_MOBLAB:
site_packages_path = constants.MOBLAB_SITE_PACKAGES
site_packages_container_path = (
constants.MOBLAB_SITE_PACKAGES_CONTAINER[1:])
else:
site_packages_path = os.path.join(common.autotest_dir,
'site-packages')
site_packages_container_path = os.path.join(
lxc_config.CONTAINER_AUTOTEST_DIR, 'site-packages')
mount_entries = [(site_packages_path, site_packages_container_path,
True),
(os.path.join(common.autotest_dir, 'puppylab'),
os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR,
'puppylab'),
True),
(result_path,
os.path.join(constants.RESULT_DIR_FMT % job_folder),
False),
]
for mount_config in deploy_config_manager.mount_configs:
mount_entries.append((mount_config.source, mount_config.target,
mount_config.readonly))
# Update container config to mount directories.
for source, destination, readonly in mount_entries:
container.mount_dir(source, destination, readonly)
# Update file permissions.
# TODO(dshi): crbug.com/459344 Skip following action when test container
# can be unprivileged container.
utils.run('sudo chown -R root "%s"' % autotest_path)
utils.run('sudo chgrp -R root "%s"' % autotest_path)
container.start(name)
deploy_config_manager.deploy_post_start()
container.modify_import_order()
container.verify_autotest_setup(job_folder)
autotest_es.post(use_http=True,
type_str=constants.CONTAINER_CREATE_METADB_TYPE,
metadata={'drone': socket.gethostname(),
'job_id': job_id,
'time_used': time.time() - start_time,
'success': True})
logging.debug('Test container %s is set up.', name)
return container