1# Copyright 2017 The Chromium OS 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
5import logging
6import os
7
8import common
9from autotest_lib.client.common_lib import error
10from autotest_lib.client.bin import utils
11from autotest_lib.client.common_lib.cros import retry
12from autotest_lib.site_utils.lxc import constants
13from autotest_lib.site_utils.lxc import utils as lxc_utils
14
15
16# Cleaning up the bind mount can sometimes be blocked if a process is active in
17# the directory.  Give cleanup operations about 10 seconds to complete.  This is
18# only an approximate measure.
19_RETRY_MAX_SECONDS = 10
20
21
22class SharedHostDir(object):
23    """A class that manages the shared host directory.
24
25    Instantiating this class sets up a shared host directory at the specified
26    path.  The directory is cleaned up and unmounted when cleanup is called.
27    """
28
29    def __init__(self,
30                 path = constants.DEFAULT_SHARED_HOST_PATH,
31                 force_delete = False):
32        """Sets up the shared host directory.
33
34        @param shared_host_path: The location of the shared host path.
35        @param force_delete: If True, the host dir will be cleared and
36                             reinitialized if it already exists.
37        """
38        self.path = os.path.realpath(path)
39
40        # If the host dir exists and is valid and force_delete is not set, there
41        # is nothing to do.  Otherwise, clear the host dir if it exists, then
42        # recreate it.  Do not use lxc_utils.path_exists as that forces a sudo
43        # call - the SharedHostDir is used all over the place, and
44        # instantiatinng one should not cause the user to have to enter their
45        # password if the host dir already exists.  The host dir is created with
46        # open permissions so it should be accessible without sudo.
47        if os.path.isdir(self.path):
48            if not force_delete and self._host_dir_is_valid():
49                return
50            else:
51                self.cleanup()
52
53        utils.run('sudo mkdir "%(path)s" && '
54                  'sudo chmod 777 "%(path)s" && '
55                  'sudo mount --bind "%(path)s" "%(path)s" && '
56                  'sudo mount --make-shared "%(path)s"' %
57                  {'path': self.path})
58
59
60    def cleanup(self, timeout=_RETRY_MAX_SECONDS):
61        """Removes the shared host directory.
62
63        This should only be called after all containers have been destroyed
64        (i.e. all host mounts have been disconnected and removed, so the shared
65        host directory should be empty).
66
67        @param timeout: Unmounting and deleting the mount point can run into
68                        race conditions vs the kernel sometimes.  This parameter
69                        specifies the number of seconds for which to keep
70                        waiting and retrying the umount/rm commands before
71                        raising a CmdError.  The default of _RETRY_MAX_SECONDS
72                        should work; this parameter is for tests to substitute a
73                        different time out.
74
75        @raises CmdError: If any of the commands involved in unmounting or
76                          deleting the mount point fail even after retries.
77        """
78        if not os.path.exists(self.path):
79            return
80
81        # Unmount and delete everything in the host path.
82        for info in utils.get_mount_info():
83            if lxc_utils.is_subdir(self.path, info.mount_point):
84                utils.run('sudo umount "%s"' % info.mount_point)
85
86        # It's possible that the directory is no longer mounted (e.g. if the
87        # system was rebooted), so check before unmounting.
88        if utils.run('findmnt %s > /dev/null' % self.path,
89                     ignore_status=True).exit_status == 0:
90            self._try_umount(timeout)
91        self._try_rm(timeout)
92
93
94    def _try_umount(self, timeout):
95        """Tries to unmount the shared host dir.
96
97        If the unmount fails, it is retried approximately once a second, for
98        <timeout> seconds.  If the command still fails, a CmdError is raised.
99
100        @param timeout: A timeout in seconds for which to retry the command.
101
102        @raises CmdError: If the command has not succeeded after
103                          _RETRY_MAX_SECONDS.
104        """
105        @retry.retry(error.CmdError, timeout_min=timeout/60.0,
106                     delay_sec=1)
107        def run_with_retry():
108            """Actually unmounts the shared host dir.  Internal function."""
109            utils.run('sudo umount %s' % self.path)
110        run_with_retry()
111
112
113    def _try_rm(self, timeout):
114        """Tries to remove the shared host dir.
115
116        If the rm command fails, it is retried approximately once a second, for
117        <timeout> seconds.  If the command still fails, a CmdError is raised.
118
119        @param timeout: A timeout in seconds for which to retry the command.
120
121        @raises CmdError: If the command has not succeeded after
122                          _RETRY_MAX_SECONDS.
123        """
124        @retry.retry(error.CmdError, timeout_min=timeout/60.0,
125                     delay_sec=1)
126        def run_with_retry():
127            """Actually removes the shared host dir.  Internal function."""
128            utils.run('sudo rm -r "%s"' % self.path)
129        run_with_retry()
130
131
132    def _host_dir_is_valid(self):
133        """Verifies that the shared host directory is set up correctly."""
134        logging.debug('Verifying existing host path: %s', self.path)
135        host_mount = list(utils.get_mount_info(mount_point=self.path))
136
137        # Check that the host mount exists and is shared
138        if host_mount:
139            if 'shared' in host_mount[0].tags:
140                return True
141            else:
142                logging.debug('Host mount not shared (%r).', host_mount)
143        else:
144            logging.debug('Host mount not found.')
145
146        return False
147