You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
409 lines
16 KiB
409 lines
16 KiB
# 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.common_lib import global_config
|
|
from autotest_lib.client.common_lib import utils
|
|
from autotest_lib.site_utils.lxc import constants
|
|
from autotest_lib.site_utils.lxc import utils as 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'
|
|
|
|
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, config_file=None):
|
|
"""Initialize the deploy config manager.
|
|
|
|
@param container: The container needs to deploy config.
|
|
@param config_file: An optional config file. For testing.
|
|
"""
|
|
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.
|
|
if config_file is None:
|
|
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)
|
|
else:
|
|
self.is_shadow_config = False
|
|
|
|
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]
|
|
tmp_append = os.path.join(self.container.rootfs,
|
|
_APPEND_FOLDER.lstrip(os.path.sep))
|
|
commands = []
|
|
if lxc_utils.path_exists(tmp_append):
|
|
commands = ['rm -rf "%s"' % tmp_append]
|
|
commands.append('mkdir -p "%s"' % tmp_append)
|
|
lxc_utils.sudo_commands(commands)
|
|
|
|
|
|
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(_APPEND_FOLDER,
|
|
os.path.basename(deploy_config.target))
|
|
else:
|
|
target = deploy_config.target
|
|
|
|
self.container.copy(deploy_config.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(constants.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)
|
|
self.container.mount_dir(mount_config.source,
|
|
mount_config.target,
|
|
mount_config.readonly)
|
|
|
|
|
|
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()
|