Create a new module for lxc code.
Create a new python module for lxc code. Move lxc.py and related code into that
module. Script-style functionality in site_utils/lxc.py and lxc_cleanup.py was
left as-is.
BUG=chromium:720219
TEST=sudo python site_utils/lxc_functional_test.py -v
2017-06-15 15:29:52,700 All tests passed.
TEST=Install autotest-server code onto moblab, run a test.
Test runs successfully.
Change-Id: I7b8400db64594f4d1268a62838ac379e713738c1
Reviewed-on: https://chromium-review.googlesource.com/538127
Commit-Ready: Ben Kwa <kenobi@chromium.org>
Tested-by: Ben Kwa <kenobi@chromium.org>
Reviewed-by: Ben Kwa <kenobi@chromium.org>
diff --git a/server/autoserv b/server/autoserv
index bdc47a9..0a7f520 100755
--- a/server/autoserv
+++ b/server/autoserv
@@ -53,7 +53,7 @@
from autotest_lib.site_utils import job_directories
from autotest_lib.site_utils import job_overhead
from autotest_lib.site_utils import lxc
-from autotest_lib.site_utils import lxc_utils
+from autotest_lib.site_utils.lxc import utils as lxc_utils
from autotest_lib.client.common_lib import pidfile, logging_manager
diff --git a/site_utils/lxc.py b/site_utils/lxc.py
old mode 100644
new mode 100755
index 135a8d5..ed1033c
--- a/site_utils/lxc.py
+++ b/site_utils/lxc.py
@@ -2,14 +2,7 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-"""This module provides some tools to interact with LXC containers, for example:
- 1. Download base container from given GS location, setup the base container.
- 2. Create a snapshot as test container from base container.
- 3. Mount a directory in drone to the test container.
- 4. Run a command in the container and return the output.
- 5. Cleanup, e.g., destroy the container.
-
-This tool can also be used to set up a base container for test. For example,
+"""This tool can be used to set up a base container for test. For example,
python lxc.py -s -p /tmp/container
This command will download and setup base container in directory /tmp/container.
After that command finishes, you can run lxc command to work with the base
@@ -18,1048 +11,12 @@
lxc-attach -P /tmp/container -n base
"""
-
import argparse
import logging
-import os
-import re
-import socket
-import sys
-import tempfile
-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 import global_config
-from autotest_lib.client.common_lib.cros import dev_server
-from autotest_lib.client.common_lib.cros import retry
-from autotest_lib.client.common_lib.cros.graphite import autotest_es
-from autotest_lib.server import utils as server_utils
-from autotest_lib.site_utils import lxc_config
-from autotest_lib.site_utils import lxc_utils
-
-try:
- from chromite.lib import metrics
-except ImportError:
- metrics = utils.metrics_mock
-
-
-config = global_config.global_config
-
-# Name of the base container.
-BASE = config.get_config_value('AUTOSERV', 'container_base_name')
-# Naming convention of test container, e.g., test_300_1422862512_2424, where:
-# 300: The test job ID.
-# 1422862512: The tick when container is created.
-# 2424: The PID of autoserv that starts the container.
-TEST_CONTAINER_NAME_FMT = 'test_%s_%d_%d'
-# Naming convention of the result directory in test container.
-RESULT_DIR_FMT = os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR, 'results',
- '%s')
-# Attributes to retrieve about containers.
-ATTRIBUTES = ['name', 'state']
-
-# Format for mount entry to share a directory in host with container.
-# source is the directory in host, destination is the directory in container.
-# readonly is a binding flag for readonly mount, its value should be `,ro`.
-MOUNT_FMT = ('lxc.mount.entry = %(source)s %(destination)s none '
- 'bind%(readonly)s 0 0')
-SSP_ENABLED = config.get_config_value('AUTOSERV', 'enable_ssp_container',
- type=bool, default=True)
-# url to the folder stores base container.
-CONTAINER_BASE_FOLDER_URL = config.get_config_value('AUTOSERV',
- 'container_base_folder_url')
-CONTAINER_BASE_URL_FMT = '%s/%%s.tar.xz' % CONTAINER_BASE_FOLDER_URL
-CONTAINER_BASE_URL = CONTAINER_BASE_URL_FMT % BASE
-# Default directory used to store LXC containers.
-DEFAULT_CONTAINER_PATH = config.get_config_value('AUTOSERV', 'container_path')
-
-# Path to drone_temp folder in the container, which stores the control file for
-# test job to run.
-CONTROL_TEMP_PATH = os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR, 'drone_tmp')
-
-# Bash command to return the file count in a directory. Test the existence first
-# so the command can return an error code if the directory doesn't exist.
-COUNT_FILE_CMD = '[ -d %(dir)s ] && ls %(dir)s | wc -l'
-
-# Command line to append content to a file
-APPEND_CMD_FMT = ('echo \'%(content)s\' | sudo tee --append %(file)s'
- '> /dev/null')
-
-# Path to site-packates in Moblab
-MOBLAB_SITE_PACKAGES = '/usr/lib64/python2.7/site-packages'
-MOBLAB_SITE_PACKAGES_CONTAINER = '/usr/local/lib/python2.7/dist-packages/'
-
-# Flag to indicate it's running in a Moblab. Due to crbug.com/457496, lxc-ls has
-# different behavior in Moblab.
-IS_MOBLAB = utils.is_moblab()
-
-# TODO(dshi): If we are adding more logic in how lxc should interact with
-# different systems, we should consider code refactoring to use a setting-style
-# object to store following flags mapping to different systems.
-# TODO(crbug.com/464834): Snapshot clone is disabled until Moblab can
-# support overlayfs or aufs, which requires a newer kernel.
-SUPPORT_SNAPSHOT_CLONE = not IS_MOBLAB
-
-# Number of seconds to wait for network to be up in a container.
-NETWORK_INIT_TIMEOUT = 300
-# Network bring up is slower in Moblab.
-NETWORK_INIT_CHECK_INTERVAL = 2 if IS_MOBLAB else 0.1
-
-# Number of seconds to download files from devserver. We chose a timeout that
-# is on the same order as the permitted CTS runtime for normal jobs (1h). In
-# principle we should not retry timeouts as they indicate server/network
-# overload, but we may be tempted to retry for other failures.
-DEVSERVER_CALL_TIMEOUT = 3600
-# Number of retries to download files from devserver. There is no point in
-# having more than one retry for a file download.
-DEVSERVER_CALL_RETRY = 2
-# Average delay before attempting a retry to download from devserver. This
-# value needs to be large enough to allow an overloaded server/network to
-# calm down even in the face of retries.
-DEVSERVER_CALL_DELAY = 600
-
-# Type string for container related metadata.
-CONTAINER_CREATE_METADB_TYPE = 'container_create'
-CONTAINER_CREATE_RETRY_METADB_TYPE = 'container_create_retry'
-CONTAINER_RUN_TEST_METADB_TYPE = 'container_run_test'
-
-# The container's hostname MUST start with `test-` or `test_`. DHCP server in
-# MobLab uses that prefix to determine the lease time. Note that `test_` is not
-# a valid hostname as hostnames cannot contain underscores. Work is underway to
-# migrate to `test-`. See crbug/726131.
-CONTAINER_UTSNAME_FORMAT = 'test-%s'
-
-STATS_KEY = 'chromeos/autotest/lxc'
-
-
-def _get_container_info_moblab(container_path, **filters):
- """Get a collection of container information in the given container path
- in a Moblab.
-
- TODO(crbug.com/457496): remove this method once python 3 can be installed
- in Moblab and lxc-ls command can use python 3 code.
-
- When running in Moblab, lxc-ls behaves differently from a server with python
- 3 installed:
- 1. lxc-ls returns a list of containers installed under /etc/lxc, the default
- lxc container directory.
- 2. lxc-ls --active lists all active containers, regardless where the
- container is located.
- For such differences, we have to special case Moblab to make the behavior
- close to a server with python 3 installed. That is,
- 1. List only containers in a given folder.
- 2. Assume all active containers have state of RUNNING.
-
- @param container_path: Path to look for containers.
- @param filters: Key value to filter the containers, e.g., name='base'
-
- @return: A list of dictionaries that each dictionary has the information of
- a container. The keys are defined in ATTRIBUTES.
- """
- info_collection = []
- active_containers = utils.run('sudo lxc-ls --active').stdout.split()
- name_filter = filters.get('name', None)
- state_filter = filters.get('state', None)
- if filters and set(filters.keys()) - set(['name', 'state']):
- raise error.ContainerError('When running in Moblab, container list '
- 'filter only supports name and state.')
-
- for name in os.listdir(container_path):
- # Skip all files and folders without rootfs subfolder.
- if (os.path.isfile(os.path.join(container_path, name)) or
- not lxc_utils.path_exists(os.path.join(container_path, name,
- 'rootfs'))):
- continue
- info = {'name': name,
- 'state': 'RUNNING' if name in active_containers else 'STOPPED'
- }
- if ((name_filter and name_filter != info['name']) or
- (state_filter and state_filter != info['state'])):
- continue
-
- info_collection.append(info)
- return info_collection
-
-
-def get_container_info(container_path, **filters):
- """Get a collection of container information in the given container path.
-
- This method parse the output of lxc-ls to get a list of container
- information. The lxc-ls command output looks like:
- NAME STATE IPV4 IPV6 AUTOSTART PID MEMORY RAM SWAP
- --------------------------------------------------------------------------
- base STOPPED - - NO - - - -
- test_123 RUNNING 10.0.3.27 - NO 8359 6.28MB 6.28MB 0.0MB
-
- @param container_path: Path to look for containers.
- @param filters: Key value to filter the containers, e.g., name='base'
-
- @return: A list of dictionaries that each dictionary has the information of
- a container. The keys are defined in ATTRIBUTES.
- """
- if IS_MOBLAB:
- return _get_container_info_moblab(container_path, **filters)
-
- cmd = 'sudo lxc-ls -P %s -f -F %s' % (os.path.realpath(container_path),
- ','.join(ATTRIBUTES))
- output = utils.run(cmd).stdout
- info_collection = []
-
- for line in output.splitlines()[1:]:
- # Only LXC 1.x has the second line of '-' as a separator.
- if line.startswith('------'):
- continue
- info_collection.append(dict(zip(ATTRIBUTES, line.split())))
- if filters:
- filtered_collection = []
- for key, value in filters.iteritems():
- for info in info_collection:
- if key in info and info[key] == value:
- filtered_collection.append(info)
- info_collection = filtered_collection
- return info_collection
-
-
-def cleanup_if_fail():
- """Decorator to do cleanup if container fails to be set up.
- """
- def deco_cleanup_if_fail(func):
- """Wrapper for the decorator.
-
- @param func: Function to be called.
- """
- def func_cleanup_if_fail(*args, **kwargs):
- """Decorator to do cleanup if container fails to be set up.
-
- The first argument must be a ContainerBucket object, which can be
- used to retrieve the container object by name.
-
- @param func: function to be called.
- @param args: arguments for function to be called.
- @param kwargs: keyword arguments for function to be called.
- """
- bucket = args[0]
- name = utils.get_function_arg_value(func, 'name', args, kwargs)
- try:
- skip_cleanup = utils.get_function_arg_value(
- func, 'skip_cleanup', args, kwargs)
- except (KeyError, ValueError):
- skip_cleanup = False
- try:
- return func(*args, **kwargs)
- except:
- exc_info = sys.exc_info()
- try:
- container = bucket.get(name)
- if container and not skip_cleanup:
- container.destroy()
- except error.CmdError as e:
- logging.error(e)
-
- try:
- job_id = utils.get_function_arg_value(
- func, 'job_id', args, kwargs)
- except (KeyError, ValueError):
- job_id = ''
- metadata={'drone': socket.gethostname(),
- 'job_id': job_id,
- 'success': False}
- # Record all args if job_id is not available.
- if not job_id:
- metadata['args'] = str(args)
- if kwargs:
- metadata.update(kwargs)
- autotest_es.post(use_http=True,
- type_str=CONTAINER_CREATE_METADB_TYPE,
- metadata=metadata)
-
- # Raise the cached exception with original backtrace.
- raise exc_info[0], exc_info[1], exc_info[2]
- return func_cleanup_if_fail
- return deco_cleanup_if_fail
-
-
-# Make sure retries only happen in the non-timeout case.
-@retry.retry((error.CmdError),
- blacklist=[error.CmdTimeoutError],
- timeout_min=DEVSERVER_CALL_TIMEOUT * DEVSERVER_CALL_RETRY / 60,
- delay_sec=DEVSERVER_CALL_DELAY)
-def download_extract(url, target, extract_dir):
- """Download the file from given url and save it to the target, then extract.
-
- @param url: Url to download the file.
- @param target: Path of the file to save to.
- @param extract_dir: Directory to extract the content of the file to.
- """
- remote_url = dev_server.DevServer.get_server_url(url)
- # TODO(xixuan): Better to only ssh to devservers in lab, and continue using
- # wget for ganeti devservers.
- if remote_url in dev_server.ImageServerBase.servers():
- # This can be run in multiple threads, pick a unique tmp_file.name.
- with tempfile.NamedTemporaryFile(prefix=os.path.basename(target) + '_',
- delete=False) as tmp_file:
- dev_server.ImageServerBase.download_file(
- url, tmp_file.name, timeout=DEVSERVER_CALL_TIMEOUT)
- utils.run('sudo mv %s %s' % (tmp_file.name, target))
- else:
- # We do not want to retry on CmdTimeoutError but still retry on
- # CmdError. Hence we can't use wget --timeout=...
- utils.run('sudo wget -nv %s -O %s' % (url, target),
- stderr_tee=utils.TEE_TO_LOGS, timeout=DEVSERVER_CALL_TIMEOUT)
-
- utils.run('sudo tar -xvf %s -C %s' % (target, extract_dir))
-
-
-def install_package_precheck(packages):
- """If SSP is not enabled or the test is running in chroot (using test_that),
- packages installation should be skipped.
-
- The check does not raise exception so tests started by test_that or running
- in an Autotest setup with SSP disabled can continue. That assume the running
- environment, chroot or a machine, has the desired packages installed
- already.
-
- @param packages: A list of names of the packages to install.
-
- @return: True if package installation can continue. False if it should be
- skipped.
-
- """
- if not SSP_ENABLED and not utils.is_in_container():
- logging.info('Server-side packaging is not enabled. Install package %s '
- 'is skipped.', packages)
- return False
-
- if server_utils.is_inside_chroot():
- logging.info('Test is running inside chroot. Install package %s is '
- 'skipped.', packages)
- return False
-
- if not utils.is_in_container():
- raise error.ContainerError('Package installation is only supported '
- 'when test is running inside container.')
-
- return True
-
-
-@metrics.SecondsTimerDecorator('%s/install_packages_duration' % STATS_KEY)
-@retry.retry(error.CmdError, timeout_min=30)
-def install_packages(packages=[], python_packages=[], force_latest=False):
- """Install the given package inside container.
-
- !!! WARNING !!!
- This call may introduce several minutes of delay in test run. The best way
- to avoid such delay is to update the base container used for the test run.
- File a bug for infra deputy to update the base container with the new
- package a test requires.
-
- @param packages: A list of names of the packages to install.
- @param python_packages: A list of names of the python packages to install
- using pip.
- @param force_latest: True to force to install the latest version of the
- package. Default to False, which means skip installing
- the package if it's installed already, even with an old
- version.
-
- @raise error.ContainerError: If package is attempted to be installed outside
- a container.
- @raise error.CmdError: If the package doesn't exist or failed to install.
-
- """
- if not install_package_precheck(packages or python_packages):
- return
-
- # If force_latest is False, only install packages that are not already
- # installed.
- if not force_latest:
- packages = [p for p in packages if not utils.is_package_installed(p)]
- python_packages = [p for p in python_packages
- if not utils.is_python_package_installed(p)]
- if not packages and not python_packages:
- logging.debug('All packages are installed already, skip reinstall.')
- return
-
- # Always run apt-get update before installing any container. The base
- # container may have outdated cache.
- utils.run('sudo apt-get update')
- # Make sure the lists are not None for iteration.
- packages = [] if not packages else packages
- if python_packages:
- packages.extend(['python-pip', 'python-dev'])
- if packages:
- utils.run('sudo apt-get install %s -y --force-yes' % ' '.join(packages))
- logging.debug('Packages are installed: %s.', packages)
-
- target_setting = ''
- # For containers running in Moblab, /usr/local/lib/python2.7/dist-packages/
- # is a readonly mount from the host. Therefore, new python modules have to
- # be installed in /usr/lib/python2.7/dist-packages/
- # Containers created in Moblab does not have autotest/site-packages folder.
- if not os.path.exists('/usr/local/autotest/site-packages'):
- target_setting = '--target="/usr/lib/python2.7/dist-packages/"'
- if python_packages:
- utils.run('sudo pip install %s %s' % (target_setting,
- ' '.join(python_packages)))
- logging.debug('Python packages are installed: %s.', python_packages)
-
-
-@retry.retry(error.CmdError, timeout_min=20)
-def install_package(package):
- """Install the given package inside container.
-
- This function is kept for backwards compatibility reason. New code should
- use function install_packages for better performance.
-
- @param package: Name of the package to install.
-
- @raise error.ContainerError: If package is attempted to be installed outside
- a container.
- @raise error.CmdError: If the package doesn't exist or failed to install.
-
- """
- logging.warn('This function is obsoleted, please use install_packages '
- 'instead.')
- install_packages(packages=[package])
-
-
-@retry.retry(error.CmdError, timeout_min=20)
-def install_python_package(package):
- """Install the given python package inside container using pip.
-
- This function is kept for backwards compatibility reason. New code should
- use function install_packages for better performance.
-
- @param package: Name of the python package to install.
-
- @raise error.CmdError: If the package doesn't exist or failed to install.
- """
- logging.warn('This function is obsoleted, please use install_packages '
- 'instead.')
- install_packages(python_packages=[package])
-
-
-class Container(object):
- """A wrapper class of an LXC container.
-
- The wrapper class provides methods to interact with a container, e.g.,
- start, stop, destroy, run a command. It also has attributes of the
- container, including:
- name: Name of the container.
- state: State of the container, e.g., ABORTING, RUNNING, STARTING, STOPPED,
- or STOPPING.
-
- lxc-ls can also collect other attributes of a container including:
- ipv4: IP address for IPv4.
- ipv6: IP address for IPv6.
- autostart: If the container will autostart at system boot.
- pid: Process ID of the container.
- memory: Memory used by the container, as a string, e.g., "6.2MB"
- ram: Physical ram used by the container, as a string, e.g., "6.2MB"
- swap: swap used by the container, as a string, e.g., "1.0MB"
-
- For performance reason, such info is not collected for now.
-
- The attributes available are defined in ATTRIBUTES constant.
- """
-
- def __init__(self, container_path, attribute_values):
- """Initialize an object of LXC container with given attribute values.
-
- @param container_path: Directory that stores the container.
- @param attribute_values: A dictionary of attribute values for the
- container.
- """
- self.container_path = os.path.realpath(container_path)
- # Path to the rootfs of the container. This will be initialized when
- # property rootfs is retrieved.
- self._rootfs = None
- for attribute, value in attribute_values.iteritems():
- setattr(self, attribute, value)
-
-
- def refresh_status(self):
- """Refresh the status information of the container.
- """
- containers = get_container_info(self.container_path, name=self.name)
- if not containers:
- raise error.ContainerError(
- 'No container found in directory %s with name of %s.' %
- self.container_path, self.name)
- attribute_values = containers[0]
- for attribute, value in attribute_values.iteritems():
- setattr(self, attribute, value)
-
-
- @property
- def rootfs(self):
- """Path to the rootfs of the container.
-
- This property returns the path to the rootfs of the container, that is,
- the folder where the container stores its local files. It reads the
- attribute lxc.rootfs from the config file of the container, e.g.,
- lxc.rootfs = /usr/local/autotest/containers/t4/rootfs
- If the container is created with snapshot, the rootfs is a chain of
- folders, separated by `:` and ordered by how the snapshot is created,
- e.g.,
- lxc.rootfs = overlayfs:/usr/local/autotest/containers/base/rootfs:
- /usr/local/autotest/containers/t4_s/delta0
- This function returns the last folder in the chain, in above example,
- that is `/usr/local/autotest/containers/t4_s/delta0`
-
- Files in the rootfs will be accessible directly within container. For
- example, a folder in host "[rootfs]/usr/local/file1", can be accessed
- inside container by path "/usr/local/file1". Note that symlink in the
- host can not across host/container boundary, instead, directory mount
- should be used, refer to function mount_dir.
-
- @return: Path to the rootfs of the container.
- """
- if not self._rootfs:
- cmd = ('sudo lxc-info -P %s -n %s -c lxc.rootfs' %
- (self.container_path, self.name))
- lxc_rootfs_config = utils.run(cmd).stdout.strip()
- match = re.match('lxc.rootfs = (.*)', lxc_rootfs_config)
- if not match:
- raise error.ContainerError(
- 'Failed to locate rootfs for container %s. lxc.rootfs '
- 'in the container config file is %s' %
- (self.name, lxc_rootfs_config))
- lxc_rootfs = match.group(1)
- self.clone_from_snapshot = ':' in lxc_rootfs
- if self.clone_from_snapshot:
- self._rootfs = lxc_rootfs.split(':')[-1]
- else:
- self._rootfs = lxc_rootfs
- return self._rootfs
-
-
- def attach_run(self, command, bash=True):
- """Attach to a given container and run the given command.
-
- @param command: Command to run in the container.
- @param bash: Run the command through bash -c "command". This allows
- pipes to be used in command. Default is set to True.
-
- @return: The output of the command.
-
- @raise error.CmdError: If container does not exist, or not running.
- """
- cmd = 'sudo lxc-attach -P %s -n %s' % (self.container_path, self.name)
- if bash and not command.startswith('bash -c'):
- command = 'bash -c "%s"' % utils.sh_escape(command)
- cmd += ' -- %s' % command
- # TODO(dshi): crbug.com/459344 Set sudo to default to False when test
- # container can be unprivileged container.
- return utils.run(cmd)
-
-
- def is_network_up(self):
- """Check if network is up in the container by curl base container url.
-
- @return: True if the network is up, otherwise False.
- """
- try:
- self.attach_run('curl --head %s' % CONTAINER_BASE_URL)
- return True
- except error.CmdError as e:
- logging.debug(e)
- return False
-
-
- @metrics.SecondsTimerDecorator('%s/container_start_duration' % STATS_KEY)
- def start(self, wait_for_network=True):
- """Start the container.
-
- @param wait_for_network: True to wait for network to be up. Default is
- set to True.
-
- @raise ContainerError: If container does not exist, or fails to start.
- """
- cmd = 'sudo lxc-start -P %s -n %s -d' % (self.container_path, self.name)
- output = utils.run(cmd).stdout
- self.refresh_status()
- if self.state != 'RUNNING':
- raise error.ContainerError(
- 'Container %s failed to start. lxc command output:\n%s' %
- (os.path.join(self.container_path, self.name),
- output))
-
- if wait_for_network:
- logging.debug('Wait for network to be up.')
- start_time = time.time()
- utils.poll_for_condition(condition=self.is_network_up,
- timeout=NETWORK_INIT_TIMEOUT,
- sleep_interval=NETWORK_INIT_CHECK_INTERVAL)
- logging.debug('Network is up after %.2f seconds.',
- time.time() - start_time)
-
-
- @metrics.SecondsTimerDecorator('%s/container_stop_duration' % STATS_KEY)
- def stop(self):
- """Stop the container.
-
- @raise ContainerError: If container does not exist, or fails to start.
- """
- cmd = 'sudo lxc-stop -P %s -n %s' % (self.container_path, self.name)
- output = utils.run(cmd).stdout
- self.refresh_status()
- if self.state != 'STOPPED':
- raise error.ContainerError(
- 'Container %s failed to be stopped. lxc command output:\n'
- '%s' % (os.path.join(self.container_path, self.name),
- output))
-
-
- @metrics.SecondsTimerDecorator('%s/container_destroy_duration' % STATS_KEY)
- def destroy(self, force=True):
- """Destroy the container.
-
- @param force: Set to True to force to destroy the container even if it's
- running. This is faster than stop a container first then
- try to destroy it. Default is set to True.
-
- @raise ContainerError: If container does not exist or failed to destroy
- the container.
- """
- cmd = 'sudo lxc-destroy -P %s -n %s' % (self.container_path,
- self.name)
- if force:
- cmd += ' -f'
- utils.run(cmd)
-
-
- 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.
- """
- # Destination path in container must be relative.
- destination = destination.lstrip('/')
- # Create directory in container for mount.
- utils.run('sudo mkdir -p %s' % os.path.join(self.rootfs, destination))
- config_file = os.path.join(self.container_path, self.name, 'config')
- mount = MOUNT_FMT % {'source': source,
- 'destination': destination,
- 'readonly': ',ro' if readonly else ''}
- utils.run(APPEND_CMD_FMT % {'content': mount, 'file': config_file})
-
-
- def verify_autotest_setup(self, job_folder):
- """Verify autotest code is set up properly in the container.
-
- @param job_folder: Name of the job result folder.
-
- @raise ContainerError: If autotest code is not set up properly.
- """
- # Test autotest code is setup by verifying a list of
- # (directory, minimum file count)
- if IS_MOBLAB:
- site_packages_path = MOBLAB_SITE_PACKAGES_CONTAINER
- else:
- site_packages_path = os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR,
- 'site-packages')
- directories_to_check = [
- (lxc_config.CONTAINER_AUTOTEST_DIR, 3),
- (RESULT_DIR_FMT % job_folder, 0),
- (site_packages_path, 3)]
- for directory, count in directories_to_check:
- result = self.attach_run(command=(COUNT_FILE_CMD %
- {'dir': directory})).stdout
- logging.debug('%s entries in %s.', int(result), directory)
- if int(result) < count:
- raise error.ContainerError('%s is not properly set up.' %
- directory)
- # lxc-attach and run command does not run in shell, thus .bashrc is not
- # loaded. Following command creates a symlink in /usr/bin/ for gsutil
- # if it's installed.
- # TODO(dshi): Remove this code after lab container is updated with
- # gsutil installed in /usr/bin/
- self.attach_run('test -f /root/gsutil/gsutil && '
- 'ln -s /root/gsutil/gsutil /usr/bin/gsutil || true')
-
-
- def modify_import_order(self):
- """Swap the python import order of lib and local/lib.
-
- In Moblab, the host's python modules located in
- /usr/lib64/python2.7/site-packages is mounted to following folder inside
- container: /usr/local/lib/python2.7/dist-packages/. The modules include
- an old version of requests module, which is used in autotest
- site-packages. For test, the module is only used in
- dev_server/symbolicate_dump for requests.call and requests.codes.OK.
- When pip is installed inside the container, it installs requests module
- with version of 2.2.1 in /usr/lib/python2.7/dist-packages/. The version
- is newer than the one used in autotest site-packages, but not the latest
- either.
- According to /usr/lib/python2.7/site.py, modules in /usr/local/lib are
- imported before the ones in /usr/lib. That leads to pip to use the older
- version of requests (0.11.2), and it will fail. On the other hand,
- requests module 2.2.1 can't be installed in CrOS (refer to CL:265759),
- and higher version of requests module can't work with pip.
- The only fix to resolve this is to switch the import order, so modules
- in /usr/lib can be imported before /usr/local/lib.
- """
- site_module = '/usr/lib/python2.7/site.py'
- self.attach_run("sed -i ':a;N;$!ba;s/\"local\/lib\",\\n/"
- "\"lib_placeholder\",\\n/g' %s" % site_module)
- self.attach_run("sed -i ':a;N;$!ba;s/\"lib\",\\n/"
- "\"local\/lib\",\\n/g' %s" % site_module)
- self.attach_run('sed -i "s/lib_placeholder/lib/g" %s' %
- site_module)
-
-
-
-class ContainerBucket(object):
- """A wrapper class to interact with containers in a specific container path.
- """
-
- def __init__(self, container_path=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 = 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 == BASE else 0):
- logging.info('Destroy container %s.', container.name)
- container.destroy()
-
-
- @metrics.SecondsTimerDecorator('%s/create_from_base_duration' % 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 = SUPPORT_SNAPSHOT_CLONE and not disable_snapshot_clone
-
- try:
- return self.clone_container(path=self.container_path,
- name=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=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=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=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 = CONTAINER_BASE_URL_FMT % name
- 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' % 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 = ' +
- CONTAINER_UTSNAME_FORMAT % dut_name.replace('.', '-'))
- utils.run(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)
-
- 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 IS_MOBLAB:
- site_packages_path = MOBLAB_SITE_PACKAGES
- site_packages_container_path = 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(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=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
+from autotest_lib.site_utils import lxc
def parse_options():
@@ -1073,14 +30,14 @@
help='Set up base container.')
parser.add_argument('-p', '--path', type=str,
help='Directory to store the container.',
- default=DEFAULT_CONTAINER_PATH)
+ default=lxc.DEFAULT_CONTAINER_PATH)
parser.add_argument('-f', '--force_delete', action='store_true',
default=False,
help=('Force to delete existing containers and rebuild '
'base containers.'))
parser.add_argument('-n', '--name', type=str,
help='Name of the base container.',
- default=BASE)
+ default=lxc.BASE)
options = parser.parse_args()
if not options.setup and not options.force_delete:
raise argparse.ArgumentError(
@@ -1100,7 +57,7 @@
utils.run('sudo true')
options = parse_options()
- bucket = ContainerBucket(container_path=options.path)
+ bucket = lxc.ContainerBucket(container_path=options.path)
if options.setup:
bucket.setup_base(name=options.name, force_delete=options.force_delete)
elif options.force_delete:
diff --git a/site_utils/lxc/__init__.py b/site_utils/lxc/__init__.py
new file mode 100644
index 0000000..8ee0426
--- /dev/null
+++ b/site_utils/lxc/__init__.py
@@ -0,0 +1,18 @@
+# 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.
+
+"""This module provides some tools to interact with LXC containers, for example:
+ 1. Download base container from given GS location, setup the base container.
+ 2. Create a snapshot as test container from base container.
+ 3. Mount a directory in drone to the test container.
+ 4. Run a command in the container and return the output.
+ 5. Cleanup, e.g., destroy the container.
+"""
+
+from constants import *
+from container import Container
+from container_bucket import ContainerBucket
+from lxc import install_package
+from lxc import install_packages
+from lxc import install_python_package
diff --git a/site_utils/lxc/cleanup_if_fail.py b/site_utils/lxc/cleanup_if_fail.py
new file mode 100644
index 0000000..14ef874
--- /dev/null
+++ b/site_utils/lxc/cleanup_if_fail.py
@@ -0,0 +1,73 @@
+# 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 socket
+import sys
+
+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 constants
+
+
+def cleanup_if_fail():
+ """Decorator to do cleanup if container fails to be set up.
+ """
+ def deco_cleanup_if_fail(func):
+ """Wrapper for the decorator.
+
+ @param func: Function to be called.
+ """
+ def func_cleanup_if_fail(*args, **kwargs):
+ """Decorator to do cleanup if container fails to be set up.
+
+ The first argument must be a ContainerBucket object, which can be
+ used to retrieve the container object by name.
+
+ @param func: function to be called.
+ @param args: arguments for function to be called.
+ @param kwargs: keyword arguments for function to be called.
+ """
+ bucket = args[0]
+ name = utils.get_function_arg_value(func, 'name', args, kwargs)
+ try:
+ skip_cleanup = utils.get_function_arg_value(
+ func, 'skip_cleanup', args, kwargs)
+ except (KeyError, ValueError):
+ skip_cleanup = False
+ try:
+ return func(*args, **kwargs)
+ except:
+ exc_info = sys.exc_info()
+ try:
+ container = bucket.get(name)
+ if container and not skip_cleanup:
+ container.destroy()
+ except error.CmdError as e:
+ logging.error(e)
+
+ try:
+ job_id = utils.get_function_arg_value(
+ func, 'job_id', args, kwargs)
+ except (KeyError, ValueError):
+ job_id = ''
+ metadata={'drone': socket.gethostname(),
+ 'job_id': job_id,
+ 'success': False}
+ # Record all args if job_id is not available.
+ if not job_id:
+ metadata['args'] = str(args)
+ if kwargs:
+ metadata.update(kwargs)
+ autotest_es.post(
+ use_http=True,
+ type_str=constants.CONTAINER_CREATE_METADB_TYPE,
+ metadata=metadata)
+
+ # Raise the cached exception with original backtrace.
+ raise exc_info[0], exc_info[1], exc_info[2]
+ return func_cleanup_if_fail
+ return deco_cleanup_if_fail
diff --git a/site_utils/lxc/common.py b/site_utils/lxc/common.py
new file mode 100644
index 0000000..41607e1
--- /dev/null
+++ b/site_utils/lxc/common.py
@@ -0,0 +1,8 @@
+import os, sys
+dirname = os.path.dirname(sys.modules[__name__].__file__)
+autotest_dir = os.path.abspath(os.path.join(dirname, "..", ".."))
+client_dir = os.path.join(autotest_dir, "client")
+sys.path.insert(0, client_dir)
+import setup_modules
+sys.path.pop(0)
+setup_modules.setup(base_path=autotest_dir, root_module_name="autotest_lib")
diff --git a/site_utils/lxc_config.py b/site_utils/lxc/config.py
similarity index 99%
rename from site_utils/lxc_config.py
rename to site_utils/lxc/config.py
index 8b02e77..34ae2f9 100644
--- a/site_utils/lxc_config.py
+++ b/site_utils/lxc/config.py
@@ -95,7 +95,7 @@
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import global_config
from autotest_lib.client.common_lib import utils
-from autotest_lib.site_utils import lxc_utils
+from autotest_lib.site_utils.lxc import utils as lxc_utils
config = global_config.global_config
diff --git a/site_utils/lxc/constants.py b/site_utils/lxc/constants.py
new file mode 100644
index 0000000..910bf8e
--- /dev/null
+++ b/site_utils/lxc/constants.py
@@ -0,0 +1,98 @@
+# 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 os
+
+import common
+from autotest_lib.client.bin import utils as common_utils
+from autotest_lib.client.common_lib.global_config import global_config
+from autotest_lib.site_utils.lxc import config as lxc_config
+
+
+# Name of the base container.
+BASE = global_config.get_config_value('AUTOSERV', 'container_base_name')
+# Naming convention of test container, e.g., test_300_1422862512_2424, where:
+# 300: The test job ID.
+# 1422862512: The tick when container is created.
+# 2424: The PID of autoserv that starts the container.
+TEST_CONTAINER_NAME_FMT = 'test_%s_%d_%d'
+# Naming convention of the result directory in test container.
+RESULT_DIR_FMT = os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR, 'results',
+ '%s')
+# Attributes to retrieve about containers.
+ATTRIBUTES = ['name', 'state']
+
+# Format for mount entry to share a directory in host with container.
+# source is the directory in host, destination is the directory in container.
+# readonly is a binding flag for readonly mount, its value should be `,ro`.
+MOUNT_FMT = ('lxc.mount.entry = %(source)s %(destination)s none '
+ 'bind%(readonly)s 0 0')
+SSP_ENABLED = global_config.get_config_value('AUTOSERV', 'enable_ssp_container',
+ type=bool, default=True)
+# url to the folder stores base container.
+CONTAINER_BASE_FOLDER_URL = global_config.get_config_value('AUTOSERV',
+ 'container_base_folder_url')
+CONTAINER_BASE_URL_FMT = '%s/%%s.tar.xz' % CONTAINER_BASE_FOLDER_URL
+CONTAINER_BASE_URL = CONTAINER_BASE_URL_FMT % BASE
+# Default directory used to store LXC containers.
+DEFAULT_CONTAINER_PATH = global_config.get_config_value('AUTOSERV',
+ 'container_path')
+
+# Path to drone_temp folder in the container, which stores the control file for
+# test job to run.
+CONTROL_TEMP_PATH = os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR, 'drone_tmp')
+
+# Bash command to return the file count in a directory. Test the existence first
+# so the command can return an error code if the directory doesn't exist.
+COUNT_FILE_CMD = '[ -d %(dir)s ] && ls %(dir)s | wc -l'
+
+# Command line to append content to a file
+APPEND_CMD_FMT = ('echo \'%(content)s\' | sudo tee --append %(file)s'
+ '> /dev/null')
+
+# Path to site-packates in Moblab
+MOBLAB_SITE_PACKAGES = '/usr/lib64/python2.7/site-packages'
+MOBLAB_SITE_PACKAGES_CONTAINER = '/usr/local/lib/python2.7/dist-packages/'
+
+# Flag to indicate it's running in a Moblab. Due to crbug.com/457496, lxc-ls has
+# different behavior in Moblab.
+IS_MOBLAB = common_utils.is_moblab()
+
+# TODO(dshi): If we are adding more logic in how lxc should interact with
+# different systems, we should consider code refactoring to use a setting-style
+# object to store following flags mapping to different systems.
+# TODO(crbug.com/464834): Snapshot clone is disabled until Moblab can
+# support overlayfs or aufs, which requires a newer kernel.
+SUPPORT_SNAPSHOT_CLONE = not IS_MOBLAB
+
+# Number of seconds to wait for network to be up in a container.
+NETWORK_INIT_TIMEOUT = 300
+# Network bring up is slower in Moblab.
+NETWORK_INIT_CHECK_INTERVAL = 2 if IS_MOBLAB else 0.1
+
+# Number of seconds to download files from devserver. We chose a timeout that
+# is on the same order as the permitted CTS runtime for normal jobs (1h). In
+# principle we should not retry timeouts as they indicate server/network
+# overload, but we may be tempted to retry for other failures.
+DEVSERVER_CALL_TIMEOUT = 3600
+# Number of retries to download files from devserver. There is no point in
+# having more than one retry for a file download.
+DEVSERVER_CALL_RETRY = 2
+# Average delay before attempting a retry to download from devserver. This
+# value needs to be large enough to allow an overloaded server/network to
+# calm down even in the face of retries.
+DEVSERVER_CALL_DELAY = 600
+
+# Type string for container related metadata.
+CONTAINER_CREATE_METADB_TYPE = 'container_create'
+CONTAINER_CREATE_RETRY_METADB_TYPE = 'container_create_retry'
+CONTAINER_RUN_TEST_METADB_TYPE = 'container_run_test'
+
+# The container's hostname MUST start with `test-` or `test_`. DHCP server in
+# MobLab uses that prefix to determine the lease time. Note that `test_` is not
+# a valid hostname as hostnames cannot contain underscores. Work is underway to
+# migrate to `test-`. See crbug/726131.
+CONTAINER_UTSNAME_FORMAT = 'test-%s'
+
+STATS_KEY = 'chromeos/autotest/lxc'
diff --git a/site_utils/lxc/container.py b/site_utils/lxc/container.py
new file mode 100644
index 0000000..a4003e2
--- /dev/null
+++ b/site_utils/lxc/container.py
@@ -0,0 +1,297 @@
+# 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 re
+import time
+
+import common
+from autotest_lib.client.bin import utils
+from autotest_lib.client.common_lib import error
+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
+
+try:
+ from chromite.lib import metrics
+except ImportError:
+ metrics = utils.metrics_mock
+
+
+class Container(object):
+ """A wrapper class of an LXC container.
+
+ The wrapper class provides methods to interact with a container, e.g.,
+ start, stop, destroy, run a command. It also has attributes of the
+ container, including:
+ name: Name of the container.
+ state: State of the container, e.g., ABORTING, RUNNING, STARTING, STOPPED,
+ or STOPPING.
+
+ lxc-ls can also collect other attributes of a container including:
+ ipv4: IP address for IPv4.
+ ipv6: IP address for IPv6.
+ autostart: If the container will autostart at system boot.
+ pid: Process ID of the container.
+ memory: Memory used by the container, as a string, e.g., "6.2MB"
+ ram: Physical ram used by the container, as a string, e.g., "6.2MB"
+ swap: swap used by the container, as a string, e.g., "1.0MB"
+
+ For performance reason, such info is not collected for now.
+
+ The attributes available are defined in ATTRIBUTES constant.
+ """
+
+ def __init__(self, container_path, attribute_values):
+ """Initialize an object of LXC container with given attribute values.
+
+ @param container_path: Directory that stores the container.
+ @param attribute_values: A dictionary of attribute values for the
+ container.
+ """
+ self.container_path = os.path.realpath(container_path)
+ # Path to the rootfs of the container. This will be initialized when
+ # property rootfs is retrieved.
+ self._rootfs = None
+ for attribute, value in attribute_values.iteritems():
+ setattr(self, attribute, value)
+
+
+ def refresh_status(self):
+ """Refresh the status information of the container.
+ """
+ containers = lxc.get_container_info(self.container_path, name=self.name)
+ if not containers:
+ raise error.ContainerError(
+ 'No container found in directory %s with name of %s.' %
+ self.container_path, self.name)
+ attribute_values = containers[0]
+ for attribute, value in attribute_values.iteritems():
+ setattr(self, attribute, value)
+
+
+ @property
+ def rootfs(self):
+ """Path to the rootfs of the container.
+
+ This property returns the path to the rootfs of the container, that is,
+ the folder where the container stores its local files. It reads the
+ attribute lxc.rootfs from the config file of the container, e.g.,
+ lxc.rootfs = /usr/local/autotest/containers/t4/rootfs
+ If the container is created with snapshot, the rootfs is a chain of
+ folders, separated by `:` and ordered by how the snapshot is created,
+ e.g.,
+ lxc.rootfs = overlayfs:/usr/local/autotest/containers/base/rootfs:
+ /usr/local/autotest/containers/t4_s/delta0
+ This function returns the last folder in the chain, in above example,
+ that is `/usr/local/autotest/containers/t4_s/delta0`
+
+ Files in the rootfs will be accessible directly within container. For
+ example, a folder in host "[rootfs]/usr/local/file1", can be accessed
+ inside container by path "/usr/local/file1". Note that symlink in the
+ host can not across host/container boundary, instead, directory mount
+ should be used, refer to function mount_dir.
+
+ @return: Path to the rootfs of the container.
+ """
+ if not self._rootfs:
+ cmd = ('sudo lxc-info -P %s -n %s -c lxc.rootfs' %
+ (self.container_path, self.name))
+ lxc_rootfs_config = utils.run(cmd).stdout.strip()
+ match = re.match('lxc.rootfs = (.*)', lxc_rootfs_config)
+ if not match:
+ raise error.ContainerError(
+ 'Failed to locate rootfs for container %s. lxc.rootfs '
+ 'in the container config file is %s' %
+ (self.name, lxc_rootfs_config))
+ lxc_rootfs = match.group(1)
+ self.clone_from_snapshot = ':' in lxc_rootfs
+ if self.clone_from_snapshot:
+ self._rootfs = lxc_rootfs.split(':')[-1]
+ else:
+ self._rootfs = lxc_rootfs
+ return self._rootfs
+
+
+ def attach_run(self, command, bash=True):
+ """Attach to a given container and run the given command.
+
+ @param command: Command to run in the container.
+ @param bash: Run the command through bash -c "command". This allows
+ pipes to be used in command. Default is set to True.
+
+ @return: The output of the command.
+
+ @raise error.CmdError: If container does not exist, or not running.
+ """
+ cmd = 'sudo lxc-attach -P %s -n %s' % (self.container_path, self.name)
+ if bash and not command.startswith('bash -c'):
+ command = 'bash -c "%s"' % utils.sh_escape(command)
+ cmd += ' -- %s' % command
+ # TODO(dshi): crbug.com/459344 Set sudo to default to False when test
+ # container can be unprivileged container.
+ return utils.run(cmd)
+
+
+ def is_network_up(self):
+ """Check if network is up in the container by curl base container url.
+
+ @return: True if the network is up, otherwise False.
+ """
+ try:
+ self.attach_run('curl --head %s' % constants.CONTAINER_BASE_URL)
+ return True
+ except error.CmdError as e:
+ logging.debug(e)
+ return False
+
+
+ @metrics.SecondsTimerDecorator(
+ '%s/container_start_duration' % constants.STATS_KEY)
+ def start(self, wait_for_network=True):
+ """Start the container.
+
+ @param wait_for_network: True to wait for network to be up. Default is
+ set to True.
+
+ @raise ContainerError: If container does not exist, or fails to start.
+ """
+ cmd = 'sudo lxc-start -P %s -n %s -d' % (self.container_path, self.name)
+ output = utils.run(cmd).stdout
+ self.refresh_status()
+ if self.state != 'RUNNING':
+ raise error.ContainerError(
+ 'Container %s failed to start. lxc command output:\n%s' %
+ (os.path.join(self.container_path, self.name),
+ output))
+
+ if wait_for_network:
+ logging.debug('Wait for network to be up.')
+ start_time = time.time()
+ utils.poll_for_condition(
+ condition=self.is_network_up,
+ timeout=constants.NETWORK_INIT_TIMEOUT,
+ sleep_interval=constants.NETWORK_INIT_CHECK_INTERVAL)
+ logging.debug('Network is up after %.2f seconds.',
+ time.time() - start_time)
+
+
+ @metrics.SecondsTimerDecorator(
+ '%s/container_stop_duration' % constants.STATS_KEY)
+ def stop(self):
+ """Stop the container.
+
+ @raise ContainerError: If container does not exist, or fails to start.
+ """
+ cmd = 'sudo lxc-stop -P %s -n %s' % (self.container_path, self.name)
+ output = utils.run(cmd).stdout
+ self.refresh_status()
+ if self.state != 'STOPPED':
+ raise error.ContainerError(
+ 'Container %s failed to be stopped. lxc command output:\n'
+ '%s' % (os.path.join(self.container_path, self.name),
+ output))
+
+
+ @metrics.SecondsTimerDecorator(
+ '%s/container_destroy_duration' % constants.STATS_KEY)
+ def destroy(self, force=True):
+ """Destroy the container.
+
+ @param force: Set to True to force to destroy the container even if it's
+ running. This is faster than stop a container first then
+ try to destroy it. Default is set to True.
+
+ @raise ContainerError: If container does not exist or failed to destroy
+ the container.
+ """
+ cmd = 'sudo lxc-destroy -P %s -n %s' % (self.container_path,
+ self.name)
+ if force:
+ cmd += ' -f'
+ utils.run(cmd)
+
+
+ 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.
+ """
+ # Destination path in container must be relative.
+ destination = destination.lstrip('/')
+ # Create directory in container for mount.
+ utils.run('sudo mkdir -p %s' % os.path.join(self.rootfs, destination))
+ config_file = os.path.join(self.container_path, self.name, 'config')
+ mount = constants.MOUNT_FMT % {'source': source,
+ 'destination': destination,
+ 'readonly': ',ro' if readonly else ''}
+ utils.run(
+ constants.APPEND_CMD_FMT % {'content': mount, 'file': config_file})
+
+
+ def verify_autotest_setup(self, job_folder):
+ """Verify autotest code is set up properly in the container.
+
+ @param job_folder: Name of the job result folder.
+
+ @raise ContainerError: If autotest code is not set up properly.
+ """
+ # Test autotest code is setup by verifying a list of
+ # (directory, minimum file count)
+ if constants.IS_MOBLAB:
+ site_packages_path = constants.MOBLAB_SITE_PACKAGES_CONTAINER
+ else:
+ site_packages_path = os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR,
+ 'site-packages')
+ directories_to_check = [
+ (lxc_config.CONTAINER_AUTOTEST_DIR, 3),
+ (constants.RESULT_DIR_FMT % job_folder, 0),
+ (site_packages_path, 3)]
+ for directory, count in directories_to_check:
+ result = self.attach_run(command=(constants.COUNT_FILE_CMD %
+ {'dir': directory})).stdout
+ logging.debug('%s entries in %s.', int(result), directory)
+ if int(result) < count:
+ raise error.ContainerError('%s is not properly set up.' %
+ directory)
+ # lxc-attach and run command does not run in shell, thus .bashrc is not
+ # loaded. Following command creates a symlink in /usr/bin/ for gsutil
+ # if it's installed.
+ # TODO(dshi): Remove this code after lab container is updated with
+ # gsutil installed in /usr/bin/
+ self.attach_run('test -f /root/gsutil/gsutil && '
+ 'ln -s /root/gsutil/gsutil /usr/bin/gsutil || true')
+
+
+ def modify_import_order(self):
+ """Swap the python import order of lib and local/lib.
+
+ In Moblab, the host's python modules located in
+ /usr/lib64/python2.7/site-packages is mounted to following folder inside
+ container: /usr/local/lib/python2.7/dist-packages/. The modules include
+ an old version of requests module, which is used in autotest
+ site-packages. For test, the module is only used in
+ dev_server/symbolicate_dump for requests.call and requests.codes.OK.
+ When pip is installed inside the container, it installs requests module
+ with version of 2.2.1 in /usr/lib/python2.7/dist-packages/. The version
+ is newer than the one used in autotest site-packages, but not the latest
+ either.
+ According to /usr/lib/python2.7/site.py, modules in /usr/local/lib are
+ imported before the ones in /usr/lib. That leads to pip to use the older
+ version of requests (0.11.2), and it will fail. On the other hand,
+ requests module 2.2.1 can't be installed in CrOS (refer to CL:265759),
+ and higher version of requests module can't work with pip.
+ The only fix to resolve this is to switch the import order, so modules
+ in /usr/lib can be imported before /usr/local/lib.
+ """
+ site_module = '/usr/lib/python2.7/site.py'
+ self.attach_run("sed -i ':a;N;$!ba;s/\"local\/lib\",\\n/"
+ "\"lib_placeholder\",\\n/g' %s" % site_module)
+ self.attach_run("sed -i ':a;N;$!ba;s/\"lib\",\\n/"
+ "\"local\/lib\",\\n/g' %s" % site_module)
+ self.attach_run('sed -i "s/lib_placeholder/lib/g" %s' %
+ site_module)
diff --git a/site_utils/lxc/container_bucket.py b/site_utils/lxc/container_bucket.py
new file mode 100644
index 0000000..6f7d73f
--- /dev/null
+++ b/site_utils/lxc/container_bucket.py
@@ -0,0 +1,382 @@
+# 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
diff --git a/site_utils/lxc/lxc.py b/site_utils/lxc/lxc.py
new file mode 100644
index 0000000..7403582
--- /dev/null
+++ b/site_utils/lxc/lxc.py
@@ -0,0 +1,275 @@
+# 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 tempfile
+
+import common
+from autotest_lib.client.bin import utils as common_utils
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib.cros import dev_server
+from autotest_lib.client.common_lib.cros import retry
+from autotest_lib.server import utils as server_utils
+from autotest_lib.site_utils.lxc import constants
+from autotest_lib.site_utils.lxc import utils as lxc_utils
+
+try:
+ from chromite.lib import metrics
+except ImportError:
+ metrics = common_utils.metrics_mock
+
+
+def _get_container_info_moblab(container_path, **filters):
+ """Get a collection of container information in the given container path
+ in a Moblab.
+
+ TODO(crbug.com/457496): remove this method once python 3 can be installed
+ in Moblab and lxc-ls command can use python 3 code.
+
+ When running in Moblab, lxc-ls behaves differently from a server with python
+ 3 installed:
+ 1. lxc-ls returns a list of containers installed under /etc/lxc, the default
+ lxc container directory.
+ 2. lxc-ls --active lists all active containers, regardless where the
+ container is located.
+ For such differences, we have to special case Moblab to make the behavior
+ close to a server with python 3 installed. That is,
+ 1. List only containers in a given folder.
+ 2. Assume all active containers have state of RUNNING.
+
+ @param container_path: Path to look for containers.
+ @param filters: Key value to filter the containers, e.g., name='base'
+
+ @return: A list of dictionaries that each dictionary has the information of
+ a container. The keys are defined in ATTRIBUTES.
+ """
+ info_collection = []
+ active_containers = common_utils.run('sudo lxc-ls --active').stdout.split()
+ name_filter = filters.get('name', None)
+ state_filter = filters.get('state', None)
+ if filters and set(filters.keys()) - set(['name', 'state']):
+ raise error.ContainerError('When running in Moblab, container list '
+ 'filter only supports name and state.')
+
+ for name in os.listdir(container_path):
+ # Skip all files and folders without rootfs subfolder.
+ if (os.path.isfile(os.path.join(container_path, name)) or
+ not lxc_utils.path_exists(os.path.join(container_path, name,
+ 'rootfs'))):
+ continue
+ info = {'name': name,
+ 'state': 'RUNNING' if name in active_containers else 'STOPPED'
+ }
+ if ((name_filter and name_filter != info['name']) or
+ (state_filter and state_filter != info['state'])):
+ continue
+
+ info_collection.append(info)
+ return info_collection
+
+
+def get_container_info(container_path, **filters):
+ """Get a collection of container information in the given container path.
+
+ This method parse the output of lxc-ls to get a list of container
+ information. The lxc-ls command output looks like:
+ NAME STATE IPV4 IPV6 AUTOSTART PID MEMORY RAM SWAP
+ --------------------------------------------------------------------------
+ base STOPPED - - NO - - - -
+ test_123 RUNNING 10.0.3.27 - NO 8359 6.28MB 6.28MB 0.0MB
+
+ @param container_path: Path to look for containers.
+ @param filters: Key value to filter the containers, e.g., name='base'
+
+ @return: A list of dictionaries that each dictionary has the information of
+ a container. The keys are defined in ATTRIBUTES.
+ """
+ if constants.IS_MOBLAB:
+ return _get_container_info_moblab(container_path, **filters)
+
+ cmd = 'sudo lxc-ls -P %s -f -F %s' % (os.path.realpath(container_path),
+ ','.join(constants.ATTRIBUTES))
+ output = common_utils.run(cmd).stdout
+ info_collection = []
+
+ for line in output.splitlines()[1:]:
+ # Only LXC 1.x has the second line of '-' as a separator.
+ if line.startswith('------'):
+ continue
+ info_collection.append(dict(zip(constants.ATTRIBUTES, line.split())))
+ if filters:
+ filtered_collection = []
+ for key, value in filters.iteritems():
+ for info in info_collection:
+ if key in info and info[key] == value:
+ filtered_collection.append(info)
+ info_collection = filtered_collection
+ return info_collection
+
+
+# Make sure retries only happen in the non-timeout case.
+@retry.retry((error.CmdError),
+ blacklist=[error.CmdTimeoutError],
+ timeout_min=(constants.DEVSERVER_CALL_TIMEOUT *
+ constants.DEVSERVER_CALL_RETRY / 60),
+ delay_sec=constants.DEVSERVER_CALL_DELAY)
+def download_extract(url, target, extract_dir):
+ """Download the file from given url and save it to the target, then extract.
+
+ @param url: Url to download the file.
+ @param target: Path of the file to save to.
+ @param extract_dir: Directory to extract the content of the file to.
+ """
+ remote_url = dev_server.DevServer.get_server_url(url)
+ # TODO(xixuan): Better to only ssh to devservers in lab, and continue using
+ # wget for ganeti devservers.
+ if remote_url in dev_server.ImageServerBase.servers():
+ # This can be run in multiple threads, pick a unique tmp_file.name.
+ with tempfile.NamedTemporaryFile(prefix=os.path.basename(target) + '_',
+ delete=False) as tmp_file:
+ dev_server.ImageServerBase.download_file(
+ url,
+ tmp_file.name,
+ timeout=constants.DEVSERVER_CALL_TIMEOUT)
+ common_utils.run('sudo mv %s %s' % (tmp_file.name, target))
+ else:
+ # We do not want to retry on CmdTimeoutError but still retry on
+ # CmdError. Hence we can't use wget --timeout=...
+ common_utils.run('sudo wget -nv %s -O %s' % (url, target),
+ stderr_tee=common_utils.TEE_TO_LOGS,
+ timeout=constants.DEVSERVER_CALL_TIMEOUT)
+
+ common_utils.run('sudo tar -xvf %s -C %s' % (target, extract_dir))
+
+
+def _install_package_precheck(packages):
+ """If SSP is not enabled or the test is running in chroot (using test_that),
+ packages installation should be skipped.
+
+ The check does not raise exception so tests started by test_that or running
+ in an Autotest setup with SSP disabled can continue. That assume the running
+ environment, chroot or a machine, has the desired packages installed
+ already.
+
+ @param packages: A list of names of the packages to install.
+
+ @return: True if package installation can continue. False if it should be
+ skipped.
+
+ """
+ if not constants.SSP_ENABLED and not common_utils.is_in_container():
+ logging.info('Server-side packaging is not enabled. Install package %s '
+ 'is skipped.', packages)
+ return False
+
+ if server_utils.is_inside_chroot():
+ logging.info('Test is running inside chroot. Install package %s is '
+ 'skipped.', packages)
+ return False
+
+ if not common_utils.is_in_container():
+ raise error.ContainerError('Package installation is only supported '
+ 'when test is running inside container.')
+
+ return True
+
+
+@metrics.SecondsTimerDecorator(
+ '%s/install_packages_duration' % constants.STATS_KEY)
+@retry.retry(error.CmdError, timeout_min=30)
+def install_packages(packages=[], python_packages=[], force_latest=False):
+ """Install the given package inside container.
+
+ !!! WARNING !!!
+ This call may introduce several minutes of delay in test run. The best way
+ to avoid such delay is to update the base container used for the test run.
+ File a bug for infra deputy to update the base container with the new
+ package a test requires.
+
+ @param packages: A list of names of the packages to install.
+ @param python_packages: A list of names of the python packages to install
+ using pip.
+ @param force_latest: True to force to install the latest version of the
+ package. Default to False, which means skip installing
+ the package if it's installed already, even with an old
+ version.
+
+ @raise error.ContainerError: If package is attempted to be installed outside
+ a container.
+ @raise error.CmdError: If the package doesn't exist or failed to install.
+
+ """
+ if not _install_package_precheck(packages or python_packages):
+ return
+
+ # If force_latest is False, only install packages that are not already
+ # installed.
+ if not force_latest:
+ packages = [p for p in packages
+ if not common_utils.is_package_installed(p)]
+ python_packages = [p for p in python_packages
+ if not common_utils.is_python_package_installed(p)]
+ if not packages and not python_packages:
+ logging.debug('All packages are installed already, skip reinstall.')
+ return
+
+ # Always run apt-get update before installing any container. The base
+ # container may have outdated cache.
+ common_utils.run('sudo apt-get update')
+ # Make sure the lists are not None for iteration.
+ packages = [] if not packages else packages
+ if python_packages:
+ packages.extend(['python-pip', 'python-dev'])
+ if packages:
+ common_utils.run(
+ 'sudo apt-get install %s -y --force-yes' % ' '.join(packages))
+ logging.debug('Packages are installed: %s.', packages)
+
+ target_setting = ''
+ # For containers running in Moblab, /usr/local/lib/python2.7/dist-packages/
+ # is a readonly mount from the host. Therefore, new python modules have to
+ # be installed in /usr/lib/python2.7/dist-packages/
+ # Containers created in Moblab does not have autotest/site-packages folder.
+ if not os.path.exists('/usr/local/autotest/site-packages'):
+ target_setting = '--target="/usr/lib/python2.7/dist-packages/"'
+ if python_packages:
+ common_utils.run('sudo pip install %s %s' % (target_setting,
+ ' '.join(python_packages)))
+ logging.debug('Python packages are installed: %s.', python_packages)
+
+
+@retry.retry(error.CmdError, timeout_min=20)
+def install_package(package):
+ """Install the given package inside container.
+
+ This function is kept for backwards compatibility reason. New code should
+ use function install_packages for better performance.
+
+ @param package: Name of the package to install.
+
+ @raise error.ContainerError: If package is attempted to be installed outside
+ a container.
+ @raise error.CmdError: If the package doesn't exist or failed to install.
+
+ """
+ logging.warn('This function is obsoleted, please use install_packages '
+ 'instead.')
+ install_packages(packages=[package])
+
+
+@retry.retry(error.CmdError, timeout_min=20)
+def install_python_package(package):
+ """Install the given python package inside container using pip.
+
+ This function is kept for backwards compatibility reason. New code should
+ use function install_packages for better performance.
+
+ @param package: Name of the python package to install.
+
+ @raise error.CmdError: If the package doesn't exist or failed to install.
+ """
+ logging.warn('This function is obsoleted, please use install_packages '
+ 'instead.')
+ install_packages(python_packages=[package])
diff --git a/site_utils/lxc_config_unittest.py b/site_utils/lxc/lxc_config_unittest.py
similarity index 92%
rename from site_utils/lxc_config_unittest.py
rename to site_utils/lxc/lxc_config_unittest.py
index 5fa873e..52184cb 100644
--- a/site_utils/lxc_config_unittest.py
+++ b/site_utils/lxc/lxc_config_unittest.py
@@ -8,8 +8,7 @@
import unittest
import common
-
-from autotest_lib.site_utils import lxc_config
+from autotest_lib.site_utils.lxc import config as lxc_config
class DeployConfigTest(unittest.TestCase):
@@ -28,4 +27,4 @@
if '__main__':
- unittest.main()
\ No newline at end of file
+ unittest.main()
diff --git a/site_utils/lxc_functional_test.py b/site_utils/lxc/lxc_functional_test.py
similarity index 100%
rename from site_utils/lxc_functional_test.py
rename to site_utils/lxc/lxc_functional_test.py
diff --git a/site_utils/lxc_utils.py b/site_utils/lxc/utils.py
similarity index 100%
rename from site_utils/lxc_utils.py
rename to site_utils/lxc/utils.py