Home | History | Annotate | Download | only in site_utils
      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 """
      6 This module helps to deploy config files from host to container. It reads
      7 the settings from a setting file (ssp_deploy_config), and deploy the config
      8 files based on the settings. The setting file has a json string of a list of
      9 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 
     24 Definition of each attribute are as follows:
     25 source: config file in host to be copied to container.
     26 target: config file's location inside container.
     27 append: 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.
     30 permission: Permission to set to the config file inside container.
     31 
     32 The sample settings will:
     33 1. Append the content of /etc/resolv.conf in host machine to file
     34    /etc/resolv.conf inside container.
     35 2. Copy all files in ssh to /root/.ssh in container.
     36 3. Change all these files' permission to 400
     37 
     38 The setting file (ssp_deploy_config) lives in AUTOTEST_DIR folder.
     39 For relative file path specified in ssp_deploy_config, AUTOTEST_DIR/containers
     40 is the parent folder.
     41 The setting file can be overridden by a shadow config, ssp_deploy_shadow_config.
     42 For lab servers, puppet should be used to deploy ssp_deploy_shadow_config to
     43 AUTOTEST_DIR and the configure files to AUTOTEST_DIR/containers.
     44 
     45 The default setting file (ssp_deploy_config) contains
     46 For SSP to work with none-lab servers, e.g., moblab and developer's workstation,
     47 the module still supports copy over files like ssh config and autotest
     48 shadow_config to container when AUTOTEST_DIR/containers/ssp_deploy_config is not
     49 presented.
     50 
     51 """
     52 
     53 import collections
     54 import getpass
     55 import json
     56 import os
     57 import socket
     58 
     59 import common
     60 from autotest_lib.client.bin import utils
     61 from autotest_lib.client.common_lib import global_config
     62 from autotest_lib.client.common_lib import utils
     63 from autotest_lib.site_utils import lxc_utils
     64 
     65 
     66 config = global_config.global_config
     67 
     68 # Path to ssp_deploy_config and ssp_deploy_shadow_config.
     69 SSP_DEPLOY_CONFIG_FILE = os.path.join(common.autotest_dir,
     70                                       'ssp_deploy_config.json')
     71 SSP_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.
     75 APPEND_FOLDER = 'usr/local/ssp_append'
     76 # Path to folder that contains autotest code inside container.
     77 CONTAINER_AUTOTEST_DIR = '/usr/local/autotest'
     78 
     79 DeployConfig = collections.namedtuple(
     80         'DeployConfig', ['source', 'target', 'append', 'permission'])
     81 
     82 
     83 class SSPDeployError(Exception):
     84     """Exception raised if any error occurs when setting up test container."""
     85 
     86 
     87 class 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