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