blob: b66689d4724162567950d281f33530dcc2d416cc [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"""
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 json
55import os
56import socket
57
58import common
59from autotest_lib.client.bin import utils
60from autotest_lib.client.common_lib import global_config
61from autotest_lib.client.common_lib import utils
62from autotest_lib.site_utils import lxc_utils
63
64
65config = global_config.global_config
66
67# Path to ssp_deploy_config and ssp_deploy_shadow_config.
68SSP_DEPLOY_CONFIG_FILE = os.path.join(common.autotest_dir,
69 'ssp_deploy_config.json')
70SSP_DEPLOY_SHADOW_CONFIG_FILE = os.path.join(common.autotest_dir,
71 'ssp_deploy_shadow_config.json')
72# A temp folder used to store files to be appended to the files inside
73# container.
74APPEND_FOLDER = 'usr/local/ssp_append'
75# Path to folder that contains autotest code inside container.
76CONTAINER_AUTOTEST_DIR = '/usr/local/autotest'
77
78DeployConfig = collections.namedtuple(
79 'DeployConfig', ['source', 'target', 'append', 'permission'])
80
81
82class SSPDeployError(Exception):
83 """Exception raised if any error occurs when setting up test container."""
84
85
86class DeployConfigManager(object):
87 """An object to deploy config to container.
88
89 The manager retrieves deploy configs from ssp_deploy_config or
90 ssp_deploy_shadow_config, and sets up the container accordingly.
91 For example:
92 1. Copy given config files to specified location inside container.
93 2. Append the content of given config files to specific files inside
94 container.
95 3. Make sure the config files have proper permission inside container.
96
97 """
98
99 @staticmethod
100 def validate(deploy_config):
101 """Validate the deploy config.
102
103 Deploy configs need to be validated and pre-processed, e.g.,
104 1. Target must be an absolute path.
105 2. Source must be updated to be an absolute path.
106
107 @param deploy_config: A dictionary of deploy config to be validated.
108
109 @return: A DeployConfig object that contains the deploy config.
110
111 @raise SSPDeployError: If the deploy config is invalid.
112
113 """
114 c = DeployConfig(**deploy_config)
115 if not os.path.isabs(c.target):
116 raise SSPDeployError('Target path must be absolute path: %s' %
117 c.target)
118 if not os.path.isabs(c.source):
119 if c.source.startswith('~'):
120 # This is to handle the case that the script is run with sudo.
121 inject_user_path = ('~%s%s' % (utils.get_real_user(),
122 c.source[1:]))
123 source = os.path.expanduser(inject_user_path)
124 else:
125 source = os.path.join(common.autotest_dir, c.source)
126 deploy_config['source'] = source
127
128 return DeployConfig(**deploy_config)
129
130
131 def __init__(self, container):
132 """Initialize the deploy config manager.
133
134 @param container: The container needs to deploy config.
135
136 """
137 self.container = container
138 # If shadow config is used, the deployment procedure will skip some
139 # special handling of config file, e.g.,
140 # 1. Set enable_master_ssh to False in autotest shadow config.
141 # 2. Set ssh logleve to ERROR for all hosts.
142 self.is_shadow_config = os.path.exists(SSP_DEPLOY_SHADOW_CONFIG_FILE)
143 config_file = (SSP_DEPLOY_SHADOW_CONFIG_FILE if self.is_shadow_config
144 else SSP_DEPLOY_CONFIG_FILE)
145 with open(config_file) as f:
146 deploy_configs = json.load(f)
147 self.deploy_configs = [self.validate(c) for c in deploy_configs]
Dan Shi5180d552015-05-03 23:24:20 -0700148 self.tmp_append = os.path.join(self.container.rootfs, APPEND_FOLDER)
Dan Shi7836d252015-04-27 15:33:58 -0700149 if lxc_utils.path_exists(self.tmp_append):
150 utils.run('sudo rm -rf "%s"' % self.tmp_append)
151 utils.run('sudo mkdir -p "%s"' % self.tmp_append)
152
153
154 def _deploy_config_pre_start(self, deploy_config):
155 """Deploy a config before container is started.
156
157 Most configs can be deployed before the container is up. For configs
158 require a reboot to take effective, they must be deployed in this
159 function.
160
161 @param deploy_config: Config to be deployed.
162
163 """
164 if not lxc_utils.path_exists(deploy_config.source):
165 return
166 # Path to the target file relative to host.
167 if deploy_config.append:
168 target = os.path.join(self.tmp_append,
169 os.path.basename(deploy_config.target))
170 else:
Dan Shi5180d552015-05-03 23:24:20 -0700171 target = os.path.join(self.container.rootfs,
Dan Shi7836d252015-04-27 15:33:58 -0700172 deploy_config.target[1:])
173 # Recursively copy files/folder to the target. `-L` to always follow
174 # symbolic links in source.
175 target_dir = os.path.dirname(target)
176 if not lxc_utils.path_exists(target_dir):
177 utils.run('sudo mkdir -p "%s"' % target_dir)
178 source = deploy_config.source
179 # Make sure the source ends with `/.` if it's a directory. Otherwise
180 # command cp will not work.
181 if os.path.isdir(source) and source[-1] != '.':
182 source += '/.' if source[-1] != '/' else '.'
183 utils.run('sudo cp -RL "%s" "%s"' % (source, target))
184
185
186 def _deploy_config_post_start(self, deploy_config):
187 """Deploy a config after container is started.
188
189 For configs to be appended after the existing config files in container,
190 they must be copied to a temp location before container is up (deployed
191 in function _deploy_config_pre_start). After the container is up, calls
192 can be made to append the content of such configs to existing config
193 files.
194
195 @param deploy_config: Config to be deployed.
196
197 """
198 if deploy_config.append:
199 source = os.path.join('/', APPEND_FOLDER,
200 os.path.basename(deploy_config.target))
201 self.container.attach_run('cat \'%s\' >> \'%s\'' %
202 (source, deploy_config.target))
203 self.container.attach_run(
204 'chmod -R %s \'%s\'' %
205 (deploy_config.permission, deploy_config.target))
206
207
208 def _modify_shadow_config(self):
209 """Update the shadow config used in container with correct values.
210
211 This only applies when no shadow SSP deploy config is applied. For
212 default SSP deploy config, autotest shadow_config.ini is from autotest
213 directory, which requires following modification to be able to work in
214 container. If one chooses to use a shadow SSP deploy config file, the
215 autotest shadow_config.ini must be from a source with following
216 modification:
217 1. Disable master ssh connection in shadow config, as it is not working
218 properly in container yet, and produces noise in the log.
219 2. Update AUTOTEST_WEB/host and SERVER/hostname to be the IP of the host
220 if any is set to localhost or 127.0.0.1. Otherwise, set it to be the
221 FQDN of the config value.
222
223 """
224 shadow_config = os.path.join(CONTAINER_AUTOTEST_DIR,
225 'shadow_config.ini')
226
227 # Inject "AUTOSERV/enable_master_ssh: False" in shadow config as
228 # container does not support master ssh connection yet.
229 self.container.attach_run(
230 'echo $\'\n[AUTOSERV]\nenable_master_ssh: False\n\' >> %s' %
231 shadow_config)
232
233 host_ip = lxc_utils.get_host_ip()
234 local_names = ['localhost', '127.0.0.1']
235
236 db_host = config.get_config_value('AUTOTEST_WEB', 'host')
237 if db_host.lower() in local_names:
238 new_host = host_ip
239 else:
240 new_host = socket.getfqdn(db_host)
241 self.container.attach_run('echo $\'\n[AUTOTEST_WEB]\nhost: %s\n\' >> %s'
242 % (new_host, shadow_config))
243
244 afe_host = config.get_config_value('SERVER', 'hostname')
245 if afe_host.lower() in local_names:
246 new_host = host_ip
247 else:
248 new_host = socket.getfqdn(afe_host)
249 self.container.attach_run('echo $\'\n[SERVER]\nhostname: %s\n\' >> %s' %
250 (new_host, shadow_config))
251
252
253 def _modify_ssh_config(self):
254 """Modify ssh config for it to work inside container.
255
256 This is only called when default ssp_deploy_config is used. If shadow
257 deploy config is manually set up, this function will not be called.
258 Therefore, the source of ssh config must be properly updated to be able
259 to work inside container.
260
261 """
262 # Remove domain specific flags.
263 ssh_config = '/root/.ssh/config'
264 self.container.attach_run('sed -i \'s/UseProxyIf=false//g\' \'%s\'' %
265 ssh_config)
266 # TODO(dshi): crbug.com/451622 ssh connection loglevel is set to
267 # ERROR in container before master ssh connection works. This is
268 # to avoid logs being flooded with warning `Permanently added
269 # '[hostname]' (RSA) to the list of known hosts.` (crbug.com/478364)
270 # The sed command injects following at the beginning of .ssh/config
271 # used in config. With such change, ssh command will not post
272 # warnings.
273 # Host *
274 # LogLevel Error
275 self.container.attach_run(
276 'sed -i \'1s/^/Host *\\n LogLevel ERROR\\n\\n/\' \'%s\'' %
277 ssh_config)
278
279 # Inject ssh config for moblab to ssh to dut from container.
280 if utils.is_moblab():
281 # ssh to moblab itself using moblab user.
282 self.container.attach_run(
283 'echo $\'\nHost 192.168.231.1\n User moblab\n '
284 'IdentityFile %%d/.ssh/testing_rsa\' >> %s' %
285 '/root/.ssh/config')
286 # ssh to duts using root user.
287 self.container.attach_run(
288 'echo $\'\nHost *\n User root\n '
289 'IdentityFile %%d/.ssh/testing_rsa\' >> %s' %
290 '/root/.ssh/config')
291
292
293 def deploy_pre_start(self):
294 """Deploy configs before the container is started.
295 """
296 for deploy_config in self.deploy_configs:
297 self._deploy_config_pre_start(deploy_config)
298
299
300 def deploy_post_start(self):
301 """Deploy configs after the container is started.
302 """
303 for deploy_config in self.deploy_configs:
304 self._deploy_config_post_start(deploy_config)
305 # Autotest shadow config requires special handling to update hostname
306 # of `localhost` with host IP. Shards always use `localhost` as value
307 # of SERVER\hostname and AUTOTEST_WEB\host.
308 self._modify_shadow_config()
309 # Only apply special treatment for files deployed by the default
310 # ssp_deploy_config
311 if not self.is_shadow_config:
312 self._modify_ssh_config()