Home | History | Annotate | Download | only in lxc
      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 and shared folders from host to
      7 container. It reads the settings from a setting file (ssp_deploy_config), and
      8 deploy the config files based on the settings. The setting file has a json
      9 string 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 
     31 Definition of each attribute for config files are as follows:
     32 source: config file in host to be copied to container.
     33 target: config file's location inside container.
     34 append: 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.
     37 permission: Permission to set to the config file inside container.
     38 
     39 Example:
     40 {
     41     "source": "/etc/resolv.conf",
     42     "target": "/etc/resolv.conf",
     43     "append": true,
     44     "permission": 400
     45 }
     46 The above example will:
     47 1. Append the content of /etc/resolv.conf in host machine to file
     48    /etc/resolv.conf inside container.
     49 2. Copy all files in ssh to /root/.ssh in container.
     50 3. Change all these files' permission to 400
     51 
     52 Definition of each attribute for sharing folders are as follows:
     53 source: a folder in host to be mounted in container.
     54 target: the folder's location inside container.
     55 mount: true to mount the source folder onto the target inside container.
     56        A setting with false value of mount is invalid.
     57 readonly: true if the mounted folder inside container should be readonly.
     58 force_create: true to create the source folder if it doesn't exist.
     59 
     60 Example:
     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  }
     68 The above example will mount folder "/usr/local/autotest/results/shared" in the
     69 host to path "/usr/local/autotest/results/shared" inside the container. The
     70 folder can be written to inside container. If the source folder doesn't exist,
     71 it will be created as `force_create` is set to true.
     72 
     73 The setting file (ssp_deploy_config) lives in AUTOTEST_DIR folder.
     74 For relative file path specified in ssp_deploy_config, AUTOTEST_DIR/containers
     75 is the parent folder.
     76 The setting file can be overridden by a shadow config, ssp_deploy_shadow_config.
     77 For lab servers, puppet should be used to deploy ssp_deploy_shadow_config to
     78 AUTOTEST_DIR and the configure files to AUTOTEST_DIR/containers.
     79 
     80 The default setting file (ssp_deploy_config) contains
     81 For SSP to work with none-lab servers, e.g., moblab and developer's workstation,
     82 the module still supports copy over files like ssh config and autotest
     83 shadow_config to container when AUTOTEST_DIR/containers/ssp_deploy_config is not
     84 presented.
     85 
     86 """
     87 
     88 import collections
     89 import getpass
     90 import json
     91 import os
     92 import socket
     93 
     94 import common
     95 from autotest_lib.client.bin import utils
     96 from autotest_lib.client.common_lib import global_config
     97 from autotest_lib.client.common_lib import utils
     98 from autotest_lib.site_utils.lxc import constants
     99 from autotest_lib.site_utils.lxc import utils as lxc_utils
    100 
    101 
    102 config = global_config.global_config
    103 
    104 # Path to ssp_deploy_config and ssp_deploy_shadow_config.
    105 SSP_DEPLOY_CONFIG_FILE = os.path.join(common.autotest_dir,
    106                                       'ssp_deploy_config.json')
    107 SSP_DEPLOY_SHADOW_CONFIG_FILE = os.path.join(common.autotest_dir,
    108                                              'ssp_deploy_shadow_config.json')
    109 # A temp folder used to store files to be appended to the files inside
    110 # container.
    111 APPEND_FOLDER = 'usr/local/ssp_append'
    112 
    113 DeployConfig = collections.namedtuple(
    114         'DeployConfig', ['source', 'target', 'append', 'permission'])
    115 MountConfig = collections.namedtuple(
    116         'MountConfig', ['source', 'target', 'mount', 'readonly',
    117                         'force_create'])
    118 
    119 
    120 class SSPDeployError(Exception):
    121     """Exception raised if any error occurs when setting up test container."""
    122 
    123 
    124 class DeployConfigManager(object):
    125     """An object to deploy config to container.
    126 
    127     The manager retrieves deploy configs from ssp_deploy_config or
    128     ssp_deploy_shadow_config, and sets up the container accordingly.
    129     For example:
    130     1. Copy given config files to specified location inside container.
    131     2. Append the content of given config files to specific files inside
    132        container.
    133     3. Make sure the config files have proper permission inside container.
    134 
    135     """
    136 
    137     @staticmethod
    138     def validate_path(deploy_config):
    139         """Validate the source and target in deploy_config dict.
    140 
    141         @param deploy_config: A dictionary of deploy config to be validated.
    142 
    143         @raise SSPDeployError: If any path in deploy config is invalid.
    144         """
    145         target = deploy_config['target']
    146         source = deploy_config['source']
    147         if not os.path.isabs(target):
    148             raise SSPDeployError('Target path must be absolute path: %s' %
    149                                  target)
    150         if not os.path.isabs(source):
    151             if source.startswith('~'):
    152                 # This is to handle the case that the script is run with sudo.
    153                 inject_user_path = ('~%s%s' % (utils.get_real_user(),
    154                                                source[1:]))
    155                 source = os.path.expanduser(inject_user_path)
    156             else:
    157                 source = os.path.join(common.autotest_dir, source)
    158             # Update the source setting in deploy config with the updated path.
    159             deploy_config['source'] = source
    160 
    161 
    162     @staticmethod
    163     def validate(deploy_config):
    164         """Validate the deploy config.
    165 
    166         Deploy configs need to be validated and pre-processed, e.g.,
    167         1. Target must be an absolute path.
    168         2. Source must be updated to be an absolute path.
    169 
    170         @param deploy_config: A dictionary of deploy config to be validated.
    171 
    172         @return: A DeployConfig object that contains the deploy config.
    173 
    174         @raise SSPDeployError: If the deploy config is invalid.
    175 
    176         """
    177         DeployConfigManager.validate_path(deploy_config)
    178         return DeployConfig(**deploy_config)
    179 
    180 
    181     @staticmethod
    182     def validate_mount(deploy_config):
    183         """Validate the deploy config for mounting a directory.
    184 
    185         Deploy configs need to be validated and pre-processed, e.g.,
    186         1. Target must be an absolute path.
    187         2. Source must be updated to be an absolute path.
    188         3. Mount must be true.
    189 
    190         @param deploy_config: A dictionary of deploy config to be validated.
    191 
    192         @return: A DeployConfig object that contains the deploy config.
    193 
    194         @raise SSPDeployError: If the deploy config is invalid.
    195 
    196         """
    197         DeployConfigManager.validate_path(deploy_config)
    198         c = MountConfig(**deploy_config)
    199         if not c.mount:
    200             raise SSPDeployError('`mount` must be true.')
    201         if not c.force_create and not os.path.exists(c.source):
    202             raise SSPDeployError('`source` does not exist.')
    203         return c
    204 
    205 
    206     def __init__(self, container, config_file=None):
    207         """Initialize the deploy config manager.
    208 
    209         @param container: The container needs to deploy config.
    210         @param config_file: An optional config file.  For testing.
    211         """
    212         self.container = container
    213         # If shadow config is used, the deployment procedure will skip some
    214         # special handling of config file, e.g.,
    215         # 1. Set enable_master_ssh to False in autotest shadow config.
    216         # 2. Set ssh logleve to ERROR for all hosts.
    217         if config_file is None:
    218             self.is_shadow_config = os.path.exists(
    219                     SSP_DEPLOY_SHADOW_CONFIG_FILE)
    220             config_file = (
    221                     SSP_DEPLOY_SHADOW_CONFIG_FILE if self.is_shadow_config
    222                     else SSP_DEPLOY_CONFIG_FILE)
    223         else:
    224             self.is_shadow_config = False
    225 
    226         with open(config_file) as f:
    227             deploy_configs = json.load(f)
    228         self.deploy_configs = [self.validate(c) for c in deploy_configs
    229                                if 'append' in c]
    230         self.mount_configs = [self.validate_mount(c) for c in deploy_configs
    231                               if 'mount' in c]
    232         self.tmp_append = os.path.join(self.container.rootfs, APPEND_FOLDER)
    233         if lxc_utils.path_exists(self.tmp_append):
    234             utils.run('sudo rm -rf "%s"' % self.tmp_append)
    235         utils.run('sudo mkdir -p "%s"' % self.tmp_append)
    236 
    237 
    238     def _deploy_config_pre_start(self, deploy_config):
    239         """Deploy a config before container is started.
    240 
    241         Most configs can be deployed before the container is up. For configs
    242         require a reboot to take effective, they must be deployed in this
    243         function.
    244 
    245         @param deploy_config: Config to be deployed.
    246 
    247         """
    248         if not lxc_utils.path_exists(deploy_config.source):
    249             return
    250         # Path to the target file relative to host.
    251         if deploy_config.append:
    252             target = os.path.join(self.tmp_append,
    253                                   os.path.basename(deploy_config.target))
    254         else:
    255             target = os.path.join(self.container.rootfs,
    256                                   deploy_config.target[1:])
    257         # Recursively copy files/folder to the target. `-L` to always follow
    258         # symbolic links in source.
    259         target_dir = os.path.dirname(target)
    260         if not lxc_utils.path_exists(target_dir):
    261             utils.run('sudo mkdir -p "%s"' % target_dir)
    262         source = deploy_config.source
    263         # Make sure the source ends with `/.` if it's a directory. Otherwise
    264         # command cp will not work.
    265         if os.path.isdir(source) and source[-1] != '.':
    266             source += '/.' if source[-1] != '/' else '.'
    267         utils.run('sudo cp -RL "%s" "%s"' % (source, target))
    268 
    269 
    270     def _deploy_config_post_start(self, deploy_config):
    271         """Deploy a config after container is started.
    272 
    273         For configs to be appended after the existing config files in container,
    274         they must be copied to a temp location before container is up (deployed
    275         in function _deploy_config_pre_start). After the container is up, calls
    276         can be made to append the content of such configs to existing config
    277         files.
    278 
    279         @param deploy_config: Config to be deployed.
    280 
    281         """
    282         if deploy_config.append:
    283             source = os.path.join('/', APPEND_FOLDER,
    284                                   os.path.basename(deploy_config.target))
    285             self.container.attach_run('cat \'%s\' >> \'%s\'' %
    286                                       (source, deploy_config.target))
    287         self.container.attach_run(
    288                 'chmod -R %s \'%s\'' %
    289                 (deploy_config.permission, deploy_config.target))
    290 
    291 
    292     def _modify_shadow_config(self):
    293         """Update the shadow config used in container with correct values.
    294 
    295         This only applies when no shadow SSP deploy config is applied. For
    296         default SSP deploy config, autotest shadow_config.ini is from autotest
    297         directory, which requires following modification to be able to work in
    298         container. If one chooses to use a shadow SSP deploy config file, the
    299         autotest shadow_config.ini must be from a source with following
    300         modification:
    301         1. Disable master ssh connection in shadow config, as it is not working
    302            properly in container yet, and produces noise in the log.
    303         2. Update AUTOTEST_WEB/host and SERVER/hostname to be the IP of the host
    304            if any is set to localhost or 127.0.0.1. Otherwise, set it to be the
    305            FQDN of the config value.
    306         3. Update SSP/user, which is used as the user makes RPC inside the
    307            container. This allows the RPC to pass ACL check as if the call is
    308            made in the host.
    309 
    310         """
    311         shadow_config = os.path.join(constants.CONTAINER_AUTOTEST_DIR,
    312                                      'shadow_config.ini')
    313 
    314         # Inject "AUTOSERV/enable_master_ssh: False" in shadow config as
    315         # container does not support master ssh connection yet.
    316         self.container.attach_run(
    317                 'echo $\'\n[AUTOSERV]\nenable_master_ssh: False\n\' >> %s' %
    318                 shadow_config)
    319 
    320         host_ip = lxc_utils.get_host_ip()
    321         local_names = ['localhost', '127.0.0.1']
    322 
    323         db_host = config.get_config_value('AUTOTEST_WEB', 'host')
    324         if db_host.lower() in local_names:
    325             new_host = host_ip
    326         else:
    327             new_host = socket.getfqdn(db_host)
    328         self.container.attach_run('echo $\'\n[AUTOTEST_WEB]\nhost: %s\n\' >> %s'
    329                                   % (new_host, shadow_config))
    330 
    331         afe_host = config.get_config_value('SERVER', 'hostname')
    332         if afe_host.lower() in local_names:
    333             new_host = host_ip
    334         else:
    335             new_host = socket.getfqdn(afe_host)
    336         self.container.attach_run('echo $\'\n[SERVER]\nhostname: %s\n\' >> %s' %
    337                                   (new_host, shadow_config))
    338 
    339         # Update configurations in SSP section:
    340         # user: The user running current process.
    341         # is_moblab: True if the autotest server is a Moblab instance.
    342         # host_container_ip: IP address of the lxcbr0 interface. Process running
    343         #     inside container can make RPC through this IP.
    344         self.container.attach_run(
    345                 'echo $\'\n[SSP]\nuser: %s\nis_moblab: %s\n'
    346                 'host_container_ip: %s\n\' >> %s' %
    347                 (getpass.getuser(), bool(utils.is_moblab()),
    348                  lxc_utils.get_host_ip(), shadow_config))
    349 
    350 
    351     def _modify_ssh_config(self):
    352         """Modify ssh config for it to work inside container.
    353 
    354         This is only called when default ssp_deploy_config is used. If shadow
    355         deploy config is manually set up, this function will not be called.
    356         Therefore, the source of ssh config must be properly updated to be able
    357         to work inside container.
    358 
    359         """
    360         # Remove domain specific flags.
    361         ssh_config = '/root/.ssh/config'
    362         self.container.attach_run('sed -i \'s/UseProxyIf=false//g\' \'%s\'' %
    363                                   ssh_config)
    364         # TODO(dshi): crbug.com/451622 ssh connection loglevel is set to
    365         # ERROR in container before master ssh connection works. This is
    366         # to avoid logs being flooded with warning `Permanently added
    367         # '[hostname]' (RSA) to the list of known hosts.` (crbug.com/478364)
    368         # The sed command injects following at the beginning of .ssh/config
    369         # used in config. With such change, ssh command will not post
    370         # warnings.
    371         # Host *
    372         #   LogLevel Error
    373         self.container.attach_run(
    374                 'sed -i \'1s/^/Host *\\n  LogLevel ERROR\\n\\n/\' \'%s\'' %
    375                 ssh_config)
    376 
    377         # Inject ssh config for moblab to ssh to dut from container.
    378         if utils.is_moblab():
    379             # ssh to moblab itself using moblab user.
    380             self.container.attach_run(
    381                     'echo $\'\nHost 192.168.231.1\n  User moblab\n  '
    382                     'IdentityFile %%d/.ssh/testing_rsa\' >> %s' %
    383                     '/root/.ssh/config')
    384             # ssh to duts using root user.
    385             self.container.attach_run(
    386                     'echo $\'\nHost *\n  User root\n  '
    387                     'IdentityFile %%d/.ssh/testing_rsa\' >> %s' %
    388                     '/root/.ssh/config')
    389 
    390 
    391     def deploy_pre_start(self):
    392         """Deploy configs before the container is started.
    393         """
    394         for deploy_config in self.deploy_configs:
    395             self._deploy_config_pre_start(deploy_config)
    396         for mount_config in self.mount_configs:
    397             if (mount_config.force_create and
    398                 not os.path.exists(mount_config.source)):
    399                 utils.run('mkdir -p %s' % mount_config.source)
    400             self.container.mount_dir(mount_config.source,
    401                                      mount_config.target,
    402                                      mount_config.readonly)
    403 
    404 
    405     def deploy_post_start(self):
    406         """Deploy configs after the container is started.
    407         """
    408         for deploy_config in self.deploy_configs:
    409             self._deploy_config_post_start(deploy_config)
    410         # Autotest shadow config requires special handling to update hostname
    411         # of `localhost` with host IP. Shards always use `localhost` as value
    412         # of SERVER\hostname and AUTOTEST_WEB\host.
    413         self._modify_shadow_config()
    414         # Only apply special treatment for files deployed by the default
    415         # ssp_deploy_config
    416         if not self.is_shadow_config:
    417             self._modify_ssh_config()
    418