| # 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() |