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