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 10 11from autotest_lib.client.bin import utils 12from autotest_lib.client.common_lib import error 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 18from autotest_lib.site_utils.lxc.base_image import BaseImage 19from autotest_lib.site_utils.lxc.constants import \ 20 CONTAINER_POOL_METRICS_PREFIX as METRICS_PREFIX 21from autotest_lib.site_utils.lxc.container import Container 22from autotest_lib.site_utils.lxc.container_factory import ContainerFactory 23 24try: 25 from chromite.lib import metrics 26 from infra_libs import ts_mon 27except ImportError: 28 import mock 29 metrics = utils.metrics_mock 30 ts_mon = mock.Mock() 31 32 33class ContainerBucket(object): 34 """A wrapper class to interact with containers in a specific container path. 35 """ 36 37 def __init__(self, container_path=constants.DEFAULT_CONTAINER_PATH, 38 base_name=constants.BASE, container_factory=None): 39 """Initialize a ContainerBucket. 40 41 @param container_path: Path to the directory used to store containers. 42 Default is set to AUTOSERV/container_path in 43 global config. 44 @param base_name: Name of the base container image. Used to initialize a 45 ContainerFactory unless one is provided via the 46 arguments. Defaults to value set via 47 AUTOSERV/container_base_name in global config. 48 @param container_factory: A factory for creating Containers. 49 """ 50 self.container_path = os.path.realpath(container_path) 51 if container_factory is not None: 52 self._factory = container_factory 53 else: 54 # Pass in the container path so that the bucket is hermetic (i.e. so 55 # that if the container path is customized, the base image doesn't 56 # fall back to using the default container path). 57 try: 58 base_image_ok = True 59 container = BaseImage(self.container_path, base_name).get() 60 except error.ContainerError: 61 base_image_ok = False 62 raise 63 finally: 64 metrics.Counter(METRICS_PREFIX + '/base_image', 65 field_spec=[ts_mon.BooleanField('corrupted')] 66 ).increment( 67 fields={'corrupted': not base_image_ok}) 68 self._factory = ContainerFactory( 69 base_container=container, 70 lxc_path=self.container_path) 71 self.container_cache = {} 72 73 74 def get_all(self, force_update=False): 75 """Get details of all containers. 76 77 Retrieves all containers owned by the bucket. Note that this doesn't 78 include the base container, or any containers owned by the container 79 pool. 80 81 @param force_update: Boolean, ignore cached values if set. 82 83 @return: A dictionary of all containers with detailed attributes, 84 indexed by container name. 85 """ 86 logging.debug("Fetching all extant LXC containers") 87 info_collection = lxc.get_container_info(self.container_path) 88 if force_update: 89 logging.debug("Clearing cached container info") 90 containers = {} if force_update else self.container_cache 91 for info in info_collection: 92 if info["name"] in containers: 93 continue 94 container = Container.create_from_existing_dir(self.container_path, 95 **info) 96 # Active containers have an ID. Zygotes and base containers, don't. 97 if container.id is not None: 98 containers[container.id] = container 99 self.container_cache = containers 100 return containers 101 102 103 def get_container(self, container_id): 104 """Get a container with matching name. 105 106 @param container_id: ID of the container. 107 108 @return: A container object with matching name. Returns None if no 109 container matches the given name. 110 """ 111 logging.debug("Fetching LXC container with id %s", container_id) 112 if container_id in self.container_cache: 113 logging.debug("Found container %s in cache", container_id) 114 return self.container_cache[container_id] 115 116 container = self.get_all().get(container_id, None) 117 if None == container: 118 logging.debug("Could not find container %s", container_id) 119 return container 120 121 122 def exist(self, container_id): 123 """Check if a container exists with the given name. 124 125 @param container_id: ID of the container. 126 127 @return: True if the container with the given ID exists, otherwise 128 returns False. 129 """ 130 return self.get_container(container_id) != None 131 132 133 def destroy_all(self): 134 """Destroy all containers, base must be destroyed at the last. 135 """ 136 containers = self.get_all().values() 137 for container in sorted( 138 containers, key=lambda n: 1 if n.name == constants.BASE else 0): 139 key = container.id 140 logging.info('Destroy container %s.', container.name) 141 container.destroy() 142 del self.container_cache[key] 143 144 def scrub_container_location(self, name, 145 timeout=constants.LXC_SCRUB_TIMEOUT): 146 """Destroy a possibly-nonexistent, possibly-malformed container. 147 148 This exists to clean up an unreachable container which may or may not 149 exist and is probably but not definitely malformed if it does exist. It 150 is accordingly scorched-earth and force-destroys the container with all 151 associated snapshots. Also accordingly, this will not raise an 152 exception if the destruction fails. 153 154 @param name: ID of the container. 155 @param timeout: Seconds to wait for removal. 156 157 @returns: CmdResult object from the shell command 158 """ 159 logging.debug( 160 "Force-destroying container %s if it exists, with timeout %s sec", 161 name, timeout) 162 try: 163 result = lxc_utils.destroy( 164 self.container_path, name, 165 force=True, snapshots=True, ignore_status=True, timeout=timeout 166 ) 167 except error.CmdTimeoutError: 168 logging.warning("Force-destruction of container %s timed out.", name) 169 logging.debug("Force-destruction exit code %s", result.exit_status) 170 return result 171 172 173 174 @metrics.SecondsTimerDecorator( 175 '%s/setup_test_duration' % constants.STATS_KEY) 176 @cleanup_if_fail() 177 def setup_test(self, container_id, job_id, server_package_url, result_path, 178 control=None, skip_cleanup=False, job_folder=None, 179 dut_name=None, isolate_hash=None): 180 """Setup test container for the test job to run. 181 182 The setup includes: 183 1. Install autotest_server package from given url. 184 2. Copy over local shadow_config.ini. 185 3. Mount local site-packages. 186 4. Mount test result directory. 187 188 TODO(dshi): Setup also needs to include test control file for autoserv 189 to run in container. 190 191 @param container_id: ID to assign to the test container. 192 @param job_id: Job id for the test job to run in the test container. 193 @param server_package_url: Url to download autotest_server package. 194 @param result_path: Directory to be mounted to container to store test 195 results. 196 @param control: Path to the control file to run the test job. Default is 197 set to None. 198 @param skip_cleanup: Set to True to skip cleanup, used to troubleshoot 199 container failures. 200 @param job_folder: Folder name of the job, e.g., 123-debug_user. 201 @param dut_name: Name of the dut to run test, used as the hostname of 202 the container. Default is None. 203 @param isolate_hash: String key to look up the isolate package needed 204 to run test. Default is None, supersedes 205 server_package_url if present. 206 @return: A Container object for the test container. 207 208 @raise ContainerError: If container does not exist, or not running. 209 """ 210 start_time = time.time() 211 212 if not os.path.exists(result_path): 213 raise error.ContainerError('Result directory does not exist: %s', 214 result_path) 215 result_path = os.path.abspath(result_path) 216 217 # Save control file to result_path temporarily. The reason is that the 218 # control file in drone_tmp folder can be deleted during scheduler 219 # restart. For test not using SSP, the window between test starts and 220 # control file being picked up by the test is very small (< 2 seconds). 221 # However, for tests using SSP, it takes around 1 minute before the 222 # container is setup. If scheduler is restarted during that period, the 223 # control file will be deleted, and the test will fail. 224 if control: 225 control_file_name = os.path.basename(control) 226 safe_control = os.path.join(result_path, control_file_name) 227 utils.run('cp %s %s' % (control, safe_control)) 228 229 # Create test container from the base container. 230 container = self._factory.create_container(container_id) 231 232 # Deploy server side package 233 if isolate_hash: 234 container.install_ssp_isolate(isolate_hash) 235 else: 236 container.install_ssp(server_package_url) 237 238 deploy_config_manager = lxc_config.DeployConfigManager(container) 239 deploy_config_manager.deploy_pre_start() 240 241 # Copy over control file to run the test job. 242 if control: 243 container.install_control_file(safe_control) 244 245 # Use a pre-packaged Trusty-compatible Autotest site_packages 246 # instead if it exists. crbug.com/1013241 247 if os.path.exists(constants.TRUSTY_SITE_PACKAGES_PATH): 248 mount_entries = [(constants.TRUSTY_SITE_PACKAGES_PATH, 249 constants.CONTAINER_SITE_PACKAGES_PATH, 250 True)] 251 else: 252 mount_entries = [(constants.SITE_PACKAGES_PATH, 253 constants.CONTAINER_SITE_PACKAGES_PATH, 254 True)] 255 mount_entries.extend([ 256 (result_path, 257 os.path.join(constants.RESULT_DIR_FMT % job_folder), 258 False), 259 ]) 260 261 # Update container config to mount directories. 262 for source, destination, readonly in mount_entries: 263 container.mount_dir(source, destination, readonly) 264 265 # Update file permissions. 266 # TODO(dshi): crbug.com/459344 Skip following action when test container 267 # can be unprivileged container. 268 autotest_path = os.path.join( 269 container.rootfs, 270 constants.CONTAINER_AUTOTEST_DIR.lstrip(os.path.sep)) 271 utils.run('sudo chown -R root "%s"' % autotest_path) 272 utils.run('sudo chgrp -R root "%s"' % autotest_path) 273 274 container.start(wait_for_network=True) 275 deploy_config_manager.deploy_post_start() 276 277 # Update the hostname of the test container to be `dut-name`. 278 # Some TradeFed tests use hostname in test results, which is used to 279 # group test results in dashboard. The default container name is set to 280 # be the name of the folder, which is unique (as it is composed of job 281 # id and timestamp. For better result view, the container's hostname is 282 # set to be a string containing the dut hostname. 283 if dut_name: 284 container.set_hostname(constants.CONTAINER_UTSNAME_FORMAT % 285 dut_name.replace('.', '-')) 286 287 container.modify_import_order() 288 289 container.verify_autotest_setup(job_folder) 290 291 logging.debug('Test container %s is set up.', container.name) 292 return container 293