[autotest] Add a new config for server-side packaging to mount a directory

With this change, user can add a config in ssp_deploy_config.json (or
ssp_deploy_shadow_config.json for local overide) to mount a directory in the
host onto a directory inside container. For example:
 {
    "source": "/usr/local/autotest/results/shared",
    "target": "/usr/local/autotest/results/shared",
    "mount": true,
    "readonly": false,
    "force_create": true
 }

BUG=chromium:621676
TEST=local run dummy_PassServer with the new config
unittest

Change-Id: I415c22be70d39d29a8a70aabd3d9f1e64a12f2a5
Reviewed-on: https://chromium-review.googlesource.com/355181
Commit-Ready: Dan Shi <dshi@google.com>
Tested-by: Dan Shi <dshi@google.com>
Reviewed-by: Dan Shi <dshi@google.com>
diff --git a/site_utils/gs_offloader.py b/site_utils/gs_offloader.py
index 51aa245..d041cbc 100755
--- a/site_utils/gs_offloader.py
+++ b/site_utils/gs_offloader.py
@@ -377,7 +377,7 @@
 
             sanitize_dir(dir_entry)
             if DEFAULT_CTS_RESULTS_GSURI:
-              upload_testresult_files(dir_entry, multiprocessing)
+                upload_testresult_files(dir_entry, multiprocessing)
 
             if LIMIT_FILE_COUNT:
                 limit_file_count(dir_entry)
diff --git a/site_utils/lxc.py b/site_utils/lxc.py
index ddbdbaf..5ddb6a2 100644
--- a/site_utils/lxc.py
+++ b/site_utils/lxc.py
@@ -918,6 +918,9 @@
                           os.path.join(RESULT_DIR_FMT % job_folder),
                           False),
                         ]
+        for mount_config in deploy_config_manager.mount_configs:
+            mount_entries.append((mount_config.source, mount_config.target,
+                                  mount_config.readonly))
         # Update container config to mount directories.
         for source, destination, readonly in mount_entries:
             container.mount_dir(source, destination, readonly)
diff --git a/site_utils/lxc_config.py b/site_utils/lxc_config.py
index 5093a0c..8b02e77 100644
--- a/site_utils/lxc_config.py
+++ b/site_utils/lxc_config.py
@@ -3,10 +3,10 @@
 # found in the LICENSE file.
 
 """
-This module helps to deploy config files from host to container. It reads
-the settings from a setting file (ssp_deploy_config), and deploy the config
-files based on the settings. The setting file has a json string of a list of
-deployment settings. For example:
+This module helps to deploy config files and shared folders from host to
+container. It reads the settings from a setting file (ssp_deploy_config), and
+deploy the config files based on the settings. The setting file has a json
+string of a list of deployment settings. For example:
 [{
     "source": "/etc/resolv.conf",
     "target": "/etc/resolv.conf",
@@ -18,10 +18,17 @@
     "target": "/root/.ssh",
     "append": false,
     "permission": 400
+ },
+ {
+    "source": "/usr/local/autotest/results/shared",
+    "target": "/usr/local/autotest/results/shared",
+    "mount": true,
+    "readonly": false,
+    "force_create": true
  }
 ]
 
-Definition of each attribute are as follows:
+Definition of each attribute for config files are as follows:
 source: config file in host to be copied to container.
 target: config file's location inside container.
 append: true to append the content of config file to existing file inside
@@ -29,12 +36,40 @@
         be overwritten.
 permission: Permission to set to the config file inside container.
 
-The sample settings will:
+Example:
+{
+    "source": "/etc/resolv.conf",
+    "target": "/etc/resolv.conf",
+    "append": true,
+    "permission": 400
+}
+The above example will:
 1. Append the content of /etc/resolv.conf in host machine to file
    /etc/resolv.conf inside container.
 2. Copy all files in ssh to /root/.ssh in container.
 3. Change all these files' permission to 400
 
+Definition of each attribute for sharing folders are as follows:
+source: a folder in host to be mounted in container.
+target: the folder's location inside container.
+mount: true to mount the source folder onto the target inside container.
+       A setting with false value of mount is invalid.
+readonly: true if the mounted folder inside container should be readonly.
+force_create: true to create the source folder if it doesn't exist.
+
+Example:
+ {
+    "source": "/usr/local/autotest/results/shared",
+    "target": "/usr/local/autotest/results/shared",
+    "mount": true,
+    "readonly": false,
+    "force_create": true
+ }
+The above example will mount folder "/usr/local/autotest/results/shared" in the
+host to path "/usr/local/autotest/results/shared" inside the container. The
+folder can be written to inside container. If the source folder doesn't exist,
+it will be created as `force_create` is set to true.
+
 The setting file (ssp_deploy_config) lives in AUTOTEST_DIR folder.
 For relative file path specified in ssp_deploy_config, AUTOTEST_DIR/containers
 is the parent folder.
@@ -78,6 +113,9 @@
 
 DeployConfig = collections.namedtuple(
         'DeployConfig', ['source', 'target', 'append', 'permission'])
+MountConfig = collections.namedtuple(
+        'MountConfig', ['source', 'target', 'mount', 'readonly',
+                        'force_create'])
 
 
 class SSPDeployError(Exception):
@@ -98,6 +136,31 @@
     """
 
     @staticmethod
