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