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