blob: 8b02e775cf5d89e8b999685b3c563adc9030ddda [file] [log] [blame]
Dan Shi7836d252015-04-27 15:33:58 -07001# 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"""
Dan Shidb9d0532016-06-22 14:35:41 -07006This 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:
Dan Shi7836d252015-04-27 15:33:58 -070010[{
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
Dan Shidb9d0532016-06-22 14:35:41 -070021 },
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
Dan Shi7836d252015-04-27 15:33:58 -070028 }
29]
30
Dan Shidb9d0532016-06-22 14:35:41 -070031Definition of each attribute for config files are as follows:
Dan Shi7836d252015-04-27 15:33:58 -070032source: 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
Dan Shidb9d0532016-06-22 14:35:41 -070039Example:
40{
41 "source": "/etc/resolv.conf",
42 "target": "/etc/resolv.conf",
43 "append": true,
44 "permission": 400
45}
46The above example will:
Dan Shi7836d252015-04-27 15:33:58 -0700471. 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
Dan Shidb9d0532016-06-22 14:35:41 -070052Definition 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
Dan Shi7836d252015-04-27 15:33:58 -070073The 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
Dan Shiff78f112015-06-12 13:34:02 -070089import getpass
Dan Shi7836d252015-04-27 15:33:58 -070090import 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'])
Dan Shidb9d0532016-06-22 14:35:41 -0700116MountConfig = collections.namedtuple(
117 'MountConfig', ['source', 'target', 'mount', 'readonly',
118 'force_create'])
Dan Shi7836d252015-04-27 15:33:58 -0700119
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
Dan Shidb9d0532016-06-22 14:35:41 -0700139 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
Dan Shi7836d252015-04-27 15:33:58 -0700164 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 """
Dan Shidb9d0532016-06-22 14:35:41 -0700178 DeployConfigManager.validate_path(deploy_config)
Dan Shi7836d252015-04-27 15:33:58 -0700179 return DeployConfig(**deploy_config)
180
181
Dan Shidb9d0532016-06-22 14:35:41 -0700182 @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
Dan Shi7836d252015-04-27 15:33:58 -0700207 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)
Dan Shidb9d0532016-06-22 14:35:41 -0700223 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]
Dan Shi5180d552015-05-03 23:24:20 -0700227 self.tmp_append = os.path.join(self.container.rootfs, APPEND_FOLDER)
Dan Shi7836d252015-04-27 15:33:58 -0700228 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:
Dan Shi5180d552015-05-03 23:24:20 -0700250 target = os.path.join(self.container.rootfs,
Dan Shi7836d252015-04-27 15:33:58 -0700251 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.
Dan Shiff78f112015-06-12 13:34:02 -0700301 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.
Dan Shi7836d252015-04-27 15:33:58 -0700304
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
Dan Shi3b2adf62015-09-02 17:46:54 -0700334 # 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.
Dan Shiff78f112015-06-12 13:34:02 -0700339 self.container.attach_run(
Dan Shi3b2adf62015-09-02 17:46:54 -0700340 '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))
Dan Shiff78f112015-06-12 13:34:02 -0700344
Dan Shi7836d252015-04-27 15:33:58 -0700345
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)
Dan Shidb9d0532016-06-22 14:35:41 -0700391 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)
Dan Shi7836d252015-04-27 15:33:58 -0700395
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()