Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright 2016 - The Android Open Source Project |
| 4 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | # you may not use this file except in compliance with the License. |
| 7 | # You may obtain a copy of the License at |
| 8 | # |
| 9 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | # See the License for the specific language governing permissions and |
| 15 | # limitations under the License. |
| 16 | |
| 17 | """Public Device Driver APIs. |
| 18 | |
| 19 | This module provides public device driver APIs that can be called |
| 20 | as a Python library. |
| 21 | |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 22 | TODO: The following APIs have not been implemented |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 23 | - RebootAVD(ip): |
| 24 | - RegisterSshPubKey(username, key): |
| 25 | - UnregisterSshPubKey(username, key): |
| 26 | - CleanupStaleImages(): |
| 27 | - CleanupStaleDevices(): |
| 28 | """ |
| 29 | |
Kevin Cheng | d9d5f0f | 2018-06-19 14:54:17 -0700 | [diff] [blame] | 30 | from __future__ import print_function |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 31 | import datetime |
| 32 | import logging |
| 33 | import os |
| 34 | |
Kevin Cheng | d9d5f0f | 2018-06-19 14:54:17 -0700 | [diff] [blame] | 35 | # pylint: disable=import-error |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 36 | import dateutil.parser |
| 37 | import dateutil.tz |
| 38 | |
Sam Chiu | 7de3b23 | 2018-12-06 19:45:52 +0800 | [diff] [blame] | 39 | from acloud import errors |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 40 | from acloud.public import avd |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 41 | from acloud.public import report |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 42 | from acloud.public.actions import common_operations |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 43 | from acloud.internal import constants |
| 44 | from acloud.internal.lib import auth |
| 45 | from acloud.internal.lib import android_build_client |
| 46 | from acloud.internal.lib import android_compute_client |
| 47 | from acloud.internal.lib import gstorage_client |
| 48 | from acloud.internal.lib import utils |
| 49 | |
| 50 | logger = logging.getLogger(__name__) |
| 51 | |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 52 | MAX_BATCH_CLEANUP_COUNT = 100 |
| 53 | |
cylan | 6671372 | 2018-10-06 01:38:26 +0800 | [diff] [blame] | 54 | #For gce_x86_phones remote instances: adb port is 5555 and vnc is 6444. |
| 55 | _TARGET_VNC_PORT = 6444 |
| 56 | _TARGET_ADB_PORT = 5555 |
| 57 | _SSH_USER = "root" |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 58 | |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 59 | |
Kevin Cheng | 5c124ec | 2018-05-16 13:28:51 -0700 | [diff] [blame] | 60 | # pylint: disable=invalid-name |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 61 | class AndroidVirtualDevicePool(object): |
| 62 | """A class that manages a pool of devices.""" |
| 63 | |
| 64 | def __init__(self, cfg, devices=None): |
| 65 | self._devices = devices or [] |
| 66 | self._cfg = cfg |
Sam Chiu | 4d9bb4b | 2018-10-26 11:38:23 +0800 | [diff] [blame] | 67 | credentials = auth.CreateCredentials(cfg) |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 68 | self._build_client = android_build_client.AndroidBuildClient( |
| 69 | credentials) |
| 70 | self._storage_client = gstorage_client.StorageClient(credentials) |
| 71 | self._compute_client = android_compute_client.AndroidComputeClient( |
| 72 | cfg, credentials) |
| 73 | |
| 74 | def _CreateGceImageWithBuildInfo(self, build_target, build_id): |
| 75 | """Creates a Gce image using build from Launch Control. |
| 76 | |
| 77 | Clone avd-system.tar.gz of a build to a cache storage bucket |
| 78 | using launch control api. And then create a Gce image. |
| 79 | |
| 80 | Args: |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 81 | build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug" |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 82 | build_id: Build id, a string, e.g. "2263051", "P2804227" |
| 83 | |
| 84 | Returns: |
| 85 | String, name of the Gce image that has been created. |
| 86 | """ |
| 87 | logger.info("Creating a new gce image using build: build_id %s, " |
| 88 | "build_target %s", build_id, build_target) |
| 89 | disk_image_id = utils.GenerateUniqueName( |
| 90 | suffix=self._cfg.disk_image_name) |
| 91 | self._build_client.CopyTo( |
| 92 | build_target, |
| 93 | build_id, |
| 94 | artifact_name=self._cfg.disk_image_name, |
| 95 | destination_bucket=self._cfg.storage_bucket_name, |
| 96 | destination_path=disk_image_id) |
| 97 | disk_image_url = self._storage_client.GetUrl( |
| 98 | self._cfg.storage_bucket_name, disk_image_id) |
| 99 | try: |
| 100 | image_name = self._compute_client.GenerateImageName(build_target, |
| 101 | build_id) |
| 102 | self._compute_client.CreateImage(image_name=image_name, |
| 103 | source_uri=disk_image_url) |
| 104 | finally: |
| 105 | self._storage_client.Delete(self._cfg.storage_bucket_name, |
| 106 | disk_image_id) |
| 107 | return image_name |
| 108 | |
| 109 | def _CreateGceImageWithLocalFile(self, local_disk_image): |
| 110 | """Create a Gce image with a local image file. |
| 111 | |
| 112 | The local disk image can be either a tar.gz file or a |
| 113 | raw vmlinux image. |
| 114 | e.g. /tmp/avd-system.tar.gz or /tmp/android_system_disk_syslinux.img |
| 115 | If a raw vmlinux image is provided, it will be archived into a tar.gz file. |
| 116 | |
| 117 | The final tar.gz file will be uploaded to a cache bucket in storage. |
| 118 | |
| 119 | Args: |
| 120 | local_disk_image: string, path to a local disk image, |
| 121 | |
| 122 | Returns: |
| 123 | String, name of the Gce image that has been created. |
| 124 | |
| 125 | Raises: |
| 126 | DriverError: if a file with an unexpected extension is given. |
| 127 | """ |
| 128 | logger.info("Creating a new gce image from a local file %s", |
| 129 | local_disk_image) |
| 130 | with utils.TempDir() as tempdir: |
| 131 | if local_disk_image.endswith(self._cfg.disk_raw_image_extension): |
| 132 | dest_tar_file = os.path.join(tempdir, |
| 133 | self._cfg.disk_image_name) |
| 134 | utils.MakeTarFile( |
| 135 | src_dict={local_disk_image: self._cfg.disk_raw_image_name}, |
| 136 | dest=dest_tar_file) |
| 137 | local_disk_image = dest_tar_file |
| 138 | elif not local_disk_image.endswith(self._cfg.disk_image_extension): |
| 139 | raise errors.DriverError( |
| 140 | "Wrong local_disk_image type, must be a *%s file or *%s file" |
| 141 | % (self._cfg.disk_raw_image_extension, |
| 142 | self._cfg.disk_image_extension)) |
| 143 | |
| 144 | disk_image_id = utils.GenerateUniqueName( |
| 145 | suffix=self._cfg.disk_image_name) |
| 146 | self._storage_client.Upload( |
| 147 | local_src=local_disk_image, |
| 148 | bucket_name=self._cfg.storage_bucket_name, |
| 149 | object_name=disk_image_id, |
| 150 | mime_type=self._cfg.disk_image_mime_type) |
| 151 | disk_image_url = self._storage_client.GetUrl( |
| 152 | self._cfg.storage_bucket_name, disk_image_id) |
| 153 | try: |
| 154 | image_name = self._compute_client.GenerateImageName() |
| 155 | self._compute_client.CreateImage(image_name=image_name, |
| 156 | source_uri=disk_image_url) |
| 157 | finally: |
| 158 | self._storage_client.Delete(self._cfg.storage_bucket_name, |
| 159 | disk_image_id) |
| 160 | return image_name |
| 161 | |
| 162 | def CreateDevices(self, |
| 163 | num, |
| 164 | build_target=None, |
| 165 | build_id=None, |
| 166 | gce_image=None, |
| 167 | local_disk_image=None, |
| 168 | cleanup=True, |
| 169 | extra_data_disk_size_gb=None, |
| 170 | precreated_data_image=None): |
| 171 | """Creates |num| devices for given build_target and build_id. |
| 172 | |
| 173 | - If gce_image is provided, will use it to create an instance. |
| 174 | - If local_disk_image is provided, will upload it to a temporary |
| 175 | caching storage bucket which is defined by user as |storage_bucket_name| |
| 176 | And then create an gce image with it; and then create an instance. |
| 177 | - If build_target and build_id are provided, will clone the disk image |
| 178 | via launch control to the temporary caching storage bucket. |
| 179 | And then create an gce image with it; and then create an instance. |
| 180 | |
| 181 | Args: |
| 182 | num: Number of devices to create. |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 183 | build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug" |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 184 | build_id: Build id, a string, e.g. "2263051", "P2804227" |
| 185 | gce_image: string, if given, will use this image |
| 186 | instead of creating a new one. |
| 187 | implies cleanup=False. |
| 188 | local_disk_image: string, path to a local disk image, e.g. |
| 189 | /tmp/avd-system.tar.gz |
| 190 | cleanup: boolean, if True clean up compute engine image after creating |
| 191 | the instance. |
| 192 | extra_data_disk_size_gb: Integer, size of extra disk, or None. |
| 193 | precreated_data_image: A string, the image to use for the extra disk. |
| 194 | |
| 195 | Raises: |
| 196 | errors.DriverError: If no source is specified for image creation. |
| 197 | """ |
| 198 | if gce_image: |
| 199 | # GCE image is provided, we can directly move to instance creation. |
| 200 | logger.info("Using existing gce image %s", gce_image) |
| 201 | image_name = gce_image |
| 202 | cleanup = False |
| 203 | elif local_disk_image: |
| 204 | image_name = self._CreateGceImageWithLocalFile(local_disk_image) |
| 205 | elif build_target and build_id: |
| 206 | image_name = self._CreateGceImageWithBuildInfo(build_target, |
| 207 | build_id) |
| 208 | else: |
| 209 | raise errors.DriverError( |
| 210 | "Invalid image source, must specify one of the following: gce_image, " |
| 211 | "local_disk_image, or build_target and build id.") |
| 212 | |
| 213 | # Create GCE instances. |
| 214 | try: |
| 215 | for _ in range(num): |
| 216 | instance = self._compute_client.GenerateInstanceName( |
| 217 | build_target, build_id) |
| 218 | extra_disk_name = None |
| 219 | if extra_data_disk_size_gb > 0: |
| 220 | extra_disk_name = self._compute_client.GetDataDiskName( |
| 221 | instance) |
| 222 | self._compute_client.CreateDisk(extra_disk_name, |
| 223 | precreated_data_image, |
| 224 | extra_data_disk_size_gb) |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 225 | self._compute_client.CreateInstance( |
| 226 | instance=instance, |
| 227 | image_name=image_name, |
| 228 | extra_disk_name=extra_disk_name) |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 229 | ip = self._compute_client.GetInstanceIP(instance) |
| 230 | self.devices.append(avd.AndroidVirtualDevice( |
| 231 | ip=ip, instance_name=instance)) |
| 232 | finally: |
| 233 | if cleanup: |
| 234 | self._compute_client.DeleteImage(image_name) |
| 235 | |
| 236 | def DeleteDevices(self): |
| 237 | """Deletes devices. |
| 238 | |
| 239 | Returns: |
| 240 | A tuple, (deleted, failed, error_msgs) |
| 241 | deleted: A list of names of instances that have been deleted. |
| 242 | faild: A list of names of instances that we fail to delete. |
| 243 | error_msgs: A list of failure messages. |
| 244 | """ |
| 245 | instance_names = [device.instance_name for device in self._devices] |
| 246 | return self._compute_client.DeleteInstances(instance_names, |
| 247 | self._cfg.zone) |
| 248 | |
| 249 | def WaitForBoot(self): |
| 250 | """Waits for all devices to boot up. |
| 251 | |
| 252 | Returns: |
| 253 | A dictionary that contains all the failures. |
| 254 | The key is the name of the instance that fails to boot, |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 255 | the value is an errors.DeviceBoottError object. |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 256 | """ |
| 257 | failures = {} |
| 258 | for device in self._devices: |
| 259 | try: |
| 260 | self._compute_client.WaitForBoot(device.instance_name) |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 261 | except errors.DeviceBootError as e: |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 262 | failures[device.instance_name] = e |
| 263 | return failures |
| 264 | |
| 265 | @property |
| 266 | def devices(self): |
| 267 | """Returns a list of devices in the pool. |
| 268 | |
| 269 | Returns: |
| 270 | A list of devices in the pool. |
| 271 | """ |
| 272 | return self._devices |
| 273 | |
| 274 | |
herbertxue | 07293a3 | 2018-11-05 20:40:11 +0800 | [diff] [blame] | 275 | def AddDeletionResultToReport(report_obj, deleted, failed, error_msgs, |
| 276 | resource_name): |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 277 | """Adds deletion result to a Report object. |
| 278 | |
| 279 | This function will add the following to report.data. |
| 280 | "deleted": [ |
| 281 | {"name": "resource_name", "type": "resource_name"}, |
| 282 | ], |
| 283 | "failed": [ |
| 284 | {"name": "resource_name", "type": "resource_name"}, |
| 285 | ], |
| 286 | This function will append error_msgs to report.errors. |
| 287 | |
| 288 | Args: |
| 289 | report_obj: A Report object. |
| 290 | deleted: A list of names of the resources that have been deleted. |
| 291 | failed: A list of names of the resources that we fail to delete. |
| 292 | error_msgs: A list of error message strings to be added to the report. |
| 293 | resource_name: A string, representing the name of the resource. |
| 294 | """ |
| 295 | for name in deleted: |
| 296 | report_obj.AddData(key="deleted", |
| 297 | value={"name": name, |
| 298 | "type": resource_name}) |
| 299 | for name in failed: |
| 300 | report_obj.AddData(key="failed", |
| 301 | value={"name": name, |
| 302 | "type": resource_name}) |
| 303 | report_obj.AddErrors(error_msgs) |
| 304 | if failed or error_msgs: |
| 305 | report_obj.SetStatus(report.Status.FAIL) |
| 306 | |
| 307 | |
| 308 | def _FetchSerialLogsFromDevices(compute_client, instance_names, output_file, |
| 309 | port): |
| 310 | """Fetch serial logs from a port for a list of devices to a local file. |
| 311 | |
| 312 | Args: |
| 313 | compute_client: An object of android_compute_client.AndroidComputeClient |
| 314 | instance_names: A list of instance names. |
| 315 | output_file: A path to a file ending with "tar.gz" |
| 316 | port: The number of serial port to read from, 0 for serial output, 1 for |
| 317 | logcat. |
| 318 | """ |
| 319 | with utils.TempDir() as tempdir: |
| 320 | src_dict = {} |
| 321 | for instance_name in instance_names: |
| 322 | serial_log = compute_client.GetSerialPortOutput( |
| 323 | instance=instance_name, port=port) |
| 324 | file_name = "%s.log" % instance_name |
| 325 | file_path = os.path.join(tempdir, file_name) |
| 326 | src_dict[file_path] = file_name |
| 327 | with open(file_path, "w") as f: |
| 328 | f.write(serial_log.encode("utf-8")) |
| 329 | utils.MakeTarFile(src_dict, output_file) |
| 330 | |
| 331 | |
Kevin Cheng | 5c124ec | 2018-05-16 13:28:51 -0700 | [diff] [blame] | 332 | # pylint: disable=too-many-locals |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 333 | def CreateAndroidVirtualDevices(cfg, |
| 334 | build_target=None, |
| 335 | build_id=None, |
| 336 | num=1, |
| 337 | gce_image=None, |
| 338 | local_disk_image=None, |
| 339 | cleanup=True, |
| 340 | serial_log_file=None, |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 341 | logcat_file=None, |
Kevin Cheng | 86d43c7 | 2018-08-30 10:59:14 -0700 | [diff] [blame] | 342 | autoconnect=False, |
| 343 | report_internal_ip=False): |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 344 | """Creates one or multiple android devices. |
| 345 | |
| 346 | Args: |
| 347 | cfg: An AcloudConfig instance. |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 348 | build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug" |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 349 | build_id: Build id, a string, e.g. "2263051", "P2804227" |
| 350 | num: Number of devices to create. |
| 351 | gce_image: string, if given, will use this gce image |
| 352 | instead of creating a new one. |
| 353 | implies cleanup=False. |
| 354 | local_disk_image: string, path to a local disk image, e.g. |
| 355 | /tmp/avd-system.tar.gz |
| 356 | cleanup: boolean, if True clean up compute engine image and |
| 357 | disk image in storage after creating the instance. |
| 358 | serial_log_file: A path to a file where serial output should |
Fang Deng | fbef7c9 | 2017-02-08 14:09:34 -0800 | [diff] [blame] | 359 | be saved to. |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 360 | logcat_file: A path to a file where logcat logs should be saved. |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 361 | autoconnect: Create ssh tunnel(s) and adb connect after device creation. |
Kevin Cheng | 86d43c7 | 2018-08-30 10:59:14 -0700 | [diff] [blame] | 362 | report_internal_ip: Boolean to report the internal ip instead of |
| 363 | external ip. |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 364 | |
| 365 | Returns: |
| 366 | A Report instance. |
| 367 | """ |
| 368 | r = report.Report(command="create") |
Sam Chiu | 4d9bb4b | 2018-10-26 11:38:23 +0800 | [diff] [blame] | 369 | credentials = auth.CreateCredentials(cfg) |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 370 | compute_client = android_compute_client.AndroidComputeClient(cfg, |
| 371 | credentials) |
| 372 | try: |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 373 | common_operations.CreateSshKeyPairIfNecessary(cfg) |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 374 | device_pool = AndroidVirtualDevicePool(cfg) |
| 375 | device_pool.CreateDevices( |
| 376 | num, |
| 377 | build_target, |
| 378 | build_id, |
| 379 | gce_image, |
| 380 | local_disk_image, |
| 381 | cleanup, |
| 382 | extra_data_disk_size_gb=cfg.extra_data_disk_size_gb, |
| 383 | precreated_data_image=cfg.precreated_data_image_map.get( |
| 384 | cfg.extra_data_disk_size_gb)) |
| 385 | failures = device_pool.WaitForBoot() |
| 386 | # Write result to report. |
| 387 | for device in device_pool.devices: |
cylan | 6671372 | 2018-10-06 01:38:26 +0800 | [diff] [blame] | 388 | ip = (device.ip.internal if report_internal_ip |
| 389 | else device.ip.external) |
| 390 | device_dict = { |
| 391 | "ip": ip, |
| 392 | "instance_name": device.instance_name |
| 393 | } |
Kevin Cheng | b596388 | 2018-05-09 00:06:27 -0700 | [diff] [blame] | 394 | if autoconnect: |
cylan | 6671372 | 2018-10-06 01:38:26 +0800 | [diff] [blame] | 395 | forwarded_ports = utils.AutoConnect(ip, |
| 396 | cfg.ssh_private_key_path, |
| 397 | _TARGET_VNC_PORT, |
| 398 | _TARGET_ADB_PORT, |
| 399 | _SSH_USER) |
| 400 | device_dict[constants.VNC_PORT] = forwarded_ports.vnc_port |
| 401 | device_dict[constants.ADB_PORT] = forwarded_ports.adb_port |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 402 | if device.instance_name in failures: |
| 403 | r.AddData(key="devices_failing_boot", value=device_dict) |
| 404 | r.AddError(str(failures[device.instance_name])) |
| 405 | else: |
| 406 | r.AddData(key="devices", value=device_dict) |
| 407 | if failures: |
| 408 | r.SetStatus(report.Status.BOOT_FAIL) |
| 409 | else: |
| 410 | r.SetStatus(report.Status.SUCCESS) |
| 411 | |
| 412 | # Dump serial and logcat logs. |
| 413 | if serial_log_file: |
Fang Deng | fbef7c9 | 2017-02-08 14:09:34 -0800 | [diff] [blame] | 414 | _FetchSerialLogsFromDevices( |
| 415 | compute_client, |
| 416 | instance_names=[d.instance_name for d in device_pool.devices], |
| 417 | port=constants.DEFAULT_SERIAL_PORT, |
| 418 | output_file=serial_log_file) |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 419 | if logcat_file: |
Fang Deng | fbef7c9 | 2017-02-08 14:09:34 -0800 | [diff] [blame] | 420 | _FetchSerialLogsFromDevices( |
| 421 | compute_client, |
| 422 | instance_names=[d.instance_name for d in device_pool.devices], |
| 423 | port=constants.LOGCAT_SERIAL_PORT, |
| 424 | output_file=logcat_file) |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 425 | except errors.DriverError as e: |
| 426 | r.AddError(str(e)) |
| 427 | r.SetStatus(report.Status.FAIL) |
| 428 | return r |
| 429 | |
| 430 | |
herbertxue | 07293a3 | 2018-11-05 20:40:11 +0800 | [diff] [blame] | 431 | def DeleteAndroidVirtualDevices(cfg, instance_names, default_report=None): |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 432 | """Deletes android devices. |
| 433 | |
| 434 | Args: |
| 435 | cfg: An AcloudConfig instance. |
| 436 | instance_names: A list of names of the instances to delete. |
herbertxue | 07293a3 | 2018-11-05 20:40:11 +0800 | [diff] [blame] | 437 | default_report: A initialized Report instance. |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 438 | |
| 439 | Returns: |
| 440 | A Report instance. |
| 441 | """ |
herbertxue | 07293a3 | 2018-11-05 20:40:11 +0800 | [diff] [blame] | 442 | r = default_report if default_report else report.Report(command="delete") |
Sam Chiu | 4d9bb4b | 2018-10-26 11:38:23 +0800 | [diff] [blame] | 443 | credentials = auth.CreateCredentials(cfg) |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 444 | compute_client = android_compute_client.AndroidComputeClient(cfg, |
| 445 | credentials) |
| 446 | try: |
| 447 | deleted, failed, error_msgs = compute_client.DeleteInstances( |
| 448 | instance_names, cfg.zone) |
herbertxue | 07293a3 | 2018-11-05 20:40:11 +0800 | [diff] [blame] | 449 | AddDeletionResultToReport( |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 450 | r, deleted, |
| 451 | failed, error_msgs, |
| 452 | resource_name="instance") |
| 453 | if r.status == report.Status.UNKNOWN: |
| 454 | r.SetStatus(report.Status.SUCCESS) |
| 455 | except errors.DriverError as e: |
| 456 | r.AddError(str(e)) |
| 457 | r.SetStatus(report.Status.FAIL) |
| 458 | return r |
| 459 | |
| 460 | |
| 461 | def _FindOldItems(items, cut_time, time_key): |
| 462 | """Finds items from |items| whose timestamp is earlier than |cut_time|. |
| 463 | |
| 464 | Args: |
| 465 | items: A list of items. Each item is a dictionary represent |
| 466 | the properties of the item. It should has a key as noted |
| 467 | by time_key. |
| 468 | cut_time: A datetime.datatime object. |
| 469 | time_key: String, key for the timestamp. |
| 470 | |
| 471 | Returns: |
| 472 | A list of those from |items| whose timestamp is earlier than cut_time. |
| 473 | """ |
| 474 | cleanup_list = [] |
| 475 | for item in items: |
| 476 | t = dateutil.parser.parse(item[time_key]) |
| 477 | if t < cut_time: |
| 478 | cleanup_list.append(item) |
| 479 | return cleanup_list |
| 480 | |
| 481 | |
| 482 | def Cleanup(cfg, expiration_mins): |
| 483 | """Cleans up stale gce images, gce instances, and disk images in storage. |
| 484 | |
| 485 | Args: |
| 486 | cfg: An AcloudConfig instance. |
| 487 | expiration_mins: Integer, resources older than |expiration_mins| will |
| 488 | be cleaned up. |
| 489 | |
| 490 | Returns: |
| 491 | A Report instance. |
| 492 | """ |
| 493 | r = report.Report(command="cleanup") |
| 494 | try: |
| 495 | cut_time = (datetime.datetime.now(dateutil.tz.tzlocal()) - |
| 496 | datetime.timedelta(minutes=expiration_mins)) |
| 497 | logger.info( |
| 498 | "Cleaning up any gce images/instances and cached build artifacts." |
| 499 | "in google storage that are older than %s", cut_time) |
Sam Chiu | 4d9bb4b | 2018-10-26 11:38:23 +0800 | [diff] [blame] | 500 | credentials = auth.CreateCredentials(cfg) |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 501 | compute_client = android_compute_client.AndroidComputeClient( |
| 502 | cfg, credentials) |
| 503 | storage_client = gstorage_client.StorageClient(credentials) |
| 504 | |
| 505 | # Cleanup expired instances |
| 506 | items = compute_client.ListInstances(zone=cfg.zone) |
| 507 | cleanup_list = [ |
| 508 | item["name"] |
| 509 | for item in _FindOldItems(items, cut_time, "creationTimestamp") |
| 510 | ] |
| 511 | logger.info("Found expired instances: %s", cleanup_list) |
| 512 | for i in range(0, len(cleanup_list), MAX_BATCH_CLEANUP_COUNT): |
| 513 | result = compute_client.DeleteInstances( |
| 514 | instances=cleanup_list[i:i + MAX_BATCH_CLEANUP_COUNT], |
| 515 | zone=cfg.zone) |
herbertxue | 07293a3 | 2018-11-05 20:40:11 +0800 | [diff] [blame] | 516 | AddDeletionResultToReport(r, *result, resource_name="instance") |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 517 | |
| 518 | # Cleanup expired images |
| 519 | items = compute_client.ListImages() |
| 520 | skip_list = cfg.precreated_data_image_map.viewvalues() |
| 521 | cleanup_list = [ |
| 522 | item["name"] |
| 523 | for item in _FindOldItems(items, cut_time, "creationTimestamp") |
| 524 | if item["name"] not in skip_list |
| 525 | ] |
| 526 | logger.info("Found expired images: %s", cleanup_list) |
| 527 | for i in range(0, len(cleanup_list), MAX_BATCH_CLEANUP_COUNT): |
| 528 | result = compute_client.DeleteImages( |
| 529 | image_names=cleanup_list[i:i + MAX_BATCH_CLEANUP_COUNT]) |
herbertxue | 07293a3 | 2018-11-05 20:40:11 +0800 | [diff] [blame] | 530 | AddDeletionResultToReport(r, *result, resource_name="image") |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 531 | |
| 532 | # Cleanup expired disks |
| 533 | # Disks should have been attached to instances with autoDelete=True. |
| 534 | # However, sometimes disks may not be auto deleted successfully. |
| 535 | items = compute_client.ListDisks(zone=cfg.zone) |
| 536 | cleanup_list = [ |
| 537 | item["name"] |
| 538 | for item in _FindOldItems(items, cut_time, "creationTimestamp") |
| 539 | if not item.get("users") |
| 540 | ] |
| 541 | logger.info("Found expired disks: %s", cleanup_list) |
| 542 | for i in range(0, len(cleanup_list), MAX_BATCH_CLEANUP_COUNT): |
| 543 | result = compute_client.DeleteDisks( |
| 544 | disk_names=cleanup_list[i:i + MAX_BATCH_CLEANUP_COUNT], |
| 545 | zone=cfg.zone) |
herbertxue | 07293a3 | 2018-11-05 20:40:11 +0800 | [diff] [blame] | 546 | AddDeletionResultToReport(r, *result, resource_name="disk") |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 547 | |
| 548 | # Cleanup expired google storage |
| 549 | items = storage_client.List(bucket_name=cfg.storage_bucket_name) |
| 550 | cleanup_list = [ |
| 551 | item["name"] |
| 552 | for item in _FindOldItems(items, cut_time, "timeCreated") |
| 553 | ] |
| 554 | logger.info("Found expired cached artifacts: %s", cleanup_list) |
| 555 | for i in range(0, len(cleanup_list), MAX_BATCH_CLEANUP_COUNT): |
| 556 | result = storage_client.DeleteFiles( |
| 557 | bucket_name=cfg.storage_bucket_name, |
| 558 | object_names=cleanup_list[i:i + MAX_BATCH_CLEANUP_COUNT]) |
herbertxue | 07293a3 | 2018-11-05 20:40:11 +0800 | [diff] [blame] | 559 | AddDeletionResultToReport( |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 560 | r, *result, resource_name="cached_build_artifact") |
| 561 | |
| 562 | # Everything succeeded, write status to report. |
| 563 | if r.status == report.Status.UNKNOWN: |
| 564 | r.SetStatus(report.Status.SUCCESS) |
| 565 | except errors.DriverError as e: |
| 566 | r.AddError(str(e)) |
| 567 | r.SetStatus(report.Status.FAIL) |
| 568 | return r |
| 569 | |
| 570 | |
| 571 | def AddSshRsa(cfg, user, ssh_rsa_path): |
| 572 | """Add public ssh rsa key to the project. |
| 573 | |
| 574 | Args: |
| 575 | cfg: An AcloudConfig instance. |
| 576 | user: the name of the user which the key belongs to. |
| 577 | ssh_rsa_path: The absolute path to public rsa key. |
| 578 | |
| 579 | Returns: |
| 580 | A Report instance. |
| 581 | """ |
| 582 | r = report.Report(command="sshkey") |
| 583 | try: |
Sam Chiu | 4d9bb4b | 2018-10-26 11:38:23 +0800 | [diff] [blame] | 584 | credentials = auth.CreateCredentials(cfg) |
Keun Soo Yim | b293fdb | 2016-09-21 16:03:44 -0700 | [diff] [blame] | 585 | compute_client = android_compute_client.AndroidComputeClient( |
| 586 | cfg, credentials) |
| 587 | compute_client.AddSshRsa(user, ssh_rsa_path) |
| 588 | r.SetStatus(report.Status.SUCCESS) |
| 589 | except errors.DriverError as e: |
| 590 | r.AddError(str(e)) |
| 591 | r.SetStatus(report.Status.FAIL) |
| 592 | return r |
Fang Deng | cef4b11 | 2017-03-02 11:20:17 -0800 | [diff] [blame] | 593 | |
| 594 | |
| 595 | def CheckAccess(cfg): |
| 596 | """Check if user has access. |
| 597 | |
| 598 | Args: |
| 599 | cfg: An AcloudConfig instance. |
| 600 | """ |
Sam Chiu | 4d9bb4b | 2018-10-26 11:38:23 +0800 | [diff] [blame] | 601 | credentials = auth.CreateCredentials(cfg) |
Fang Deng | cef4b11 | 2017-03-02 11:20:17 -0800 | [diff] [blame] | 602 | compute_client = android_compute_client.AndroidComputeClient( |
Kevin Cheng | 5c124ec | 2018-05-16 13:28:51 -0700 | [diff] [blame] | 603 | cfg, credentials) |
Fang Deng | cef4b11 | 2017-03-02 11:20:17 -0800 | [diff] [blame] | 604 | logger.info("Checking if user has access to project %s", cfg.project) |
| 605 | if not compute_client.CheckAccess(): |
| 606 | logger.error("User does not have access to project %s", cfg.project) |
| 607 | # Print here so that command line user can see it. |
Kevin Cheng | d9d5f0f | 2018-06-19 14:54:17 -0700 | [diff] [blame] | 608 | print("Looks like you do not have access to %s. " % cfg.project) |
Fang Deng | cef4b11 | 2017-03-02 11:20:17 -0800 | [diff] [blame] | 609 | if cfg.project in cfg.no_project_access_msg_map: |
Kevin Cheng | d9d5f0f | 2018-06-19 14:54:17 -0700 | [diff] [blame] | 610 | print(cfg.no_project_access_msg_map[cfg.project]) |