• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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