| # 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. |
| |
| This tool can also 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 |
| container, e.g., |
| lxc-start -P /tmp/container -n base -d |
| lxc-attach -P /tmp/container -n base |
| """ |
| |
| |
| import argparse |
| import logging |
| import os |
| import socket |
| import sys |
| import time |
| |
| import common |
| import netifaces |
| 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 retry |
| from autotest_lib.client.common_lib.cros.graphite import autotest_stats |
| |
| |
| config = global_config.global_config |
| |
| # Name of the base container. |
| BASE = 'base' |
| # Naming convention of test container, e.g., test_300_1422862512, where 300 is |
| # the test job ID, 1422862512 is the tick when container is created. |
| TEST_CONTAINER_NAME_FMT = 'test_%s_%d' |
| CONTAINER_AUTOTEST_DIR = '/usr/local/autotest' |
| # Naming convention of the result directory in test container. |
| RESULT_DIR_FMT = os.path.join(CONTAINER_AUTOTEST_DIR, 'results', '%s') |
| # Attributes to retrieve about containers. |
| ATTRIBUTES = ['name', 'state', 'ipv4', 'ipv6', 'autostart', 'pid', 'memory', |
| 'ram', 'swap'] |
| |
| # 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') |
| # url to the base container. |
| CONTAINER_BASE_URL = config.get_config_value('AUTOSERV', 'container_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(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() |
| |
| # Number of seconds to wait for network to be up in a container. |
| NETWORK_INIT_TIMEOUT = 120 |
| # Network bring up is slower in Moblab. |
| NETWORK_INIT_CHECK_INTERVAL = 2 if IS_MOBLAB else 0.1 |
| |
| STATS_KEY = 'lxc.%s' % socket.gethostname() |
| timer = autotest_stats.Timer(STATS_KEY) |
| |
| def run(cmd, sudo=True, **kwargs): |
| """Runs a command on the local system. |
| |
| @param cmd: The command to run. |
| @param sudo: True to run the command as root user, default to True. |
| @param kwargs: Other parameters can be passed to utils.run, e.g., timeout. |
| |
| @returns: A CmdResult object. |
| |
| @raise error.CmdError: If there was a non-0 return code. |
| """ |
| # TODO(dshi): crbug.com/459344 Set sudo to default to False when test |
| # container can be unprivileged container. |
| if sudo: |
| cmd = 'sudo ' + cmd |
| logging.debug(cmd) |
| return utils.run(cmd, kwargs) |
| |
| |
| def is_in_container(): |
| """Check if the process is running inside a container. |
| |
| @return: True if the process is running inside a container, otherwise False. |
| """ |
| try: |
| run('cat /proc/1/cgroup | grep "/lxc/" || false') |
| return True |
| except error.CmdError: |
| return False |
| |
| |
| def path_exists(path): |
| """Check if path exists. |
| |
| If the process is not running with root user, os.path.exists may fail to |
| check if a path owned by root user exists. This function uses command |
| `ls path` to check if path exists. |
| |
| @param path: Path to check if it exists. |
| |
| @return: True if path exists, otherwise False. |
| """ |
| try: |
| run('ls "%s"' % path) |
| return True |
| except error.CmdError: |
| return False |
| |
| |
| 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 = run('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 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 = 'lxc-ls -P %s -f -F %s' % (os.path.realpath(container_path), |
| ','.join(ATTRIBUTES)) |
| output = run(cmd).stdout |
| info_collection = [] |
| |
| for line in output.splitlines()[2:]: |
| 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) |
| # 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 |
| |
| |
| @retry.retry(error.CmdError, timeout_min=5) |
| 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. |
| """ |
| run('wget --timeout=300 -nv %s -O %s' % (url, target)) |
| run('tar -xvf %s -C %s' % (target, extract_dir)) |
| |
| |
| 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. |
| 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" |
| |
| 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) |
| |
| 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) |
| |
| |
| 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 = 'lxc-attach -P %s -n %s' % (self.container_path, self.name) |
| if bash and not command.startswith('bash -c'): |
| command = 'bash -c "%s"' % command |
| cmd += ' -- %s' % command |
| return 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 |
| |
| |
| @timer.decorate |
| 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 = 'lxc-start -P %s -n %s -d' % (self.container_path, self.name) |
| output = 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) |
| |
| |
| @timer.decorate |
| def stop(self): |
| """Stop the container. |
| |
| @raise ContainerError: If container does not exist, or fails to start. |
| """ |
| cmd = 'lxc-stop -P %s -n %s' % (self.container_path, self.name) |
| output = 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)) |
| |
| |
| @timer.decorate |
| 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 = 'lxc-destroy -P %s -n %s' % (self.container_path, |
| self.name) |
| if force: |
| cmd += ' -f' |
| 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('/') |
| # Path to the rootfs directory of the container. If the container is |
| # created from base container by snapshot, base_dir should be set to |
| # the path to the delta0 folder. |
| base_dir = os.path.join(self.container_path, self.name, 'delta0') |
| if not path_exists(base_dir): |
| base_dir = os.path.join(self.container_path, self.name, 'rootfs') |
| # Create directory in container for mount. |
| run('mkdir -p %s' % os.path.join(base_dir, destination)) |
| config_file = os.path.join(self.container_path, self.name, 'config') |
| mount = MOUNT_FMT % {'source': source, |
| 'destination': destination, |
| 'readonly': ',ro' if readonly else ''} |
| run(APPEND_CMD_FMT % {'content': mount, 'file': config_file}) |
| |
| |
| def verify_autotest_setup(self, job_id): |
| """Verify autotest code is set up properly in the container. |
| |
| @param job_id: ID of the job, used to format 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(CONTAINER_AUTOTEST_DIR, |
| 'site-packages') |
| directories_to_check = [ |
| (CONTAINER_AUTOTEST_DIR, 3), |
| (RESULT_DIR_FMT % job_id, 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) |
| |
| |
| 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() |
| |
| |
| @timer.decorate |
| def create_from_base(self, name): |
| """Create a container from the base container. |
| |
| @param name: Name of the 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): |
| raise error.ContainerError('Container %s already exists.' % name) |
| # TODO(crbug.com/464834): Snapshot clone is disabled until Moblab can |
| # support overlayfs, which requires a newer kernel. |
| snapshot = '-s' if not IS_MOBLAB else '' |
| cmd = ('lxc-clone -p %s -P %s %s %s %s' % |
| (self.container_path, self.container_path, snapshot, BASE, name)) |
| run(cmd) |
| return self.get(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: |
| raise error.ContainerError( |
| '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') |
| |
| # 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): |
| run('rm -rf "%s"' % path) |
| download_extract(CONTAINER_BASE_URL, tar_path, self.container_path) |
| # Remove the downloaded container tar file. |
| run('rm "%s"' % tar_path) |
| # Set proper file permission. |
| # TODO(dshi): Change root to current user when test container can be |
| # unprivileged container. |
| run('sudo chown -R root "%s"' % base_path) |
| 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') |
| run('sed -i "s|container_dir|%s|g" "%s"' % |
| (self.container_path, config_path)) |
| |
| |
| def get_host_ip(self): |
| """Get the IP address of the host running containers on lxcbr*. |
| |
| This function gets the IP address on network interface lxcbr*. The |
| assumption is that lxc uses the network interface started with "lxcbr". |
| |
| @return: IP address of the host running containers. |
| """ |
| lxc_network = None |
| for name in netifaces.interfaces(): |
| if name.startswith('lxcbr'): |
| lxc_network = name |
| break |
| if not lxc_network: |
| raise error.ContainerError('Failed to find network interface used ' |
| 'by lxc. All existing interfaces are: ' |
| '%s' % netifaces.interfaces()) |
| return netifaces.ifaddresses(lxc_network)[netifaces.AF_INET][0]['addr'] |
| |
| |
| def modify_shadow_config(self, container, shadow_config): |
| """Update the shadow config used in container with correct values. |
| |
| 1. Disable master ssh connection in shadow config, as it is not working |
| properly in container yet, and produces noise in the log. |
| 2. Update AUTOTEST_WEB/host and SERVER/hostname to be the IP of the host |
| if any is set to localhost or 127.0.0.1. Otherwise, set it to be the |
| FQDN of the config value. |
| |
| @param container: The container object to be updated in shadow config. |
| @param shadow_config: Path the the shadow config file to be used in the |
| container. |
| """ |
| # Inject "AUTOSERV/enable_master_ssh: False" in shadow config as |
| # container does not support master ssh connection yet. |
| container.attach_run( |
| 'echo $\'\n[AUTOSERV]\nenable_master_ssh: False\n\' >> %s' % |
| shadow_config) |
| |
| host_ip = self.get_host_ip() |
| local_names = ['localhost', '127.0.0.1'] |
| |
| db_host = config.get_config_value('AUTOTEST_WEB', 'host') |
| if db_host.lower() in local_names: |
| new_host = host_ip |
| else: |
| new_host = socket.getfqdn(db_host) |
| container.attach_run('echo $\'\n[AUTOTEST_WEB]\nhost: %s\n\' >> %s' % |
| (new_host, shadow_config)) |
| |
| afe_host = config.get_config_value('SERVER', 'hostname') |
| if afe_host.lower() in local_names: |
| new_host = host_ip |
| else: |
| new_host = socket.getfqdn(afe_host) |
| container.attach_run('echo $\'\n[SERVER]\nhostname: %s\n\' >> %s' % |
| (new_host, shadow_config)) |
| |
| |
| @timer.decorate |
| @cleanup_if_fail() |
| def setup_test(self, name, job_id, server_package_url, result_path, |
| control=None, skip_cleanup=False): |
| """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. |
| |
| @return: A Container object for the test container. |
| |
| @raise ContainerError: If container does not exist, or not running. |
| """ |
| 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) |
| |
| # Create test container from the base container. |
| container = self.create_from_base(name) |
| |
| # Deploy server side package |
| usr_local_path = os.path.join(self.container_path, name, |
| 'rootfs' if IS_MOBLAB else 'delta0', |
| '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. |
| run('mkdir -p %s'% usr_local_path) |
| |
| download_extract(server_package_url, autotest_pkg_path, usr_local_path) |
| # Copy over local shadow_config.ini |
| shadow_config = os.path.join(common.autotest_dir, 'shadow_config.ini') |
| container_shadow_config = os.path.join(autotest_path, |
| 'shadow_config.ini') |
| run('cp %s %s' % (shadow_config, container_shadow_config)) |
| |
| # Copy over control file to run the test job. |
| if control: |
| container_drone_temp = os.path.join(autotest_path, 'drone_tmp') |
| run('mkdir -p %s'% container_drone_temp) |
| container_control_file = os.path.join( |
| container_drone_temp, os.path.basename(control)) |
| run('cp %s %s' % (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(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(CONTAINER_AUTOTEST_DIR, 'puppylab'), |
| True), |
| (result_path, |
| os.path.join(RESULT_DIR_FMT % job_id), |
| False), |
| ] |
| # 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. |
| run('chown -R root "%s"' % autotest_path) |
| run('chgrp -R root "%s"' % autotest_path) |
| |
| container.start(name) |
| # Make sure the rsa file has right permission. |
| container.attach_run('chmod 700 /root/.ssh/testing_rsa') |
| self.modify_shadow_config( |
| container, |
| os.path.join(CONTAINER_AUTOTEST_DIR, 'shadow_config.ini')) |
| |
| container.verify_autotest_setup(job_id) |
| |
| logging.debug('Test container %s is set up.', name) |
| return container |
| |
| |
| def parse_options(): |
| """Parse command line inputs. |
| |
| @raise argparse.ArgumentError: If command line arguments are invalid. |
| """ |
| parser = argparse.ArgumentParser() |
| parser.add_argument('-s', '--setup', action='store_true', |
| default=False, |
| help='Set up base container.') |
| parser.add_argument('-p', '--path', type=str, |
| help='Directory to store the container.', |
| default=DEFAULT_CONTAINER_PATH) |
| parser.add_argument('-f', '--force_delete', action='store_true', |
| default=False, |
| help=('Force to delete existing containers and rebuild ' |
| 'base containers.')) |
| options = parser.parse_args() |
| if not options.setup and not options.force_delete: |
| raise argparse.ArgumentError( |
| 'Use --setup to setup a base container, or --force_delete to ' |
| 'delete all containers in given path.') |
| return options |
| |
| |
| def main(): |
| """main script.""" |
| options = parse_options() |
| bucket = ContainerBucket(container_path=options.path) |
| if options.setup: |
| bucket.setup_base(force_delete=options.force_delete) |
| elif options.force_delete: |
| bucket.destroy_all() |
| |
| |
| if __name__ == '__main__': |
| main() |