• 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 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 getpass
55import 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]
149        self.tmp_append = os.path.join(self.container.rootfs, APPEND_FOLDER)
150        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:
172            target = os.path.join(self.container.rootfs,
173                                  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.
223        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.
226
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
256        # Update configurations in SSP section:
257        # user: The user running current process.
258        # is_moblab: True if the autotest server is a Moblab instance.
259        # host_container_ip: IP address of the lxcbr0 interface. Process running
260        #     inside container can make RPC through this IP.
261        self.container.attach_run(
262                'echo $\'\n[SSP]\nuser: %s\nis_moblab: %s\n'
263                'host_container_ip: %s\n\' >> %s' %
264                (getpass.getuser(), bool(utils.is_moblab()),
265                 lxc_utils.get_host_ip(), shadow_config))
266
267
268    def _modify_ssh_config(self):
269        """Modify ssh config for it to work inside container.
270
271        This is only called when default ssp_deploy_config is used. If shadow
272        deploy config is manually set up, this function will not be called.
273        Therefore, the source of ssh config must be properly updated to be able
274        to work inside container.
275
276        """
277        # Remove domain specific flags.
278        ssh_config = '/root/.ssh/config'
279        self.container.attach_run('sed -i \'s/UseProxyIf=false//g\' \'%s\'' %
280                                  ssh_config)
281        # TODO(dshi): crbug.com/451622 ssh connection loglevel is set to
282        # ERROR in container before master ssh connection works. This is
283        # to avoid logs being flooded with warning `Permanently added
284        # '[hostname]' (RSA) to the list of known hosts.` (crbug.com/478364)
285        # The sed command injects following at the beginning of .ssh/config
286        # used in config. With such change, ssh command will not post
287        # warnings.
288        # Host *
289        #   LogLevel Error
290        self.container.attach_run(
291                'sed -i \'1s/^/Host *\\n  LogLevel ERROR\\n\\n/\' \'%s\'' %
292                ssh_config)
293
294        # Inject ssh config for moblab to ssh to dut from container.
295        if utils.is_moblab():
296            # ssh to moblab itself using moblab user.
297            self.container.attach_run(
298                    'echo $\'\nHost 192.168.231.1\n  User moblab\n  '
299                    'IdentityFile %%d/.ssh/testing_rsa\' >> %s' %
300                    '/root/.ssh/config')
301            # ssh to duts using root user.
302            self.container.attach_run(
303                    'echo $\'\nHost *\n  User root\n  '
304                    'IdentityFile %%d/.ssh/testing_rsa\' >> %s' %
305                    '/root/.ssh/config')
306
307
308    def deploy_pre_start(self):
309        """Deploy configs before the container is started.
310        """
311        for deploy_config in self.deploy_configs:
312            self._deploy_config_pre_start(deploy_config)
313
314
315    def deploy_post_start(self):
316        """Deploy configs after the container is started.
317        """
318        for deploy_config in self.deploy_configs:
319            self._deploy_config_post_start(deploy_config)
320        # Autotest shadow config requires special handling to update hostname
321        # of `localhost` with host IP. Shards always use `localhost` as value
322        # of SERVER\hostname and AUTOTEST_WEB\host.
323        self._modify_shadow_config()
324        # Only apply special treatment for files deployed by the default
325        # ssp_deploy_config
326        if not self.is_shadow_config:
327            self._modify_ssh_config()
328