[autotest] Add a generic config deployment module to handle configs for container.
For container to run a test, it needs some config files to be able to do
something like:
resolve hostname with dns configured in host.
ssh to dut.
ssh to devserver.
Currently we hard code to copy each file to container, and modify each file for
it to work within container.
This CL provides a generic way to manage a list of deploy config, each config
specifies what file needs to be copied to where and if the existing file should
be overwritten. For container to work in developer's workstation, the special
handling of some configure files is still available. For lab servers, puppet
should be used to push out the config files and shadow deploy config (
ssp_deploy_shadow_config).
BUG=chromium:481706
TEST=local test with and without shadow ssp deploy config
sudo python site_utils/lxc_functional_test.py -v -d chromeos1-dshi1.cros -r 172.17.40.27
also verify in moblab (python package install does not work in moblab yet due
to bug 483371)
Change-Id: I8bbe33a83262b7c746fbe0112cf61cea046931ea
Reviewed-on: https://chromium-review.googlesource.com/267569
Trybot-Ready: Dan Shi <[email protected]>
Tested-by: Dan Shi <[email protected]>
Reviewed-by: Simran Basi <[email protected]>
Commit-Queue: Dan Shi <[email protected]>
diff --git a/.gitignore b/.gitignore
index df32260..41a77ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,3 +65,6 @@
# Container directory
containers/
+
+# Shadow config for ssp deploy
+ssp_deploy_shadow_config.json
diff --git a/client/common_lib/site_utils.py b/client/common_lib/site_utils.py
index 4e21cb2..ececc83 100644
--- a/client/common_lib/site_utils.py
+++ b/client/common_lib/site_utils.py
@@ -476,3 +476,19 @@
return False
# Versioned build, i.e., rc or release build.
return stripped_version == release_version
+
+
+def get_real_user():
+ """Get the real user that runs the script.
+
+ The function check environment variable SUDO_USER for the user if the
+ script is run with sudo. Otherwise, it returns the value of environment
+ variable USER.
+
+ @return: The user name that runs the script.
+
+ """
+ user = os.environ.get('SUDO_USER')
+ if not user:
+ user = os.environ.get('USER')
+ return user
diff --git a/server/autoserv b/server/autoserv
index 60ee3d4..aa7212e 100755
--- a/server/autoserv
+++ b/server/autoserv
@@ -21,6 +21,7 @@
import common
+from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import control_data
from autotest_lib.client.common_lib import global_config
from autotest_lib.client.common_lib.cros.graphite import autotest_es
@@ -61,6 +62,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.client.common_lib import pidfile, logging_manager
from autotest_lib.client.common_lib.cros.graphite import autotest_stats
@@ -213,8 +215,8 @@
if not results:
return
- lxc.run('chown -R %s "%s"' % (os.getuid(), results))
- lxc.run('chgrp -R %s "%s"' % (os.getgid(), results))
+ utils.run('sudo chown -R %s "%s"' % (os.getuid(), results))
+ utils.run('sudo chgrp -R %s "%s"' % (os.getgid(), results))
def run_autoserv(pid_file_manager, results, parser, ssp_url, use_ssp):
@@ -558,7 +560,7 @@
# wait until now to perform this check, so it get properly logged
if (parser.options.use_existing_results and not resultdir_exists and
- not lxc.is_in_container()):
+ not lxc_utils.is_in_container()):
logging.error("No existing results directory found: %s", results)
sys.exit(1)
diff --git a/site_utils/lxc.py b/site_utils/lxc.py
index b750087..633f089 100644
--- a/site_utils/lxc.py
+++ b/site_utils/lxc.py
@@ -27,13 +27,14 @@
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_es
from autotest_lib.client.common_lib.cros.graphite import autotest_stats
+from autotest_lib.site_utils import lxc_config
+from autotest_lib.site_utils import lxc_utils
config = global_config.global_config
@@ -45,9 +46,9 @@
# 1422862512: The tick when container is created.
# 2424: The PID of autoserv that starts the container.
TEST_CONTAINER_NAME_FMT = 'test_%s_%d_%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')
+RESULT_DIR_FMT = os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR, 'results',
+ '%s')
# Attributes to retrieve about containers.
ATTRIBUTES = ['name', 'state']
@@ -63,7 +64,7 @@
# 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')
+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.
@@ -107,54 +108,6 @@
STATS_KEY = 'lxc.%s' % socket.gethostname().replace('.', '_')
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
@@ -181,7 +134,7 @@
a container. The keys are defined in ATTRIBUTES.
"""
info_collection = []
- active_containers = run('lxc-ls --active').stdout.split()
+ 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']):
@@ -191,7 +144,8 @@
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'))):
+ 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'
@@ -223,9 +177,9 @@
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
+ 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()[2:]:
@@ -301,8 +255,8 @@
@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))
+ utils.run('sudo wget --timeout=300 -nv %s -O %s' % (url, target))
+ utils.run('sudo tar -xvf %s -C %s' % (target, extract_dir))
@timer.decorate
@@ -313,13 +267,13 @@
@raise error.CmdError: If the package doesn't exist or failed to install.
"""
- if not is_in_container():
+ if not lxc_utils.is_in_container():
raise error.ContainerError('Package installation is only supported '
'when test is running inside container.')
# Always run apt-get update before installing any container. The base
# container may have outdated cache.
- run('apt-get update')
- run('apt-get install %s -y' % package)
+ utils.run('sudo apt-get update')
+ utils.run('sudo apt-get install %s -y' % package)
@timer.decorate
@@ -331,7 +285,7 @@
@raise error.CmdError: If the package doesn't exist or failed to install.
"""
install_package('python-pip')
- run('pip install %s' % package)
+ utils.run('sudo pip install %s' % package)
class Container(object):
@@ -395,11 +349,13 @@
@raise error.CmdError: If container does not exist, or not running.
"""
- cmd = 'lxc-attach -P %s -n %s' % (self.container_path, self.name)
+ 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"' % command
+ command = 'bash -c "%s"' % utils.sh_escape(command)
cmd += ' -- %s' % command
- return run(cmd)
+ # 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):
@@ -424,8 +380,8 @@
@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
+ 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(
@@ -449,8 +405,8 @@
@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
+ 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(
@@ -470,11 +426,11 @@
@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)
+ cmd = 'sudo lxc-destroy -P %s -n %s' % (self.container_path,
+ self.name)
if force:
cmd += ' -f'
- run(cmd)
+ utils.run(cmd)
def mount_dir(self, source, destination, readonly=False):
@@ -490,15 +446,15 @@
# 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):
+ if not lxc_utils.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))
+ utils.run('sudo 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})
+ utils.run(APPEND_CMD_FMT % {'content': mount, 'file': config_file})
def verify_autotest_setup(self, job_id):
@@ -513,10 +469,10 @@
if IS_MOBLAB:
site_packages_path = MOBLAB_SITE_PACKAGES_CONTAINER
else:
- site_packages_path = os.path.join(CONTAINER_AUTOTEST_DIR,
+ site_packages_path = os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR,
'site-packages')
directories_to_check = [
- (CONTAINER_AUTOTEST_DIR, 3),
+ (lxc_config.CONTAINER_AUTOTEST_DIR, 3),
(RESULT_DIR_FMT % job_id, 0),
(site_packages_path, 3)]
for directory, count in directories_to_check:
@@ -605,11 +561,17 @@
# support overlayfs, which requires a newer kernel.
snapshot = '-s' if SUPPORT_SNAPSHOT_CLONE else ''
aufs = '-B aufs' if SNAPSHOT_CLONE_REQUIRE_AUFS else ''
- cmd = ('lxc-clone -p %s -P %s %s' %
+ cmd = ('sudo lxc-clone -p %s -P %s %s' %
(self.container_path, self.container_path,
' '.join([BASE, name, snapshot, aufs])))
- run(cmd)
- return self.get(name)
+ utils.run(cmd)
+ container = self.get(name)
+ # base_dir is the path (in host) mapping to / inside container. Files
+ # copied to that folder are visible inside container.
+ container.base_dir = os.path.join(
+ self.container_path, name,
+ 'rootfs' if not SUPPORT_SNAPSHOT_CLONE else 'delta0')
+ return container
@cleanup_if_fail()
@@ -635,6 +597,12 @@
'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.
@@ -648,79 +616,20 @@
path_to_cleanup = [tar_path, base_path]
for path in path_to_cleanup:
if os.path.exists(path):
- run('rm -rf "%s"' % path)
+ utils.run('sudo 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)
+ 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.
- run('sudo chown -R root "%s"' % base_path)
- run('sudo chgrp -R root "%s"' % base_path)
+ 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')
- 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))
+ utils.run('sudo sed -i "s|container_dir|%s|g" "%s"' %
+ (self.container_path, config_path))
@timer.decorate
@@ -771,53 +680,19 @@
'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)
+ utils.run('sudo 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 local .ssh/config file if exists.
- ssh_config = os.path.expanduser('~/.ssh/config')
- container_ssh = os.path.join(
- self.container_path, name,
- 'rootfs' if not SUPPORT_SNAPSHOT_CLONE else 'delta0',
- 'root', '.ssh')
- container_ssh_config = os.path.join(container_ssh, 'config')
- if os.path.exists(ssh_config):
- run('mkdir -p %s'% container_ssh)
- run('cp "%s" "%s"' % (ssh_config, container_ssh_config))
- # Remove domain specific flags.
- run('sed -i "s/UseProxyIf=false//g" %s' % container_ssh_config)
- # TODO(dshi): crbug.com/451622 ssh connection loglevel is set to
- # ERROR in container before master ssh connection works. This is
- # to avoid logs being flooded with warning `Permanently added
- # '[hostname]' (RSA) to the list of known hosts.` (crbug.com/478364)
- # The sed command injects following at the beginning of .ssh/config
- # used in config. With such change, ssh command will not post
- # warnings.
- # Host *
- # LogLevel Error
- run('sed -i "1s/^/Host *\\n LogLevel ERROR\\n\\n/" %s' %
- container_ssh_config)
-
- # Copy over resolv.conf for DNS search path. The file is copied to
- # autotest folder so its content can be appended in /etc/resolv.conf
- # after the container is started.
- resolv_conf = '/etc/resolv.conf'
- container_resolv_conf = os.path.join(autotest_path, 'resolv.conf')
- run('cp "%s" "%s"' % (resolv_conf, container_resolv_conf))
+ 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')
- run('mkdir -p %s'% container_drone_temp)
+ utils.run('sudo 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))
+ utils.run('sudo cp %s %s' % (control, container_control_file))
if IS_MOBLAB:
site_packages_path = MOBLAB_SITE_PACKAGES
@@ -825,12 +700,13 @@
else:
site_packages_path = os.path.join(common.autotest_dir,
'site-packages')
- site_packages_container_path = os.path.join(CONTAINER_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(CONTAINER_AUTOTEST_DIR, 'puppylab'),
+ os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR,
+ 'puppylab'),
True),
(result_path,
os.path.join(RESULT_DIR_FMT % job_id),
@@ -843,20 +719,11 @@
# 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)
+ utils.run('sudo chown -R root "%s"' % autotest_path)
+ utils.run('sudo 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')
- container.attach_run('chmod 700 /root/.ssh/config')
- # Update resolv.conf
- container.attach_run('cat /usr/local/autotest/resolv.conf >> '
- '/etc/resolv.conf')
-
- self.modify_shadow_config(
- container,
- os.path.join(CONTAINER_AUTOTEST_DIR, 'shadow_config.ini'))
+ deploy_config_manager.deploy_post_start()
container.verify_autotest_setup(job_id)
diff --git a/site_utils/lxc_config.py b/site_utils/lxc_config.py
new file mode 100644
index 0000000..a609b8a
--- /dev/null
+++ b/site_utils/lxc_config.py
@@ -0,0 +1,312 @@
+# 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 helps to deploy config files from host to container. It reads
+the settings from a setting file (ssp_deploy_config), and deploy the config
+files based on the settings. The setting file has a json string of a list of
+deployment settings. For example:
+[{
+ "source": "/etc/resolv.conf",
+ "target": "/etc/resolv.conf",
+ "append": true,
+ "permission": 400
+ },
+ {
+ "source": "ssh",
+ "target": "/root/.ssh",
+ "append": false,
+ "permission": 400
+ }
+]
+
+Definition of each attribute are as follows:
+source: config file in host to be copied to container.
+target: config file's location inside container.
+append: true to append the content of config file to existing file inside
+ container. If it's set to false, the existing file inside container will
+ be overwritten.
+permission: Permission to set to the config file inside container.
+
+The sample settings will:
+1. Append the content of /etc/resolv.conf in host machine to file
+ /etc/resolv.conf inside container.
+2. Copy all files in ssh to /root/.ssh in container.
+3. Change all these files' permission to 400
+
+The setting file (ssp_deploy_config) lives in AUTOTEST_DIR folder.
+For relative file path specified in ssp_deploy_config, AUTOTEST_DIR/containers
+is the parent folder.
+The setting file can be overridden by a shadow config, ssp_deploy_shadow_config.
+For lab servers, puppet should be used to deploy ssp_deploy_shadow_config to
+AUTOTEST_DIR and the configure files to AUTOTEST_DIR/containers.
+
+The default setting file (ssp_deploy_config) contains
+For SSP to work with none-lab servers, e.g., moblab and developer's workstation,
+the module still supports copy over files like ssh config and autotest
+shadow_config to container when AUTOTEST_DIR/containers/ssp_deploy_config is not
+presented.
+
+"""
+
+import collections
+import json
+import os
+import socket
+
+import common
+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
+
+
+config = global_config.global_config
+
+# Path to ssp_deploy_config and ssp_deploy_shadow_config.
+SSP_DEPLOY_CONFIG_FILE = os.path.join(common.autotest_dir,
+ 'ssp_deploy_config.json')
+SSP_DEPLOY_SHADOW_CONFIG_FILE = os.path.join(common.autotest_dir,
+ 'ssp_deploy_shadow_config.json')
+# A temp folder used to store files to be appended to the files inside
+# container.
+APPEND_FOLDER = 'usr/local/ssp_append'
+# Path to folder that contains autotest code inside container.
+CONTAINER_AUTOTEST_DIR = '/usr/local/autotest'
+
+DeployConfig = collections.namedtuple(
+ 'DeployConfig', ['source', 'target', 'append', 'permission'])
+
+
+class SSPDeployError(Exception):
+ """Exception raised if any error occurs when setting up test container."""
+
+
+class DeployConfigManager(object):
+ """An object to deploy config to container.
+
+ The manager retrieves deploy configs from ssp_deploy_config or
+ ssp_deploy_shadow_config, and sets up the container accordingly.
+ For example:
+ 1. Copy given config files to specified location inside container.
+ 2. Append the content of given config files to specific files inside
+ container.
+ 3. Make sure the config files have proper permission inside container.
+
+ """
+
+ @staticmethod
+ def validate(deploy_config):
+ """Validate the deploy config.
+
+ Deploy configs need to be validated and pre-processed, e.g.,
+ 1. Target must be an absolute path.
+ 2. Source must be updated to be an absolute path.
+
+ @param deploy_config: A dictionary of deploy config to be validated.
+
+ @return: A DeployConfig object that contains the deploy config.
+
+ @raise SSPDeployError: If the deploy config is invalid.
+
+ """
+ c = DeployConfig(**deploy_config)
+ if not os.path.isabs(c.target):
+ raise SSPDeployError('Target path must be absolute path: %s' %
+ c.target)
+ if not os.path.isabs(c.source):
+ if c.source.startswith('~'):
+ # This is to handle the case that the script is run with sudo.
+ inject_user_path = ('~%s%s' % (utils.get_real_user(),
+ c.source[1:]))
+ source = os.path.expanduser(inject_user_path)
+ else:
+ source = os.path.join(common.autotest_dir, c.source)
+ deploy_config['source'] = source
+
+ return DeployConfig(**deploy_config)
+
+
+ def __init__(self, container):
+ """Initialize the deploy config manager.
+
+ @param container: The container needs to deploy config.
+
+ """
+ self.container = container
+ # If shadow config is used, the deployment procedure will skip some
+ # special handling of config file, e.g.,
+ # 1. Set enable_master_ssh to False in autotest shadow config.
+ # 2. Set ssh logleve to ERROR for all hosts.
+ self.is_shadow_config = os.path.exists(SSP_DEPLOY_SHADOW_CONFIG_FILE)
+ config_file = (SSP_DEPLOY_SHADOW_CONFIG_FILE if self.is_shadow_config
+ else SSP_DEPLOY_CONFIG_FILE)
+ with open(config_file) as f:
+ deploy_configs = json.load(f)
+ self.deploy_configs = [self.validate(c) for c in deploy_configs]
+ self.tmp_append = os.path.join(self.container.base_dir, APPEND_FOLDER)
+ if lxc_utils.path_exists(self.tmp_append):
+ utils.run('sudo rm -rf "%s"' % self.tmp_append)
+ utils.run('sudo mkdir -p "%s"' % self.tmp_append)
+
+
+ def _deploy_config_pre_start(self, deploy_config):
+ """Deploy a config before container is started.
+
+ Most configs can be deployed before the container is up. For configs
+ require a reboot to take effective, they must be deployed in this
+ function.
+
+ @param deploy_config: Config to be deployed.
+
+ """
+ if not lxc_utils.path_exists(deploy_config.source):
+ return
+ # Path to the target file relative to host.
+ if deploy_config.append:
+ target = os.path.join(self.tmp_append,
+ os.path.basename(deploy_config.target))
+ else:
+ target = os.path.join(self.container.base_dir,
+ deploy_config.target[1:])
+ # Recursively copy files/folder to the target. `-L` to always follow
+ # symbolic links in source.
+ target_dir = os.path.dirname(target)
+ if not lxc_utils.path_exists(target_dir):
+ utils.run('sudo mkdir -p "%s"' % target_dir)
+ source = deploy_config.source
+ # Make sure the source ends with `/.` if it's a directory. Otherwise
+ # command cp will not work.
+ if os.path.isdir(source) and source[-1] != '.':
+ source += '/.' if source[-1] != '/' else '.'
+ utils.run('sudo cp -RL "%s" "%s"' % (source, target))
+
+
+ def _deploy_config_post_start(self, deploy_config):
+ """Deploy a config after container is started.
+
+ For configs to be appended after the existing config files in container,
+ they must be copied to a temp location before container is up (deployed
+ in function _deploy_config_pre_start). After the container is up, calls
+ can be made to append the content of such configs to existing config
+ files.
+
+ @param deploy_config: Config to be deployed.
+
+ """
+ if deploy_config.append:
+ source = os.path.join('/', APPEND_FOLDER,
+ os.path.basename(deploy_config.target))
+ self.container.attach_run('cat \'%s\' >> \'%s\'' %
+ (source, deploy_config.target))
+ self.container.attach_run(
+ 'chmod -R %s \'%s\'' %
+ (deploy_config.permission, deploy_config.target))
+
+
+ def _modify_shadow_config(self):
+ """Update the shadow config used in container with correct values.
+
+ This only applies when no shadow SSP deploy config is applied. For
+ default SSP deploy config, autotest shadow_config.ini is from autotest
+ directory, which requires following modification to be able to work in
+ container. If one chooses to use a shadow SSP deploy config file, the
+ autotest shadow_config.ini must be from a source with following
+ modification:
+ 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.
+
+ """
+ shadow_config = os.path.join(CONTAINER_AUTOTEST_DIR,
+ 'shadow_config.ini')
+
+ # Inject "AUTOSERV/enable_master_ssh: False" in shadow config as
+ # container does not support master ssh connection yet.
+ self.container.attach_run(
+ 'echo $\'\n[AUTOSERV]\nenable_master_ssh: False\n\' >> %s' %
+ shadow_config)
+
+ host_ip = lxc_utils.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)
+ self.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)
+ self.container.attach_run('echo $\'\n[SERVER]\nhostname: %s\n\' >> %s' %
+ (new_host, shadow_config))
+
+
+ def _modify_ssh_config(self):
+ """Modify ssh config for it to work inside container.
+
+ This is only called when default ssp_deploy_config is used. If shadow
+ deploy config is manually set up, this function will not be called.
+ Therefore, the source of ssh config must be properly updated to be able
+ to work inside container.
+
+ """
+ # Remove domain specific flags.
+ ssh_config = '/root/.ssh/config'
+ self.container.attach_run('sed -i \'s/UseProxyIf=false//g\' \'%s\'' %
+ ssh_config)
+ # TODO(dshi): crbug.com/451622 ssh connection loglevel is set to
+ # ERROR in container before master ssh connection works. This is
+ # to avoid logs being flooded with warning `Permanently added
+ # '[hostname]' (RSA) to the list of known hosts.` (crbug.com/478364)
+ # The sed command injects following at the beginning of .ssh/config
+ # used in config. With such change, ssh command will not post
+ # warnings.
+ # Host *
+ # LogLevel Error
+ self.container.attach_run(
+ 'sed -i \'1s/^/Host *\\n LogLevel ERROR\\n\\n/\' \'%s\'' %
+ ssh_config)
+
+ # Inject ssh config for moblab to ssh to dut from container.
+ if utils.is_moblab():
+ # ssh to moblab itself using moblab user.
+ self.container.attach_run(
+ 'echo $\'\nHost 192.168.231.1\n User moblab\n '
+ 'IdentityFile %%d/.ssh/testing_rsa\' >> %s' %
+ '/root/.ssh/config')
+ # ssh to duts using root user.
+ self.container.attach_run(
+ 'echo $\'\nHost *\n User root\n '
+ 'IdentityFile %%d/.ssh/testing_rsa\' >> %s' %
+ '/root/.ssh/config')
+
+
+ def deploy_pre_start(self):
+ """Deploy configs before the container is started.
+ """
+ for deploy_config in self.deploy_configs:
+ self._deploy_config_pre_start(deploy_config)
+
+
+ def deploy_post_start(self):
+ """Deploy configs after the container is started.
+ """
+ for deploy_config in self.deploy_configs:
+ self._deploy_config_post_start(deploy_config)
+ # Autotest shadow config requires special handling to update hostname
+ # of `localhost` with host IP. Shards always use `localhost` as value
+ # of SERVER\hostname and AUTOTEST_WEB\host.
+ self._modify_shadow_config()
+ # Only apply special treatment for files deployed by the default
+ # ssp_deploy_config
+ if not self.is_shadow_config:
+ self._modify_ssh_config()
diff --git a/site_utils/lxc_config_unittest.py b/site_utils/lxc_config_unittest.py
new file mode 100644
index 0000000..5fa873e
--- /dev/null
+++ b/site_utils/lxc_config_unittest.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+# 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 json
+import os
+import unittest
+
+import common
+
+from autotest_lib.site_utils import lxc_config
+
+
+class DeployConfigTest(unittest.TestCase):
+ """Test DeployConfigManager.
+ """
+
+ def testValidate(self):
+ """Test ssp_deploy_config.json can be validated.
+ """
+ global_deploy_config_file = os.path.join(
+ common.autotest_dir, lxc_config.SSP_DEPLOY_CONFIG_FILE)
+ with open(global_deploy_config_file) as f:
+ deploy_configs = json.load(f)
+ for config in deploy_configs:
+ lxc_config.DeployConfigManager.validate(config)
+
+
+if '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/site_utils/lxc_functional_test.py b/site_utils/lxc_functional_test.py
index b8dc141..4d0e1c3 100644
--- a/site_utils/lxc_functional_test.py
+++ b/site_utils/lxc_functional_test.py
@@ -20,6 +20,7 @@
import time
import common
+from autotest_lib.client.bin import utils
from autotest_lib.site_utils import lxc
@@ -188,10 +189,30 @@
container.attach_run('sudo pip install selenium')
+def test_ssh(container, remote):
+ """Test container can run ssh to remote server.
+
+ @param container: The test container.
+ @param remote: The remote server to ssh to.
+
+ @raise: error.CmdError if container can't ssh to remote server.
+ """
+ logging.info('Test ssh to %s.', remote)
+ container.attach_run('ssh %s -a -x -o StrictHostKeyChecking=no '
+ '-o BatchMode=yes -o UserKnownHostsFile=/dev/null '
+ '-p 22 "true"' % remote)
+
+
def parse_options():
"""Parse command line inputs.
"""
parser = argparse.ArgumentParser()
+ parser.add_argument('-d', '--dut', type=str,
+ help='Test device to ssh to.',
+ default=None)
+ parser.add_argument('-r', '--devserver', type=str,
+ help='Test devserver to ssh to.',
+ default=None)
parser.add_argument('-v', '--verbose', action='store_true',
default=False,
help='Print out ALL entries.')
@@ -223,6 +244,11 @@
container = setup_test(bucket, container_test_name, options.skip_cleanup)
test_share(container)
test_autoserv(container)
+ if options.dut:
+ test_ssh(container, options.dut)
+ if options.devserver:
+ test_ssh(container, options.devserver)
+ # Install package takes the longest time, leave it to the last test.
test_package_install(container)
logging.info('All tests passed.')
@@ -237,4 +263,4 @@
try:
lxc.ContainerBucket(TEMP_DIR).destroy_all()
finally:
- lxc.run('rm -rf "%s"' % TEMP_DIR)
+ utils.run('sudo rm -rf "%s"' % TEMP_DIR)
diff --git a/site_utils/lxc_utils.py b/site_utils/lxc_utils.py
new file mode 100644
index 0000000..192d2f5
--- /dev/null
+++ b/site_utils/lxc_utils.py
@@ -0,0 +1,62 @@
+# 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 utilities used by LXC and its tools.
+"""
+
+import netifaces
+
+import common
+from autotest_lib.client.bin import utils
+from autotest_lib.client.common_lib import error
+
+
+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:
+ utils.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
+ `test -e` to check if path exists.
+
+ @param path: Path to check if it exists.
+
+ @return: True if path exists, otherwise False.
+ """
+ try:
+ utils.run('sudo test -e "%s"' % path)
+ return True
+ except error.CmdError:
+ return False
+
+
+def get_host_ip():
+ """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']
diff --git a/ssp_deploy_config.json b/ssp_deploy_config.json
new file mode 100644
index 0000000..4a75247
--- /dev/null
+++ b/ssp_deploy_config.json
@@ -0,0 +1,20 @@
+[
+ {
+ "source": "/etc/resolv.conf",
+ "target": "/etc/resolv.conf",
+ "append": true,
+ "permission": 400
+ },
+ {
+ "source": "~/.ssh",
+ "target": "/root/.ssh",
+ "append": false,
+ "permission": 400
+ },
+ {
+ "source": "shadow_config.ini",
+ "target": "/usr/local/autotest/shadow_config.ini",
+ "append": false,
+ "permission": 644
+ }
+]
\ No newline at end of file