# 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 and shared folders 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
},
{
"source": "/usr/local/autotest/results/shared",
"target": "/usr/local/autotest/results/shared",
"mount": true,
"readonly": false,
"force_create": true
}
]
Definition of each attribute for config files 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.
Example:
{
"source": "/etc/resolv.conf",
"target": "/etc/resolv.conf",
"append": true,
"permission": 400
}
The above example 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
Definition of each attribute for sharing folders are as follows:
source: a folder in host to be mounted in container.
target: the folder's location inside container.
mount: true to mount the source folder onto the target inside container.
A setting with false value of mount is invalid.
readonly: true if the mounted folder inside container should be readonly.
force_create: true to create the source folder if it doesn't exist.
Example:
{
"source": "/usr/local/autotest/results/shared",
"target": "/usr/local/autotest/results/shared",
"mount": true,
"readonly": false,
"force_create": true
}
The above example will mount folder "/usr/local/autotest/results/shared" in the
host to path "/usr/local/autotest/results/shared" inside the container. The
folder can be written to inside container. If the source folder doesn't exist,
it will be created as `force_create` is set to true.
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 getpass
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'])
MountConfig = collections.namedtuple(
'MountConfig', ['source', 'target', 'mount', 'readonly',
'force_create'])
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_path(deploy_config):
"""Validate the source and target in deploy_config dict.
@param deploy_config: A dictionary of deploy config to be validated.
@raise SSPDeployError: If any path in deploy config is invalid.
"""
target = deploy_config['target']
source = deploy_config['source']
if not os.path.isabs(target):
raise SSPDeployError('Target path must be absolute path: %s' %
target)
if not os.path.isabs(source):
if source.startswith('~'):
# This is to handle the case that the script is run with sudo.
inject_user_path = ('~%s%s' % (utils.get_real_user(),
source[1:]))
source = os.path.expanduser(inject_user_path)
else:
source = os.path.join(common.autotest_dir, source)
# Update the source setting in deploy config with the updated path.
deploy_config['source'] = source
@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.
"""
DeployConfigManager.validate_path(deploy_config)
return DeployConfig(**deploy_config)
@staticmethod
def validate_mount(deploy_config):
"""Validate the deploy config for mounting a directory.
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.
3. Mount must be true.
@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.
"""
DeployConfigManager.validate_path(deploy_config)
c = MountConfig(**deploy_config)
if not c.mount:
raise SSPDeployError('`mount` must be true.')
if not c.force_create and not os.path.exists(c.source):
raise SSPDeployError('`source` does not exist.')
return c
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
if 'append' in c]
self.mount_configs = [self.validate_mount(c) for c in deploy_configs
if 'mount' in c]
self.tmp_append = os.path.join(self.container.rootfs, 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.rootfs,
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.
3. Update SSP/user, which is used as the user makes RPC inside the
container. This allows the RPC to pass ACL check as if the call is
made in the host.
"""
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))
# Update configurations in SSP section:
# user: The user running current process.
# is_moblab: True if the autotest server is a Moblab instance.
# host_container_ip: IP address of the lxcbr0 interface. Process running
# inside container can make RPC through this IP.
self.container.attach_run(
'echo $\'\n[SSP]\nuser: %s\nis_moblab: %s\n'
'host_container_ip: %s\n\' >> %s' %
(getpass.getuser(), bool(utils.is_moblab()),
lxc_utils.get_host_ip(), 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)
for mount_config in self.mount_configs:
if (mount_config.force_create and
not os.path.exists(mount_config.source)):
utils.run('mkdir -p %s' % mount_config.source)
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()