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 7import time 8 9import common 10from autotest_lib.client.bin import utils 11from autotest_lib.client.common_lib import error 12from autotest_lib.site_utils.lxc import Container 13from autotest_lib.site_utils.lxc import config as lxc_config 14from autotest_lib.site_utils.lxc import constants 15from autotest_lib.site_utils.lxc import lxc 16from autotest_lib.site_utils.lxc import utils as lxc_utils 17from autotest_lib.site_utils.lxc.cleanup_if_fail import cleanup_if_fail 18 19try: 20 from chromite.lib import metrics 21except ImportError: 22 metrics = utils.metrics_mock 23 24 25class ContainerBucket(object): 26 """A wrapper class to interact with containers in a specific container path. 27 """ 28 29 def __init__(self, 30 container_path=constants.DEFAULT_CONTAINER_PATH, 31 shared_host_path = constants.DEFAULT_SHARED_HOST_PATH): 32 """Initialize a ContainerBucket. 33 34 @param container_path: Path to the directory used to store containers. 35 Default is set to AUTOSERV/container_path in 36 global config. 37 """ 38 self.container_path = os.path.realpath(container_path) 39 self.shared_host_path = os.path.realpath(shared_host_path) 40 # Try to create the base container. 41 try: 42 base_container = Container.createFromExistingDir( 43 container_path, constants.BASE); 44 base_container.refresh_status() 45 self.base_container = base_container 46 except error.ContainerError: 47 self.base_container = None 48 49 50 def get_all(self): 51 """Get details of all containers. 52 53 @return: A dictionary of all containers with detailed attributes, 54 indexed by container name. 55 """ 56 info_collection = lxc.get_container_info(self.container_path) 57 containers = {} 58 for info in info_collection: 59 container = Container.createFromExistingDir(self.container_path, 60 **info) 61 containers[container.name] = container 62 return containers 63 64 65 def get(self, name): 66 """Get a container with matching name. 67 68 @param name: Name of the container. 69 70 @return: A container object with matching name. Returns None if no 71 container matches the given name. 72 """ 73 return self.get_all().get(name, None) 74 75 76 def exist(self, name): 77 """Check if a container exists with the given name. 78 79 @param name: Name of the container. 80 81 @return: True if the container with the given name exists, otherwise 82 returns False. 83 """ 84 return self.get(name) != None 85 86 87 def destroy_all(self): 88 """Destroy all containers, base must be destroyed at the last. 89 """ 90 containers = self.get_all().values() 91 for container in sorted( 92 containers, key=lambda n: 1 if n.name == constants.BASE else 0): 93 logging.info('Destroy container %s.', container.name) 94 container.destroy() 95 self._cleanup_shared_host_path() 96 97 98 @metrics.SecondsTimerDecorator( 99 '%s/create_from_base_duration' % constants.STATS_KEY) 100 def create_from_base(self, name, disable_snapshot_clone=False, 101 force_cleanup=False): 102 """Create a container from the base container. 103 104 @param name: Name of the container. 105 @param disable_snapshot_clone: Set to True to force to clone without 106 using snapshot clone even if the host supports that. 107 @param force_cleanup: Force to cleanup existing container. 108 109 @return: A Container object for the created container. 110 111 @raise ContainerError: If the container already exist. 112 @raise error.CmdError: If lxc-clone call failed for any reason. 113 """ 114 if self.exist(name) and not force_cleanup: 115 raise error.ContainerError('Container %s already exists.' % name) 116 117 use_snapshot = (constants.SUPPORT_SNAPSHOT_CLONE and not 118 disable_snapshot_clone) 119 120 try: 121 return Container.clone(src=self.base_container, 122 new_name=name, 123 new_path=self.container_path, 124 snapshot=use_snapshot, 125 cleanup=force_cleanup) 126 except error.CmdError: 127 logging.debug('Creating snapshot clone failed. Attempting without ' 128 'snapshot...') 129 if not use_snapshot: 130 raise 131 else: 132 # Snapshot clone failed, retry clone without snapshot. 133 container = Container.clone(src=self.base_container, 134 new_name=name, 135 new_path=self.container_path, 136 snapshot=False, 137 cleanup=force_cleanup) 138 return container 139 140 141 @cleanup_if_fail() 142 def setup_base(self, name=constants.BASE, force_delete=False): 143 """Setup base container. 144 145 @param name: Name of the base container, default to base. 146 @param force_delete: True to force to delete existing base container. 147 This action will destroy all running test 148 containers. Default is set to False. 149 """ 150 if not self.container_path: 151 raise error.ContainerError( 152 'You must set a valid directory to store containers in ' 153 'global config "AUTOSERV/ container_path".') 154 155 if not os.path.exists(self.container_path): 156 os.makedirs(self.container_path) 157 158 base_path = os.path.join(self.container_path, name) 159 if self.exist(name) and not force_delete: 160 logging.error( 161 'Base container already exists. Set force_delete to True ' 162 'to force to re-stage base container. Note that this ' 163 'action will destroy all running test containers') 164 # Set proper file permission. base container in moblab may have 165 # owner of not being root. Force to update the folder's owner. 166 # TODO(dshi): Change root to current user when test container can be 167 # unprivileged container. 168 utils.run('sudo chown -R root "%s"' % base_path) 169 utils.run('sudo chgrp -R root "%s"' % base_path) 170 return 171 172 # Destroy existing base container if exists. 173 if self.exist(name): 174 # TODO: We may need to destroy all snapshots created from this base 175 # container, not all container. 176 self.destroy_all() 177 178 # Download and untar the base container. 179 tar_path = os.path.join(self.container_path, '%s.tar.xz' % name) 180 path_to_cleanup = [tar_path, base_path] 181 for path in path_to_cleanup: 182 if os.path.exists(path): 183 utils.run('sudo rm -rf "%s"' % path) 184 container_url = constants.CONTAINER_BASE_URL_FMT % name 185 lxc.download_extract(container_url, tar_path, self.container_path) 186 # Remove the downloaded container tar file. 187 utils.run('sudo rm "%s"' % tar_path) 188 # Set proper file permission. 189 # TODO(dshi): Change root to current user when test container can be 190 # unprivileged container. 191 utils.run('sudo chown -R root "%s"' % base_path) 192 utils.run('sudo chgrp -R root "%s"' % base_path) 193 194 # Update container config with container_path from global config. 195 config_path = os.path.join(base_path, 'config') 196 rootfs_path = os.path.join(base_path, 'rootfs') 197 utils.run(('sudo sed ' 198 '-i "s|\(lxc\.rootfs[[:space:]]*=\).*$|\\1 {rootfs}|" ' 199 '"{config}"').format(rootfs=rootfs_path, 200 config=config_path)) 201 202 self.base_container = Container.createFromExistingDir( 203 self.container_path, name) 204 205 self._setup_shared_host_path() 206 207 208 def _setup_shared_host_path(self): 209 """Sets up the shared host directory.""" 210 # First, clear out the old shared host dir if it exists. 211 if lxc_utils.path_exists(self.shared_host_path): 212 self._cleanup_shared_host_path() 213 # Create the dir and set it up as a shared mount point. 214 utils.run(('sudo mkdir "{path}" && ' 215 'sudo mount --bind "{path}" "{path}" && ' 216 'sudo mount --make-unbindable "{path}" && ' 217 'sudo mount --make-shared "{path}"') 218 .format(path=self.shared_host_path)) 219 220 221 def _cleanup_shared_host_path(self): 222 """Removes the shared host directory. 223 224 This should only be called after all containers have been destroyed 225 (i.e. all host mounts have been disconnected and removed, so the shared 226 host directory should be empty). 227 """ 228 if not os.path.exists(self.shared_host_path): 229 return 230 231 if len(os.listdir(self.shared_host_path)) > 0: 232 raise RuntimeError('Attempting to clean up host dir before all ' 233 'hosts have been disconnected') 234 utils.run('sudo umount "{path}" && sudo rmdir "{path}"' 235 .format(path=self.shared_host_path)) 236 237 238 @metrics.SecondsTimerDecorator( 239 '%s/setup_test_duration' % constants.STATS_KEY) 240 @cleanup_if_fail() 241 def setup_test(self, name, job_id, server_package_url, result_path, 242 control=None, skip_cleanup=False, job_folder=None, 243 dut_name=None): 244 """Setup test container for the test job to run. 245 246 The setup includes: 247 1. Install autotest_server package from given url. 248 2. Copy over local shadow_config.ini. 249 3. Mount local site-packages. 250 4. Mount test result directory. 251 252 TODO(dshi): Setup also needs to include test control file for autoserv 253 to run in container. 254 255 @param name: Name of the container. 256 @param job_id: Job id for the test job to run in the test container. 257 @param server_package_url: Url to download autotest_server package. 258 @param result_path: Directory to be mounted to container to store test 259 results. 260 @param control: Path to the control file to run the test job. Default is 261 set to None. 262 @param skip_cleanup: Set to True to skip cleanup, used to troubleshoot 263 container failures. 264 @param job_folder: Folder name of the job, e.g., 123-debug_user. 265 @param dut_name: Name of the dut to run test, used as the hostname of 266 the container. Default is None. 267 @return: A Container object for the test container. 268 269 @raise ContainerError: If container does not exist, or not running. 270 """ 271 start_time = time.time() 272 273 if not os.path.exists(result_path): 274 raise error.ContainerError('Result directory does not exist: %s', 275 result_path) 276 result_path = os.path.abspath(result_path) 277 278 # Save control file to result_path temporarily. The reason is that the 279 # control file in drone_tmp folder can be deleted during scheduler 280 # restart. For test not using SSP, the window between test starts and 281 # control file being picked up by the test is very small (< 2 seconds). 282 # However, for tests using SSP, it takes around 1 minute before the 283 # container is setup. If scheduler is restarted during that period, the 284 # control file will be deleted, and the test will fail. 285 if control: 286 control_file_name = os.path.basename(control) 287 safe_control = os.path.join(result_path, control_file_name) 288 utils.run('cp %s %s' % (control, safe_control)) 289 290 # Create test container from the base container. 291 container = self.create_from_base(name) 292 293 # Update the hostname of the test container to be `dut-name`. 294 # Some TradeFed tests use hostname in test results, which is used to 295 # group test results in dashboard. The default container name is set to 296 # be the name of the folder, which is unique (as it is composed of job 297 # id and timestamp. For better result view, the container's hostname is 298 # set to be a string containing the dut hostname. 299 if dut_name: 300 container.set_hostname(dut_name.replace('.', '-')) 301 302 # Deploy server side package 303 container.install_ssp(server_package_url) 304 305 deploy_config_manager = lxc_config.DeployConfigManager(container) 306 deploy_config_manager.deploy_pre_start() 307 308 # Copy over control file to run the test job. 309 if control: 310 container.install_control_file(safe_control) 311 312 mount_entries = [(constants.SITE_PACKAGES_PATH, 313 constants.CONTAINER_SITE_PACKAGES_PATH, 314 True), 315 (os.path.join(common.autotest_dir, 'puppylab'), 316 os.path.join(constants.CONTAINER_AUTOTEST_DIR, 317 'puppylab'), 318 True), 319 (result_path, 320 os.path.join(constants.RESULT_DIR_FMT % job_folder), 321 False), 322 ] 323 324 # Update container config to mount directories. 325 for source, destination, readonly in mount_entries: 326 container.mount_dir(source, destination, readonly) 327 328 # Update file permissions. 329 # TODO(dshi): crbug.com/459344 Skip following action when test container 330 # can be unprivileged container. 331 autotest_path = os.path.join( 332 container.rootfs, 333 constants.CONTAINER_AUTOTEST_DIR.lstrip(os.path.sep)) 334 utils.run('sudo chown -R root "%s"' % autotest_path) 335 utils.run('sudo chgrp -R root "%s"' % autotest_path) 336 337 container.start(name) 338 deploy_config_manager.deploy_post_start() 339 340 container.modify_import_order() 341 342 container.verify_autotest_setup(job_folder) 343 344 logging.debug('Test container %s is set up.', name) 345 return container 346