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