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