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