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