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