+    def validate_path(deploy_config):
+        """Validate the source and target in deploy_config dict.
+
+        @param deploy_config: A dictionary of deploy config to be validated.
+
+        @raise SSPDeployError: If any path in deploy config is invalid.
+        """
+        target = deploy_config['target']
+        source = deploy_config['source']
+        if not os.path.isabs(target):
+            raise SSPDeployError('Target path must be absolute path: %s' %
+                                 target)
+        if not os.path.isabs(source):
+            if source.startswith('~'):
+                # This is to handle the case that the script is run with sudo.
+                inject_user_path = ('~%s%s' % (utils.get_real_user(),
+                                               source[1:]))
+                source = os.path.expanduser(inject_user_path)
+            else:
+                source = os.path.join(common.autotest_dir, source)
+            # Update the source setting in deploy config with the updated path.
+            deploy_config['source'] = source
+
+
+    @staticmethod
     def validate(deploy_config):
         """Validate the deploy config.
 
@@ -112,23 +175,35 @@
         @raise SSPDeployError: If the deploy config is invalid.
 
         """
-        c = DeployConfig(**deploy_config)
-        if not os.path.isabs(c.target):
-            raise SSPDeployError('Target path must be absolute path: %s' %
-                                 c.target)
-        if not os.path.isabs(c.source):
-            if c.source.startswith('~'):
-                # This is to handle the case that the script is run with sudo.
-                inject_user_path = ('~%s%s' % (utils.get_real_user(),
-                                               c.source[1:]))
-                source = os.path.expanduser(inject_user_path)
-            else:
-                source = os.path.join(common.autotest_dir, c.source)
-            deploy_config['source'] = source
-
+        DeployConfigManager.validate_path(deploy_config)
         return DeployConfig(**deploy_config)
 
 
+    @staticmethod
+    def validate_mount(deploy_config):
+        """Validate the deploy config for mounting a directory.
+
+        Deploy configs need to be validated and pre-processed, e.g.,
+        1. Target must be an absolute path.
+        2. Source must be updated to be an absolute path.
+        3. Mount must be true.
+
+        @param deploy_config: A dictionary of deploy config to be validated.
+
+        @return: A DeployConfig object that contains the deploy config.
+
+        @raise SSPDeployError: If the deploy config is invalid.
+
+        """
+        DeployConfigManager.validate_path(deploy_config)
+        c = MountConfig(**deploy_config)
+        if not c.mount:
+            raise SSPDeployError('`mount` must be true.')
+        if not c.force_create and not os.path.exists(c.source):
+            raise SSPDeployError('`source` does not exist.')
+        return c
+
+
     def __init__(self, container):
         """Initialize the deploy config manager.
 
@@ -145,7 +220,10 @@
                        else SSP_DEPLOY_CONFIG_FILE)
         with open(config_file) as f:
             deploy_configs = json.load(f)
-        self.deploy_configs = [self.validate(c) for c in deploy_configs]
+        self.deploy_configs = [self.validate(c) for c in deploy_configs
+                               if 'append' in c]
+        self.mount_configs = [self.validate_mount(c) for c in deploy_configs
+                              if 'mount' in c]
         self.tmp_append = os.path.join(self.container.rootfs, APPEND_FOLDER)
         if lxc_utils.path_exists(self.tmp_append):
             utils.run('sudo rm -rf "%s"' % self.tmp_append)
@@ -310,6 +388,10 @@
         """
         for deploy_config in self.deploy_configs:
             self._deploy_config_pre_start(deploy_config)
+        for mount_config in self.mount_configs:
+            if (mount_config.force_create and
+                not os.path.exists(mount_config.source)):
+                utils.run('mkdir -p %s' % mount_config.source)
 
 
     def deploy_post_start(self):