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         commands = []
    234         if lxc_utils.path_exists(tmp_append):
    235             commands = ['rm -rf "%s"' % tmp_append]
    236         commands.append('mkdir -p "%s"' % tmp_append)
    237         lxc_utils.sudo_commands(commands)
    238 
    239 
    240     def _deploy_config_pre_start(self, deploy_config):
    241         """Deploy a config before container is started.
    242 
    243         Most configs can be deployed before the container is up. For configs
    244         require a reboot to take effective, they must be deployed in this
    245         function.
    246 
    247         @param deploy_config: Config to be deployed.
    248         """
    249         if not lxc_utils.path_exists(deploy_config.source):
    250             return
    251         # Path to the target file relative to host.
    252         if deploy_config.append:
    253             target = os.path.join(_APPEND_FOLDER,
    254                                   os.path.basename(deploy_config.target))
    255         else:
    256             target = deploy_config.target
    257 
    258         self.container.copy(deploy_config.source, target)
    259 
    260 
    261     def _deploy_config_post_start(self, deploy_config):
    262         """Deploy a config after container is started.
    263 
    264         For configs to be appended after the existing config files in container,
    265         they must be copied to a temp location before container is up (deployed
    266         in function _deploy_config_pre_start). After the container is up, calls
    267         can be made to append the content of such configs to existing config
    268         files.
    269 
    270         @param deploy_config: Config to be deployed.
    271 
    272         """
    273         if deploy_config.append:
    274             source = os.path.join(_APPEND_FOLDER,
    275                                   os.path.basename(deploy_config.target))
    276             self.container.attach_run('cat \'%s\' >> \'%s\'' %
    277                                       (source, deploy_config.target))
    278         self.container.attach_run(
    279                 'chmod -R %s \'%s\'' %
    280                 (deploy_config.permission, deploy_config.target))
    281 
    282 
    283     def _modify_shadow_config(self):
    284         """Update the shadow config used in container with correct values.
    285 
    286         This only applies when no shadow SSP deploy config is applied. For
    287         default SSP deploy config, autotest shadow_config.ini is from autotest
    288         directory, which requires following modification to be able to work in
    289         container. If one chooses to use a shadow SSP deploy config file, the
    290         autotest shadow_config.ini must be from a source with following
    291         modification:
    292         1. Disable master ssh connection in shadow config, as it is not working
    293            properly in container yet, and produces noise in the log.
    294         2. Update AUTOTEST_WEB/host and SERVER/hostname to be the IP of the host
    295            if any is set to localhost or 127.0.0.1. Otherwise, set it to be the
    296            FQDN of the config value.
    297         3. Update SSP/user, which is used as the user makes RPC inside the
    298            container. This allows the RPC to pass ACL check as if the call is
    299            made in the host.
    300 
    301         """
    302         shadow_config = os.path.join(constants.CONTAINER_AUTOTEST_DIR,
    303                                      'shadow_config.ini')
    304 
    305         # Inject "AUTOSERV/enable_master_ssh: False" in shadow config as
    306         # container does not support master ssh connection yet.
    307         self.container.attach_run(
    308                 'echo $\'\n[AUTOSERV]\nenable_master_ssh: False\n\' >> %s' %
    309                 shadow_config)
    310 
    311         host_ip = lxc_utils.get_host_ip()
    312         local_names = ['localhost', '127.0.0.1']
    313 
    314         db_host = config.get_config_value('AUTOTEST_WEB', 'host')
    315         if db_host.lower() in local_names:
    316             new_host = host_ip
    317         else:
    318             new_host = socket.getfqdn(db_host)
    319         self.container.attach_run('echo $\'\n[AUTOTEST_WEB]\nhost: %s\n\' >> %s'
    320                                   % (new_host, shadow_config))
    321 
    322         afe_host = config.get_config_value('SERVER', 'hostname')
    323         if afe_host.lower() in local_names:
    324             new_host = host_ip
    325         else:
    326             new_host = socket.getfqdn(afe_host)
    327         self.container.attach_run('echo $\'\n[SERVER]\nhostname: %s\n\' >> %s' %
    328                                   (new_host, shadow_config))
    329 
    330         # Update configurations in SSP section:
    331         # user: The user running current process.
    332         # is_moblab: True if the autotest server is a Moblab instance.
    333         # host_container_ip: IP address of the lxcbr0 interface. Process running
    334         #     inside container can make RPC through this IP.
    335         self.container.attach_run(
    336                 'echo $\'\n[SSP]\nuser: %s\nis_moblab: %s\n'
    337                 'host_container_ip: %s\n\' >> %s' %
    338                 (getpass.getuser(), bool(utils.is_moblab()),
    339                  lxc_utils.get_host_ip(), shadow_config))
    340 
    341 
    342     def _modify_ssh_config(self):
    343         """Modify ssh config for it to work inside container.
    344 
    345         This is only called when default ssp_deploy_config is used. If shadow
    346         deploy config is manually set up, this function will not be called.
    347         Therefore, the source of ssh config must be properly updated to be able
    348         to work inside container.
    349 
    350         """
    351         # Remove domain specific flags.
    352         ssh_config = '/root/.ssh/config'
    353         self.container.attach_run('sed -i \'s/UseProxyIf=false//g\' \'%s\'' %
    354                                   ssh_config)
    355         # TODO(dshi): crbug.com/451622 ssh connection loglevel is set to
    356         # ERROR in container before master ssh connection works. This is
    357         # to avoid logs being flooded with warning `Permanently added
    358         # '[hostname]' (RSA) to the list of known hosts.` (crbug.com/478364)
    359         # The sed command injects following at the beginning of .ssh/config
    360         # used in config. With such change, ssh command will not post
    361         # warnings.
    362         # Host *
    363         #   LogLevel Error
    364         self.container.attach_run(
    365                 'sed -i \'1s/^/Host *\\n  LogLevel ERROR\\n\\n/\' \'%s\'' %
    366                 ssh_config)
    367 
    368         # Inject ssh config for moblab to ssh to dut from container.
    369         if utils.is_moblab():
    370             # ssh to moblab itself using moblab user.
    371             self.container.attach_run(
    372                     'echo $\'\nHost 192.168.231.1\n  User moblab\n  '
    373                     'IdentityFile %%d/.ssh/testing_rsa\' >> %s' %
    374                     '/root/.ssh/config')
    375             # ssh to duts using root user.
    376             self.container.attach_run(
    377                     'echo $\'\nHost *\n  User root\n  '
    378                     'IdentityFile %%d/.ssh/testing_rsa\' >> %s' %
    379                     '/root/.ssh/config')
    380 
    381 
    382     def deploy_pre_start(self):
    383         """Deploy configs before the container is started.
    384         """
    385         for deploy_config in self.deploy_configs:
    386             self._deploy_config_pre_start(deploy_config)
    387         for mount_config in self.mount_configs:
    388             if (mount_config.force_create and
    389                 not os.path.exists(mount_config.source)):
    390                 utils.run('mkdir -p %s' % mount_config.source)
    391             self.container.mount_dir(mount_config.source,
    392                                      mount_config.target,
    393                                      mount_config.readonly)
    394 
    395 
    396     def deploy_post_start(self):
    397         """Deploy configs after the container is started.
    398         """
    399         for deploy_config in self.deploy_configs:
    400             self._deploy_config_post_start(deploy_config)
    401         # Autotest shadow config requires special handling to update hostname
    402         # of `localhost` with host IP. Shards always use `localhost` as value
    403         # of SERVER\hostname and AUTOTEST_WEB\host.
    404         self._modify_shadow_config()
    405         # Only apply special treatment for files deployed by the default
    406         # ssp_deploy_config
    407         if not self.is_shadow_config:
    408             self._modify_ssh_config()
    409