1# Copyright 2015 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5""" 6This module helps to deploy config files from host to container. It reads 7the settings from a setting file (ssp_deploy_config), and deploy the config 8files based on the settings. The setting file has a json string of a list of 9deployment settings. For example: 10[{ 11 "source": "/etc/resolv.conf", 12 "target": "/etc/resolv.conf", 13 "append": true, 14 "permission": 400 15 }, 16 { 17 "source": "ssh", 18 "target": "/root/.ssh", 19 "append": false, 20 "permission": 400 21 } 22] 23 24Definition of each attribute are as follows: 25source: config file in host to be copied to container. 26target: config file's location inside container. 27append: true to append the content of config file to existing file inside 28 container. If it's set to false, the existing file inside container will 29 be overwritten. 30permission: Permission to set to the config file inside container. 31 32The sample settings will: 331. Append the content of /etc/resolv.conf in host machine to file 34 /etc/resolv.conf inside container. 352. Copy all files in ssh to /root/.ssh in container. 363. Change all these files' permission to 400 37 38The setting file (ssp_deploy_config) lives in AUTOTEST_DIR folder. 39For relative file path specified in ssp_deploy_config, AUTOTEST_DIR/containers 40is the parent folder. 41The setting file can be overridden by a shadow config, ssp_deploy_shadow_config. 42For lab servers, puppet should be used to deploy ssp_deploy_shadow_config to 43AUTOTEST_DIR and the configure files to AUTOTEST_DIR/containers. 44 45The default setting file (ssp_deploy_config) contains 46For SSP to work with none-lab servers, e.g., moblab and developer's workstation, 47the module still supports copy over files like ssh config and autotest 48shadow_config to container when AUTOTEST_DIR/containers/ssp_deploy_config is not 49presented. 50 51""" 52 53import collections 54import getpass 55import json 56import os 57import socket 58 59import common 60from autotest_lib.client.bin import utils 61from autotest_lib.client.common_lib import global_config 62from autotest_lib.client.common_lib import utils 63from autotest_lib.site_utils import lxc_utils 64 65 66config = global_config.global_config 67 68# Path to ssp_deploy_config and ssp_deploy_shadow_config. 69SSP_DEPLOY_CONFIG_FILE = os.path.join(common.autotest_dir, 70 'ssp_deploy_config.json') 71SSP_DEPLOY_SHADOW_CONFIG_FILE = os.path.join(common.autotest_dir, 72 'ssp_deploy_shadow_config.json') 73# A temp folder used to store files to be appended to the files inside 74# container. 75APPEND_FOLDER = 'usr/local/ssp_append' 76# Path to folder that contains autotest code inside container. 77CONTAINER_AUTOTEST_DIR = '/usr/local/autotest' 78 79DeployConfig = collections.namedtuple( 80 'DeployConfig', ['source', 'target', 'append', 'permission']) 81 82 83class SSPDeployError(Exception): 84 """Exception raised if any error occurs when setting up test container.""" 85 86 87class DeployConfigManager(object): 88 """An object to deploy config to container. 89 90 The manager retrieves deploy configs from ssp_deploy_config or 91 ssp_deploy_shadow_config, and sets up the container accordingly. 92 For example: 93 1. Copy given config files to specified location inside container. 94 2. Append the content of given config files to specific files inside 95 container. 96 3. Make sure the config files have proper permission inside container. 97 98 """ 99 100 @staticmethod 101 def validate(deploy_config): 102 """Validate the deploy config. 103 104 Deploy configs need to be validated and pre-processed, e.g., 105 1. Target must be an absolute path. 106 2. Source must be updated to be an absolute path. 107 108 @param deploy_config: A dictionary of deploy config to be validated. 109 110 @return: A DeployConfig object that contains the deploy config. 111 112 @raise SSPDeployError: If the deploy config is invalid. 113 114 """ 115 c = DeployConfig(**deploy_config) 116 if not os.path.isabs(c.target): 117 raise SSPDeployError('Target path must be absolute path: %s' % 118 c.target) 119 if not os.path.isabs(c.source): 120 if c.source.startswith('~'): 121 # This is to handle the case that the script is run with sudo. 122 inject_user_path = ('~%s%s' % (utils.get_real_user(), 123 c.source[1:])) 124 source = os.path.expanduser(inject_user_path) 125 else: 126 source = os.path.join(common.autotest_dir, c.source) 127 deploy_config['source'] = source 128 129 return DeployConfig(**deploy_config) 130 131 132 def __init__(self, container): 133 """Initialize the deploy config manager. 134 135 @param container: The container needs to deploy config. 136 137 """ 138 self.container = container 139 # If shadow config is used, the deployment procedure will skip some 140 # special handling of config file, e.g., 141 # 1. Set enable_master_ssh to False in autotest shadow config. 142 # 2. Set ssh logleve to ERROR for all hosts. 143 self.is_shadow_config = os.path.exists(SSP_DEPLOY_SHADOW_CONFIG_FILE) 144 config_file = (SSP_DEPLOY_SHADOW_CONFIG_FILE if self.is_shadow_config 145 else SSP_DEPLOY_CONFIG_FILE) 146 with open(config_file) as f: 147 deploy_configs = json.load(f) 148 self.deploy_configs = [self.validate(c) for c in deploy_configs] 149 self.tmp_append = os.path.join(self.container.rootfs, APPEND_FOLDER) 150 if lxc_utils.path_exists(self.tmp_append): 151 utils.run('sudo rm -rf "%s"' % self.tmp_append) 152 utils.run('sudo mkdir -p "%s"' % self.tmp_append) 153 154 155 def _deploy_config_pre_start(self, deploy_config): 156 """Deploy a config before container is started. 157 158 Most configs can be deployed before the container is up. For configs 159 require a reboot to take effective, they must be deployed in this 160 function. 161 162 @param deploy_config: Config to be deployed. 163 164 """ 165 if not lxc_utils.path_exists(deploy_config.source): 166 return 167 # Path to the target file relative to host. 168 if deploy_config.append: 169 target = os.path.join(self.tmp_append, 170 os.path.basename(deploy_config.target)) 171 else: 172 target = os.path.join(self.container.rootfs, 173 deploy_config.target[1:]) 174 # Recursively copy files/folder to the target. `-L` to always follow 175 # symbolic links in source. 176 target_dir = os.path.dirname(target) 177 if not lxc_utils.path_exists(target_dir): 178 utils.run('sudo mkdir -p "%s"' % target_dir) 179 source = deploy_config.source 180 # Make sure the source ends with `/.` if it's a directory. Otherwise 181 # command cp will not work. 182 if os.path.isdir(source) and source[-1] != '.': 183 source += '/.' if source[-1] != '/' else '.' 184 utils.run('sudo cp -RL "%s" "%s"' % (source, target)) 185 186 187 def _deploy_config_post_start(self, deploy_config): 188 """Deploy a config after container is started. 189 190 For configs to be appended after the existing config files in container, 191 they must be copied to a temp location before container is up (deployed 192 in function _deploy_config_pre_start). After the container is up, calls 193 can be made to append the content of such configs to existing config 194 files. 195 196 @param deploy_config: Config to be deployed. 197 198 """ 199 if deploy_config.append: 200 source = os.path.join('/', APPEND_FOLDER, 201 os.path.basename(deploy_config.target)) 202 self.container.attach_run('cat \'%s\' >> \'%s\'' % 203 (source, deploy_config.target)) 204 self.container.attach_run( 205 'chmod -R %s \'%s\'' % 206 (deploy_config.permission, deploy_config.target)) 207 208 209 def _modify_shadow_config(self): 210 """Update the shadow config used in container with correct values. 211 212 This only applies when no shadow SSP deploy config is applied. For 213 default SSP deploy config, autotest shadow_config.ini is from autotest 214 directory, which requires following modification to be able to work in 215 container. If one chooses to use a shadow SSP deploy config file, the 216 autotest shadow_config.ini must be from a source with following 217 modification: 218 1. Disable master ssh connection in shadow config, as it is not working 219 properly in container yet, and produces noise in the log. 220 2. Update AUTOTEST_WEB/host and SERVER/hostname to be the IP of the host 221 if any is set to localhost or 127.0.0.1. Otherwise, set it to be the 222 FQDN of the config value. 223 3. Update SSP/user, which is used as the user makes RPC inside the 224 container. This allows the RPC to pass ACL check as if the call is 225 made in the host. 226 227 """ 228 shadow_config = os.path.join(CONTAINER_AUTOTEST_DIR, 229 'shadow_config.ini') 230 231 # Inject "AUTOSERV/enable_master_ssh: False" in shadow config as 232 # container does not support master ssh connection yet. 233 self.container.attach_run( 234 'echo $\'\n[AUTOSERV]\nenable_master_ssh: False\n\' >> %s' % 235 shadow_config) 236 237 host_ip = lxc_utils.get_host_ip() 238 local_names = ['localhost', '127.0.0.1'] 239 240 db_host = config.get_config_value('AUTOTEST_WEB', 'host') 241 if db_host.lower() in local_names: 242 new_host = host_ip 243 else: 244 new_host = socket.getfqdn(db_host) 245 self.container.attach_run('echo $\'\n[AUTOTEST_WEB]\nhost: %s\n\' >> %s' 246 % (new_host, shadow_config)) 247 248 afe_host = config.get_config_value('SERVER', 'hostname') 249 if afe_host.lower() in local_names: 250 new_host = host_ip 251 else: 252 new_host = socket.getfqdn(afe_host) 253 self.container.attach_run('echo $\'\n[SERVER]\nhostname: %s\n\' >> %s' % 254 (new_host, shadow_config)) 255 256 # Update configurations in SSP section: 257 # user: The user running current process. 258 # is_moblab: True if the autotest server is a Moblab instance. 259 # host_container_ip: IP address of the lxcbr0 interface. Process running 260 # inside container can make RPC through this IP. 261 self.container.attach_run( 262 'echo $\'\n[SSP]\nuser: %s\nis_moblab: %s\n' 263 'host_container_ip: %s\n\' >> %s' % 264 (getpass.getuser(), bool(utils.is_moblab()), 265 lxc_utils.get_host_ip(), shadow_config)) 266 267 268 def _modify_ssh_config(self): 269 """Modify ssh config for it to work inside container. 270 271 This is only called when default ssp_deploy_config is used. If shadow 272 deploy config is manually set up, this function will not be called. 273 Therefore, the source of ssh config must be properly updated to be able 274 to work inside container. 275 276 """ 277 # Remove domain specific flags. 278 ssh_config = '/root/.ssh/config' 279 self.container.attach_run('sed -i \'s/UseProxyIf=false//g\' \'%s\'' % 280 ssh_config) 281 # TODO(dshi): crbug.com/451622 ssh connection loglevel is set to 282 # ERROR in container before master ssh connection works. This is 283 # to avoid logs being flooded with warning `Permanently added 284 # '[hostname]' (RSA) to the list of known hosts.` (crbug.com/478364) 285 # The sed command injects following at the beginning of .ssh/config 286 # used in config. With such change, ssh command will not post 287 # warnings. 288 # Host * 289 # LogLevel Error 290 self.container.attach_run( 291 'sed -i \'1s/^/Host *\\n LogLevel ERROR\\n\\n/\' \'%s\'' % 292 ssh_config) 293 294 # Inject ssh config for moblab to ssh to dut from container. 295 if utils.is_moblab(): 296 # ssh to moblab itself using moblab user. 297 self.container.attach_run( 298 'echo $\'\nHost 192.168.231.1\n User moblab\n ' 299 'IdentityFile %%d/.ssh/testing_rsa\' >> %s' % 300 '/root/.ssh/config') 301 # ssh to duts using root user. 302 self.container.attach_run( 303 'echo $\'\nHost *\n User root\n ' 304 'IdentityFile %%d/.ssh/testing_rsa\' >> %s' % 305 '/root/.ssh/config') 306 307 308 def deploy_pre_start(self): 309 """Deploy configs before the container is started. 310 """ 311 for deploy_config in self.deploy_configs: 312 self._deploy_config_pre_start(deploy_config) 313 314 315 def deploy_post_start(self): 316 """Deploy configs after the container is started. 317 """ 318 for deploy_config in self.deploy_configs: 319 self._deploy_config_post_start(deploy_config) 320 # Autotest shadow config requires special handling to update hostname 321 # of `localhost` with host IP. Shards always use `localhost` as value 322 # of SERVER\hostname and AUTOTEST_WEB\host. 323 self._modify_shadow_config() 324 # Only apply special treatment for files deployed by the default 325 # ssp_deploy_config 326 if not self.is_shadow_config: 327 self._modify_ssh_config() 328