blob: cfda6876735215b46feb10e34d6f07cabe3bb9d8 [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"""A client that manages Google Compute Engine.
18
19** ComputeClient **
20
21ComputeClient is a wrapper around Google Compute Engine APIs.
22It provides a set of methods for managing a google compute engine project,
23such as creating images, creating instances, etc.
24
25Design philosophy: We tried to make ComputeClient as stateless as possible,
26and it only keeps states about authentication. ComputeClient should be very
27generic, and only knows how to talk to Compute Engine APIs.
28"""
Fang Dengcef4b112017-03-02 11:20:17 -080029import copy
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070030import functools
31import logging
32import os
33
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070034from acloud.internal.lib import base_cloud_client
35from acloud.internal.lib import utils
36from acloud.public import errors
37
38logger = logging.getLogger(__name__)
39
Kevin Chengb5963882018-05-09 00:06:27 -070040_MAX_RETRIES_ON_FINGERPRINT_CONFLICT = 10
41
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070042
43class OperationScope(object):
44 """Represents operation scope enum."""
45 ZONE = "zone"
46 REGION = "region"
47 GLOBAL = "global"
48
49
Kevin Chengb5963882018-05-09 00:06:27 -070050class PersistentDiskType(object):
51 """Represents different persistent disk types.
52
53 pd-standard for regular hard disk.
54 pd-ssd for solid state disk.
55 """
56 STANDARD = "pd-standard"
57 SSD = "pd-ssd"
58
59
60class ImageStatus(object):
61 """Represents the status of an image."""
62 PENDING = "PENDING"
63 READY = "READY"
64 FAILED = "FAILED"
65
66
67def _IsFingerPrintError(exc):
68 """Determine if the exception is a HTTP error with code 412.
69
70 Args:
71 exc: Exception instance.
72
73 Returns:
74 Boolean. True if the exception is a "Precondition Failed" error.
75 """
76 return isinstance(exc, errors.HttpError) and exc.code == 412
77
78
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070079class ComputeClient(base_cloud_client.BaseCloudApiClient):
80 """Client that manages GCE."""
81
82 # API settings, used by BaseCloudApiClient.
83 API_NAME = "compute"
84 API_VERSION = "v1"
85 SCOPE = " ".join(["https://www.googleapis.com/auth/compute",
86 "https://www.googleapis.com/auth/devstorage.read_write"])
87 # Default settings for gce operations
88 DEFAULT_INSTANCE_SCOPE = [
89 "https://www.googleapis.com/auth/devstorage.read_only",
90 "https://www.googleapis.com/auth/logging.write"
91 ]
Kevin Chengb5963882018-05-09 00:06:27 -070092 OPERATION_TIMEOUT_SECS = 30 * 60 # 30 mins
93 OPERATION_POLL_INTERVAL_SECS = 20
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070094 MACHINE_SIZE_METRICS = ["guestCpus", "memoryMb"]
Fang Dengcef4b112017-03-02 11:20:17 -080095 ACCESS_DENIED_CODE = 403
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070096
97 def __init__(self, acloud_config, oauth2_credentials):
98 """Initialize.
99
100 Args:
101 acloud_config: An AcloudConfig object.
102 oauth2_credentials: An oauth2client.OAuth2Credentials instance.
103 """
104 super(ComputeClient, self).__init__(oauth2_credentials)
105 self._project = acloud_config.project
106
107 def _GetOperationStatus(self, operation, operation_scope, scope_name=None):
108 """Get status of an operation.
109
110 Args:
111 operation: An Operation resource in the format of json.
112 operation_scope: A value from OperationScope, "zone", "region",
113 or "global".
114 scope_name: If operation_scope is "zone" or "region", this should be
115 the name of the zone or region, e.g. "us-central1-f".
116
117 Returns:
118 Status of the operation, one of "DONE", "PENDING", "RUNNING".
119
120 Raises:
121 errors.DriverError: if the operation fails.
122 """
123 operation_name = operation["name"]
124 if operation_scope == OperationScope.GLOBAL:
125 api = self.service.globalOperations().get(project=self._project,
126 operation=operation_name)
127 result = self.Execute(api)
128 elif operation_scope == OperationScope.ZONE:
129 api = self.service.zoneOperations().get(project=self._project,
130 operation=operation_name,
131 zone=scope_name)
132 result = self.Execute(api)
133 elif operation_scope == OperationScope.REGION:
134 api = self.service.regionOperations().get(project=self._project,
135 operation=operation_name,
136 region=scope_name)
137 result = self.Execute(api)
138
139 if result.get("error"):
140 errors_list = result["error"]["errors"]
141 raise errors.DriverError("Get operation state failed, errors: %s" %
142 str(errors_list))
143 return result["status"]
144
145 def WaitOnOperation(self, operation, operation_scope, scope_name=None):
146 """Wait for an operation to finish.
147
148 Args:
149 operation: An Operation resource in the format of json.
150 operation_scope: A value from OperationScope, "zone", "region",
151 or "global".
152 scope_name: If operation_scope is "zone" or "region", this should be
153 the name of the zone or region, e.g. "us-central1-f".
154 """
155 timeout_exception = errors.GceOperationTimeoutError(
156 "Operation hits timeout, did not complete within %d secs." %
157 self.OPERATION_TIMEOUT_SECS)
158 utils.PollAndWait(
159 func=self._GetOperationStatus,
160 expected_return="DONE",
161 timeout_exception=timeout_exception,
162 timeout_secs=self.OPERATION_TIMEOUT_SECS,
163 sleep_interval_secs=self.OPERATION_POLL_INTERVAL_SECS,
164 operation=operation,
165 operation_scope=operation_scope,
166 scope_name=scope_name)
167
168 def GetProject(self):
169 """Get project information.
170
171 Returns:
172 A project resource in json.
173 """
174 api = self.service.projects().get(project=self._project)
175 return self.Execute(api)
176
177 def GetDisk(self, disk_name, zone):
178 """Get disk information.
179
180 Args:
181 disk_name: A string.
182 zone: String, name of zone.
183
184 Returns:
185 An disk resource in json.
186 https://cloud.google.com/compute/docs/reference/latest/disks#resource
187 """
188 api = self.service.disks().get(project=self._project,
189 zone=zone,
190 disk=disk_name)
191 return self.Execute(api)
192
193 def CheckDiskExists(self, disk_name, zone):
194 """Check if disk exists.
195
196 Args:
197 disk_name: A string
198 zone: String, name of zone.
199
200 Returns:
201 True if disk exists, otherwise False.
202 """
203 try:
204 self.GetDisk(disk_name, zone)
205 exists = True
206 except errors.ResourceNotFoundError:
207 exists = False
208 logger.debug("CheckDiskExists: disk_name: %s, result: %s", disk_name,
209 exists)
210 return exists
211
Kevin Chengb5963882018-05-09 00:06:27 -0700212 def CreateDisk(self, disk_name, source_image, size_gb, zone,
213 source_project=None, disk_type=PersistentDiskType.STANDARD):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700214 """Create a gce disk.
215
216 Args:
217 disk_name: A string
Kevin Chengb5963882018-05-09 00:06:27 -0700218 source_image: A string, name of the image.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700219 size_gb: Integer, size in gb.
220 zone: Name of the zone, e.g. us-central1-b.
Kevin Chengb5963882018-05-09 00:06:27 -0700221 source_project: String, required if the image is located in a different
222 project.
223 disk_type: String, a value from PersistentDiskType, STANDARD
224 for regular hard disk or SSD for solid state disk.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700225 """
Kevin Chengb5963882018-05-09 00:06:27 -0700226 source_project = source_project or self._project
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700227 source_image = "projects/%s/global/images/%s" % (
Kevin Chengb5963882018-05-09 00:06:27 -0700228 source_project, source_image) if source_image else None
229 logger.info("Creating disk %s, size_gb: %d, source_image: %s",
230 disk_name, size_gb, str(source_image))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700231 body = {
232 "name": disk_name,
233 "sizeGb": size_gb,
Kevin Chengb5963882018-05-09 00:06:27 -0700234 "type": "projects/%s/zones/%s/diskTypes/%s" % (
235 self._project, zone, disk_type),
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700236 }
237 api = self.service.disks().insert(project=self._project,
238 sourceImage=source_image,
239 zone=zone,
240 body=body)
241 operation = self.Execute(api)
242 try:
243 self.WaitOnOperation(operation=operation,
244 operation_scope=OperationScope.ZONE,
245 scope_name=zone)
246 except errors.DriverError:
247 logger.error("Creating disk failed, cleaning up: %s", disk_name)
248 if self.CheckDiskExists(disk_name, zone):
249 self.DeleteDisk(disk_name, zone)
250 raise
251 logger.info("Disk %s has been created.", disk_name)
252
253 def DeleteDisk(self, disk_name, zone):
254 """Delete a gce disk.
255
256 Args:
257 disk_name: A string, name of disk.
258 zone: A string, name of zone.
259 """
260 logger.info("Deleting disk %s", disk_name)
261 api = self.service.disks().delete(project=self._project,
262 zone=zone,
263 disk=disk_name)
264 operation = self.Execute(api)
265 self.WaitOnOperation(operation=operation,
Kevin Chengb5963882018-05-09 00:06:27 -0700266 operation_scope=OperationScope.ZONE,
267 scope_name=zone)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700268 logger.info("Deleted disk %s", disk_name)
269
270 def DeleteDisks(self, disk_names, zone):
271 """Delete multiple disks.
272
273 Args:
274 disk_names: A list of disk names.
275 zone: A string, name of zone.
276
277 Returns:
278 A tuple, (deleted, failed, error_msgs)
279 deleted: A list of names of disks that have been deleted.
280 failed: A list of names of disks that we fail to delete.
281 error_msgs: A list of failure messages.
282 """
283 if not disk_names:
284 logger.warn("Nothing to delete. Arg disk_names is not provided.")
285 return [], [], []
286 # Batch send deletion requests.
287 logger.info("Deleting disks: %s", disk_names)
288 delete_requests = {}
289 for disk_name in set(disk_names):
290 request = self.service.disks().delete(project=self._project,
291 disk=disk_name,
292 zone=zone)
293 delete_requests[disk_name] = request
294 return self._BatchExecuteAndWait(delete_requests,
Kevin Chengb5963882018-05-09 00:06:27 -0700295 OperationScope.ZONE,
296 scope_name=zone)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700297
298 def ListDisks(self, zone, disk_filter=None):
299 """List disks.
300
301 Args:
302 zone: A string, representing zone name. e.g. "us-central1-f"
303 disk_filter: A string representing a filter in format of
304 FIELD_NAME COMPARISON_STRING LITERAL_STRING
305 e.g. "name ne example-instance"
306 e.g. "name eq "example-instance-[0-9]+""
307
308 Returns:
309 A list of disks.
310 """
311 return self.ListWithMultiPages(api_resource=self.service.disks().list,
312 project=self._project,
313 zone=zone,
314 filter=disk_filter)
315
Kevin Chengb5963882018-05-09 00:06:27 -0700316 def CreateImage(self, image_name, source_uri=None, source_disk=None,
317 labels=None):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700318 """Create a Gce image.
319
320 Args:
321 image_name: A string
322 source_uri: Full Google Cloud Storage URL where the disk image is
Kevin Chengb5963882018-05-09 00:06:27 -0700323 stored. e.g. "https://storage.googleapis.com/my-bucket/
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700324 avd-system-2243663.tar.gz"
Kevin Chengb5963882018-05-09 00:06:27 -0700325 source_disk: String, this should be the disk's selfLink value
326 (including zone and project), rather than the disk_name
327 e.g. https://www.googleapis.com/compute/v1/projects/
328 google.com:android-builds-project/zones/
329 us-east1-d/disks/<disk_name>
330 labels: Dict, will be added to the image's labels.
331
332 Returns:
333 A GlobalOperation resouce.
334 https://cloud.google.com/compute/docs/reference/latest/globalOperations
335
336 Raises:
337 errors.DriverError: For malformed request or response.
338 errors.GceOperationTimeoutError: Operation takes too long to finish.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700339 """
Kevin Chengb5963882018-05-09 00:06:27 -0700340 if (source_uri and source_disk) or (not source_uri and not source_disk):
341 raise errors.DriverError(
342 "Creating image %s requires either source_uri %s or "
343 "source_disk %s but not both" % (image_name, source_uri, source_disk))
344 elif source_uri:
345 logger.info("Creating image %s, source_uri %s", image_name, source_uri)
346 body = {
347 "name": image_name,
348 "rawDisk": {
349 "source": source_uri,
350 },
351 }
352 else:
353 logger.info("Creating image %s, source_disk %s", image_name, source_disk)
354 body = {
355 "name": image_name,
356 "sourceDisk": source_disk,
357 }
358 if labels is not None:
359 body["labels"] = labels
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700360 api = self.service.images().insert(project=self._project, body=body)
361 operation = self.Execute(api)
362 try:
363 self.WaitOnOperation(operation=operation,
364 operation_scope=OperationScope.GLOBAL)
365 except errors.DriverError:
366 logger.error("Creating image failed, cleaning up: %s", image_name)
367 if self.CheckImageExists(image_name):
368 self.DeleteImage(image_name)
369 raise
370 logger.info("Image %s has been created.", image_name)
371
Kevin Chengb5963882018-05-09 00:06:27 -0700372 @utils.RetryOnException(_IsFingerPrintError,
373 _MAX_RETRIES_ON_FINGERPRINT_CONFLICT)
374 def SetImageLabels(self, image_name, new_labels):
375 """Update image's labels. Retry for finger print conflict.
376
377 Note: Decorator RetryOnException will retry the call for FingerPrint
378 conflict (HTTP error code 412). The fingerprint is used to detect
379 conflicts of GCE resource updates. The fingerprint is initially generated
380 by Compute Engine and changes after every request to modify or update
381 resources (e.g. GCE "image" resource has "fingerPrint" for "labels"
382 updates).
383
384 Args:
385 image_name: A string, the image name.
386 new_labels: Dict, will be added to the image's labels.
387
388 Returns:
389 A GlobalOperation resouce.
390 https://cloud.google.com/compute/docs/reference/latest/globalOperations
391 """
392 image = self.GetImage(image_name)
393 labels = image.get("labels", {})
394 labels.update(new_labels)
395 body = {
396 "labels": labels,
397 "labelFingerprint": image["labelFingerprint"]
398 }
399 api = self.service.images().setLabels(project=self._project,
400 resource=image_name, body=body)
401 return self.Execute(api)
402
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700403 def CheckImageExists(self, image_name):
404 """Check if image exists.
405
406 Args:
407 image_name: A string
408
409 Returns:
410 True if image exists, otherwise False.
411 """
412 try:
413 self.GetImage(image_name)
414 exists = True
415 except errors.ResourceNotFoundError:
416 exists = False
417 logger.debug("CheckImageExists: image_name: %s, result: %s",
418 image_name, exists)
419 return exists
420
Kevin Chengb5963882018-05-09 00:06:27 -0700421 def GetImage(self, image_name, image_project=None):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700422 """Get image information.
423
424 Args:
425 image_name: A string
Kevin Chengb5963882018-05-09 00:06:27 -0700426 image_project: A string
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700427
428 Returns:
429 An image resource in json.
430 https://cloud.google.com/compute/docs/reference/latest/images#resource
431 """
Kevin Chengb5963882018-05-09 00:06:27 -0700432 api = self.service.images().get(project=image_project or self._project,
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700433 image=image_name)
434 return self.Execute(api)
435
436 def DeleteImage(self, image_name):
437 """Delete an image.
438
439 Args:
440 image_name: A string
441 """
442 logger.info("Deleting image %s", image_name)
443 api = self.service.images().delete(project=self._project,
444 image=image_name)
445 operation = self.Execute(api)
446 self.WaitOnOperation(operation=operation,
447 operation_scope=OperationScope.GLOBAL)
448 logger.info("Deleted image %s", image_name)
449
450 def DeleteImages(self, image_names):
451 """Delete multiple images.
452
453 Args:
454 image_names: A list of image names.
455
456 Returns:
457 A tuple, (deleted, failed, error_msgs)
458 deleted: A list of names of images that have been deleted.
459 failed: A list of names of images that we fail to delete.
460 error_msgs: A list of failure messages.
461 """
462 if not image_names:
463 return [], [], []
464 # Batch send deletion requests.
465 logger.info("Deleting images: %s", image_names)
466 delete_requests = {}
467 for image_name in set(image_names):
468 request = self.service.images().delete(project=self._project,
469 image=image_name)
470 delete_requests[image_name] = request
471 return self._BatchExecuteAndWait(delete_requests,
472 OperationScope.GLOBAL)
473
Kevin Chengb5963882018-05-09 00:06:27 -0700474 def ListImages(self, image_filter=None, image_project=None):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700475 """List images.
476
477 Args:
478 image_filter: A string representing a filter in format of
479 FIELD_NAME COMPARISON_STRING LITERAL_STRING
480 e.g. "name ne example-image"
481 e.g. "name eq "example-image-[0-9]+""
Kevin Chengb5963882018-05-09 00:06:27 -0700482 image_project: String. If not provided, will list images from the default
483 project. Otherwise, will list images from the given
484 project, which can be any arbitrary project where the
485 account has read access
486 (i.e. has the role "roles/compute.imageUser")
487
488 Read more about image sharing across project:
489 https://cloud.google.com/compute/docs/images/sharing-images-across-projects
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700490
491 Returns:
492 A list of images.
493 """
494 return self.ListWithMultiPages(api_resource=self.service.images().list,
Kevin Chengb5963882018-05-09 00:06:27 -0700495 project=image_project or self._project,
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700496 filter=image_filter)
497
498 def GetInstance(self, instance, zone):
499 """Get information about an instance.
500
501 Args:
502 instance: A string, representing instance name.
503 zone: A string, representing zone name. e.g. "us-central1-f"
504
505 Returns:
506 An instance resource in json.
507 https://cloud.google.com/compute/docs/reference/latest/instances#resource
508 """
509 api = self.service.instances().get(project=self._project,
510 zone=zone,
511 instance=instance)
512 return self.Execute(api)
513
Kevin Chengb5963882018-05-09 00:06:27 -0700514 def AttachAccelerator(self, instance, zone, accelerator_count,
515 accelerator_type):
516 """Attach a GPU accelerator to the instance.
517
518 Note: In order for this to succeed the following must hold:
519 - The machine schedule must be set to "terminate" i.e:
520 SetScheduling(self, instance, zone, on_host_maintenance="terminate")
521 must have been called.
522 - The machine is not starting or running. i.e.
523 StopInstance(self, instance) must have been called.
524
525 Args:
526 instance: A string, representing instance name.
527 zone: String, name of zone.
528 accelerator_count: The number accelerators to be attached to the instance.
529 a value of 0 will detach all accelerators.
530 accelerator_type: The type of accelerator to attach. e.g.
531 "nvidia-tesla-k80"
532 """
533 body = {
534 "guestAccelerators": [{
535 "acceleratorType": self.GetAcceleratorUrl(accelerator_type, zone),
536 "acceleratorCount": accelerator_count
537 }]
538 }
539 api = self.service.instances().setMachineResources(
540 project=self._project, zone=zone, instance=instance, body=body)
541 operation = self.Execute(api)
542 try:
543 self.WaitOnOperation(
544 operation=operation,
545 operation_scope=OperationScope.ZONE,
546 scope_name=zone)
547 except errors.GceOperationTimeoutError:
548 logger.error("Attach instance failed: %s", instance)
549 raise
550 logger.info("%d x %s have been attached to instance %s.", accelerator_count,
551 accelerator_type, instance)
552
553 def AttachDisk(self, instance, zone, **kwargs):
554 """Attach the external disk to the instance.
555
556 Args:
557 instance: A string, representing instance name.
558 zone: String, name of zone.
559 **kwargs: The attachDisk request body. See "https://cloud.google.com/
560 compute/docs/reference/latest/instances/attachDisk" for detail.
561 {
562 "kind": "compute#attachedDisk",
563 "type": string,
564 "mode": string,
565 "source": string,
566 "deviceName": string,
567 "index": integer,
568 "boot": boolean,
569 "initializeParams": {
570 "diskName": string,
571 "sourceImage": string,
572 "diskSizeGb": long,
573 "diskType": string,
574 "sourceImageEncryptionKey": {
575 "rawKey": string,
576 "sha256": string
577 }
578 },
579 "autoDelete": boolean,
580 "licenses": [
581 string
582 ],
583 "interface": string,
584 "diskEncryptionKey": {
585 "rawKey": string,
586 "sha256": string
587 }
588 }
589
590 Returns:
591 An disk resource in json.
592 https://cloud.google.com/compute/docs/reference/latest/disks#resource
593
594
595 Raises:
596 errors.GceOperationTimeoutError: Operation takes too long to finish.
597 """
598 api = self.service.instances().attachDisk(
599 project=self._project, zone=zone, instance=instance,
600 body=kwargs)
601 operation = self.Execute(api)
602 try:
603 self.WaitOnOperation(
604 operation=operation, operation_scope=OperationScope.ZONE,
605 scope_name=zone)
606 except errors.GceOperationTimeoutError:
607 logger.error("Attach instance failed: %s", instance)
608 raise
609 logger.info("Disk has been attached to instance %s.", instance)
610
611 def DetachDisk(self, instance, zone, disk_name):
612 """Attach the external disk to the instance.
613
614 Args:
615 instance: A string, representing instance name.
616 zone: String, name of zone.
617 disk_name: A string, the name of the detach disk.
618
619 Returns:
620 A ZoneOperation resource.
621 See https://cloud.google.com/compute/docs/reference/latest/zoneOperations
622
623 Raises:
624 errors.GceOperationTimeoutError: Operation takes too long to finish.
625 """
626 api = self.service.instances().detachDisk(
627 project=self._project, zone=zone, instance=instance,
628 deviceName=disk_name)
629 operation = self.Execute(api)
630 try:
631 self.WaitOnOperation(
632 operation=operation, operation_scope=OperationScope.ZONE,
633 scope_name=zone)
634 except errors.GceOperationTimeoutError:
635 logger.error("Detach instance failed: %s", instance)
636 raise
637 logger.info("Disk has been detached to instance %s.", instance)
638
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700639 def StartInstance(self, instance, zone):
640 """Start |instance| in |zone|.
641
642 Args:
643 instance: A string, representing instance name.
644 zone: A string, representing zone name. e.g. "us-central1-f"
645
646 Raises:
647 errors.GceOperationTimeoutError: Operation takes too long to finish.
648 """
649 api = self.service.instances().start(project=self._project,
650 zone=zone,
651 instance=instance)
652 operation = self.Execute(api)
653 try:
654 self.WaitOnOperation(operation=operation,
655 operation_scope=OperationScope.ZONE,
656 scope_name=zone)
657 except errors.GceOperationTimeoutError:
658 logger.error("Start instance failed: %s", instance)
659 raise
660 logger.info("Instance %s has been started.", instance)
661
662 def StartInstances(self, instances, zone):
663 """Start |instances| in |zone|.
664
665 Args:
666 instances: A list of strings, representing instance names's list.
667 zone: A string, representing zone name. e.g. "us-central1-f"
668
669 Returns:
670 A tuple, (done, failed, error_msgs)
671 done: A list of string, representing the names of instances that
672 have been executed.
673 failed: A list of string, representing the names of instances that
674 we failed to execute.
675 error_msgs: A list of string, representing the failure messages.
676 """
677 action = functools.partial(self.service.instances().start,
678 project=self._project,
679 zone=zone)
680 return self._BatchExecuteOnInstances(instances, zone, action)
681
682 def StopInstance(self, instance, zone):
683 """Stop |instance| in |zone|.
684
685 Args:
686 instance: A string, representing instance name.
687 zone: A string, representing zone name. e.g. "us-central1-f"
688
689 Raises:
690 errors.GceOperationTimeoutError: Operation takes too long to finish.
691 """
692 api = self.service.instances().stop(project=self._project,
693 zone=zone,
694 instance=instance)
695 operation = self.Execute(api)
696 try:
697 self.WaitOnOperation(operation=operation,
698 operation_scope=OperationScope.ZONE,
699 scope_name=zone)
700 except errors.GceOperationTimeoutError:
701 logger.error("Stop instance failed: %s", instance)
702 raise
703 logger.info("Instance %s has been terminated.", instance)
704
705 def StopInstances(self, instances, zone):
706 """Stop |instances| in |zone|.
707
708 Args:
Kevin Chengb5963882018-05-09 00:06:27 -0700709 instances: A list of strings, representing instance names's list.
710 zone: A string, representing zone name. e.g. "us-central1-f"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700711
712 Returns:
713 A tuple, (done, failed, error_msgs)
714 done: A list of string, representing the names of instances that
715 have been executed.
716 failed: A list of string, representing the names of instances that
717 we failed to execute.
718 error_msgs: A list of string, representing the failure messages.
719 """
720 action = functools.partial(self.service.instances().stop,
721 project=self._project,
722 zone=zone)
723 return self._BatchExecuteOnInstances(instances, zone, action)
724
725 def SetScheduling(self,
726 instance,
727 zone,
728 automatic_restart=True,
729 on_host_maintenance="MIGRATE"):
730 """Update scheduling config |automatic_restart| and |on_host_maintenance|.
731
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700732 Args:
733 instance: A string, representing instance name.
734 zone: A string, representing zone name. e.g. "us-central1-f".
735 automatic_restart: Boolean, determine whether the instance will
736 automatically restart if it crashes or not,
737 default to True.
Kevin Chengb5963882018-05-09 00:06:27 -0700738 on_host_maintenance: enum["MIGRATE", "TERMINATE"]
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700739 The instance's maintenance behavior, which
740 determines whether the instance is live
Kevin Chengb5963882018-05-09 00:06:27 -0700741 "MIGRATE" or "TERMINATE" when there is
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700742 a maintenance event.
743
744 Raises:
745 errors.GceOperationTimeoutError: Operation takes too long to finish.
746 """
747 body = {"automaticRestart": automatic_restart,
Kevin Chengb5963882018-05-09 00:06:27 -0700748 "onHostMaintenance": on_host_maintenance}
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700749 api = self.service.instances().setScheduling(project=self._project,
750 zone=zone,
751 instance=instance,
752 body=body)
753 operation = self.Execute(api)
754 try:
755 self.WaitOnOperation(operation=operation,
756 operation_scope=OperationScope.ZONE,
757 scope_name=zone)
758 except errors.GceOperationTimeoutError:
759 logger.error("Set instance scheduling failed: %s", instance)
760 raise
761 logger.info("Instance scheduling changed:\n"
762 " automaticRestart: %s\n"
763 " onHostMaintenance: %s\n",
764 str(automatic_restart).lower(), on_host_maintenance)
765
766 def ListInstances(self, zone, instance_filter=None):
767 """List instances.
768
769 Args:
770 zone: A string, representing zone name. e.g. "us-central1-f"
771 instance_filter: A string representing a filter in format of
772 FIELD_NAME COMPARISON_STRING LITERAL_STRING
773 e.g. "name ne example-instance"
774 e.g. "name eq "example-instance-[0-9]+""
775
776 Returns:
777 A list of instances.
778 """
779 return self.ListWithMultiPages(
780 api_resource=self.service.instances().list,
781 project=self._project,
782 zone=zone,
783 filter=instance_filter)
784
785 def SetSchedulingInstances(self,
786 instances,
787 zone,
788 automatic_restart=True,
789 on_host_maintenance="MIGRATE"):
790 """Update scheduling config |automatic_restart| and |on_host_maintenance|.
791
792 See //cloud/cluster/api/mixer_instances.proto Scheduling for config option.
793
794 Args:
795 instances: A list of string, representing instance names.
796 zone: A string, representing zone name. e.g. "us-central1-f".
797 automatic_restart: Boolean, determine whether the instance will
798 automatically restart if it crashes or not,
799 default to True.
Kevin Chengb5963882018-05-09 00:06:27 -0700800 on_host_maintenance: enum["MIGRATE", "TERMINATE"]
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700801 The instance's maintenance behavior, which
802 determines whether the instance is live
Kevin Chengb5963882018-05-09 00:06:27 -0700803 migrated or terminated when there is
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700804 a maintenance event.
805
806 Returns:
807 A tuple, (done, failed, error_msgs)
808 done: A list of string, representing the names of instances that
809 have been executed.
810 failed: A list of string, representing the names of instances that
811 we failed to execute.
812 error_msgs: A list of string, representing the failure messages.
813 """
814 body = {"automaticRestart": automatic_restart,
815 "OnHostMaintenance": on_host_maintenance}
816 action = functools.partial(self.service.instances().setScheduling,
817 project=self._project,
818 zone=zone,
819 body=body)
820 return self._BatchExecuteOnInstances(instances, zone, action)
821
822 def _BatchExecuteOnInstances(self, instances, zone, action):
823 """Batch processing operations requiring computing time.
824
825 Args:
826 instances: A list of instance names.
827 zone: A string, e.g. "us-central1-f".
828 action: partial func, all kwargs for this gcloud action has been
829 defined in the caller function (e.g. See "StartInstances")
830 except 'instance' which will be defined by iterating the
831 |instances|.
832
833 Returns:
834 A tuple, (done, failed, error_msgs)
835 done: A list of string, representing the names of instances that
836 have been executed.
837 failed: A list of string, representing the names of instances that
838 we failed to execute.
839 error_msgs: A list of string, representing the failure messages.
840 """
841 if not instances:
842 return [], [], []
843 # Batch send requests.
844 logger.info("Batch executing instances: %s", instances)
845 requests = {}
846 for instance_name in set(instances):
847 requests[instance_name] = action(instance=instance_name)
848 return self._BatchExecuteAndWait(requests,
849 operation_scope=OperationScope.ZONE,
850 scope_name=zone)
851
852 def _BatchExecuteAndWait(self, requests, operation_scope, scope_name=None):
853 """Batch processing requests and wait on the operation.
854
855 Args:
Kevin Chengb5963882018-05-09 00:06:27 -0700856 requests: A dictionary. The key is a string representing the resource
857 name. For example, an instance name, or an image name.
858 operation_scope: A value from OperationScope, "zone", "region",
859 or "global".
860 scope_name: If operation_scope is "zone" or "region", this should be
861 the name of the zone or region, e.g. "us-central1-f".
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700862 Returns:
Kevin Chengb5963882018-05-09 00:06:27 -0700863 A tuple, (done, failed, error_msgs)
864 done: A list of string, representing the resource names that have
865 been executed.
866 failed: A list of string, representing resource names that
867 we failed to execute.
868 error_msgs: A list of string, representing the failure messages.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700869 """
870 results = self.BatchExecute(requests)
871 # Initialize return values
872 failed = []
873 error_msgs = []
874 for resource_name, (_, error) in results.iteritems():
875 if error is not None:
876 failed.append(resource_name)
877 error_msgs.append(str(error))
878 done = []
879 # Wait for the executing operations to finish.
880 logger.info("Waiting for executing operations")
881 for resource_name in requests.iterkeys():
882 operation, _ = results[resource_name]
883 if operation:
884 try:
885 self.WaitOnOperation(operation, operation_scope,
886 scope_name)
887 done.append(resource_name)
888 except errors.DriverError as exc:
889 failed.append(resource_name)
890 error_msgs.append(str(exc))
891 return done, failed, error_msgs
892
893 def ListZones(self):
894 """List all zone instances in the project.
895
896 Returns:
897 Gcompute response instance. For example:
898 {
899 "id": "projects/google.com%3Aandroid-build-staging/zones",
900 "kind": "compute#zoneList",
901 "selfLink": "https://www.googleapis.com/compute/v1/projects/"
902 "google.com:android-build-staging/zones"
903 "items": [
904 {
905 'creationTimestamp': '2014-07-15T10:44:08.663-07:00',
906 'description': 'asia-east1-c',
907 'id': '2222',
908 'kind': 'compute#zone',
909 'name': 'asia-east1-c',
910 'region': 'https://www.googleapis.com/compute/v1/projects/'
911 'google.com:android-build-staging/regions/asia-east1',
912 'selfLink': 'https://www.googleapis.com/compute/v1/projects/'
913 'google.com:android-build-staging/zones/asia-east1-c',
914 'status': 'UP'
915 }, {
916 'creationTimestamp': '2014-05-30T18:35:16.575-07:00',
917 'description': 'asia-east1-b',
918 'id': '2221',
919 'kind': 'compute#zone',
920 'name': 'asia-east1-b',
921 'region': 'https://www.googleapis.com/compute/v1/projects/'
922 'google.com:android-build-staging/regions/asia-east1',
923 'selfLink': 'https://www.googleapis.com/compute/v1/projects'
924 '/google.com:android-build-staging/zones/asia-east1-b',
925 'status': 'UP'
926 }]
927 }
928 See cloud cluster's api/mixer_zones.proto
929 """
930 api = self.service.zones().list(project=self._project)
931 return self.Execute(api)
932
Kevin Chengb5963882018-05-09 00:06:27 -0700933 def ListRegions(self):
934 """List all the regions for a project.
935
936 Returns:
937 A dictionary containing all the zones and additional data. See this link
938 for the detailed response:
939 https://cloud.google.com/compute/docs/reference/latest/regions/list.
940 Example:
941 {
942 'items': [{
943 'name':
944 'us-central1',
945 'quotas': [{
946 'usage': 2.0,
947 'limit': 24.0,
948 'metric': 'CPUS'
949 }, {
950 'usage': 1.0,
951 'limit': 23.0,
952 'metric': 'IN_USE_ADDRESSES'
953 }, {
954 'usage': 209.0,
955 'limit': 10240.0,
956 'metric': 'DISKS_TOTAL_GB'
957 }, {
958 'usage': 1000.0,
959 'limit': 20000.0,
960 'metric': 'INSTANCES'
961 }]
962 },..]
963 }
964 """
965 api = self.service.regions().list(project=self._project)
966 return self.Execute(api)
967
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700968 def _GetNetworkArgs(self, network):
969 """Helper to generate network args that is used to create an instance.
970
971 Args:
972 network: A string, e.g. "default".
973
974 Returns:
975 A dictionary representing network args.
976 """
977 return {
978 "network": self.GetNetworkUrl(network),
979 "accessConfigs": [{"name": "External NAT",
980 "type": "ONE_TO_ONE_NAT"}]
981 }
982
Kevin Chengb5963882018-05-09 00:06:27 -0700983 def _GetDiskArgs(self, disk_name, image_name, image_project=None):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700984 """Helper to generate disk args that is used to create an instance.
985
986 Args:
987 disk_name: A string
988 image_name: A string
Kevin Chengb5963882018-05-09 00:06:27 -0700989 image_project: A string
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700990
991 Returns:
992 A dictionary representing disk args.
993 """
994 return [{
995 "type": "PERSISTENT",
996 "boot": True,
997 "mode": "READ_WRITE",
998 "autoDelete": True,
999 "initializeParams": {
1000 "diskName": disk_name,
Kevin Chengb5963882018-05-09 00:06:27 -07001001 "sourceImage": self.GetImage(image_name, image_project)["selfLink"],
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001002 },
1003 }]
1004
1005 def CreateInstance(self,
1006 instance,
1007 image_name,
1008 machine_type,
1009 metadata,
1010 network,
1011 zone,
Kevin Chengb5963882018-05-09 00:06:27 -07001012 disk_args=None,
1013 image_project=None,
1014 gpu=None):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001015 """Create a gce instance with a gce image.
1016
1017 Args:
Kevin Chengb5963882018-05-09 00:06:27 -07001018 instance: instance name.
1019 image_name: A source image used to create this disk.
1020 machine_type: A string, representing machine_type, e.g. "n1-standard-1"
1021 metadata: A dictionary that maps a metadata name to its value.
1022 network: A string, representing network name, e.g. "default"
1023 zone: A string, representing zone name, e.g. "us-central1-f"
1024 disk_args: A list of extra disk args, see _GetDiskArgs for example,
1025 if None, will create a disk using the given image.
1026 image_project: A string, name of the project where the image belongs.
1027 Assume the default project if None.
1028 gpu: The type of gpu to attach. e.g. "nvidia-tesla-k80", if None no
1029 gpus will be attached. For more details see:
1030 https://cloud.google.com/compute/docs/gpus/add-gpus
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001031 """
Kevin Chengb5963882018-05-09 00:06:27 -07001032 disk_args = (disk_args or self._GetDiskArgs(instance, image_name,
1033 image_project))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001034 body = {
1035 "machineType": self.GetMachineType(machine_type, zone)["selfLink"],
1036 "name": instance,
1037 "networkInterfaces": [self._GetNetworkArgs(network)],
1038 "disks": disk_args,
1039 "serviceAccounts": [
1040 {"email": "default",
Kevin Chengb5963882018-05-09 00:06:27 -07001041 "scopes": self.DEFAULT_INSTANCE_SCOPE}],
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001042 }
1043
Kevin Chengb5963882018-05-09 00:06:27 -07001044 if gpu:
1045 body["guestAccelerators"] = [{
1046 "acceleratorType": self.GetAcceleratorUrl(gpu, zone),
1047 "acceleratorCount": 1
1048 }]
1049 # Instances with GPUs cannot live migrate because they are assigned
1050 # to specific hardware devices.
1051 body["scheduling"] = {"onHostMaintenance": "terminate"}
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001052 if metadata:
1053 metadata_list = [{"key": key,
1054 "value": val}
1055 for key, val in metadata.iteritems()]
1056 body["metadata"] = {"items": metadata_list}
1057 logger.info("Creating instance: project %s, zone %s, body:%s",
Kevin Chengb5963882018-05-09 00:06:27 -07001058 self._project, zone, body)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001059 api = self.service.instances().insert(project=self._project,
1060 zone=zone,
1061 body=body)
1062 operation = self.Execute(api)
1063 self.WaitOnOperation(operation,
1064 operation_scope=OperationScope.ZONE,
1065 scope_name=zone)
1066 logger.info("Instance %s has been created.", instance)
1067
1068 def DeleteInstance(self, instance, zone):
1069 """Delete a gce instance.
1070
1071 Args:
Kevin Chengb5963882018-05-09 00:06:27 -07001072 instance: A string, instance name.
1073 zone: A string, e.g. "us-central1-f"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001074 """
1075 logger.info("Deleting instance: %s", instance)
1076 api = self.service.instances().delete(project=self._project,
1077 zone=zone,
1078 instance=instance)
1079 operation = self.Execute(api)
1080 self.WaitOnOperation(operation,
1081 operation_scope=OperationScope.ZONE,
1082 scope_name=zone)
1083 logger.info("Deleted instance: %s", instance)
1084
1085 def DeleteInstances(self, instances, zone):
1086 """Delete multiple instances.
1087
1088 Args:
1089 instances: A list of instance names.
1090 zone: A string, e.g. "us-central1-f".
1091
1092 Returns:
1093 A tuple, (deleted, failed, error_msgs)
1094 deleted: A list of names of instances that have been deleted.
1095 failed: A list of names of instances that we fail to delete.
1096 error_msgs: A list of failure messages.
1097 """
1098 action = functools.partial(self.service.instances().delete,
1099 project=self._project,
1100 zone=zone)
1101 return self._BatchExecuteOnInstances(instances, zone, action)
1102
1103 def ResetInstance(self, instance, zone):
1104 """Reset the gce instance.
1105
1106 Args:
1107 instance: A string, instance name.
1108 zone: A string, e.g. "us-central1-f".
1109 """
1110 logger.info("Resetting instance: %s", instance)
1111 api = self.service.instances().reset(project=self._project,
1112 zone=zone,
1113 instance=instance)
1114 operation = self.Execute(api)
1115 self.WaitOnOperation(operation,
1116 operation_scope=OperationScope.ZONE,
1117 scope_name=zone)
1118 logger.info("Instance has been reset: %s", instance)
1119
1120 def GetMachineType(self, machine_type, zone):
1121 """Get URL for a given machine typle.
1122
1123 Args:
1124 machine_type: A string, name of the machine type.
1125 zone: A string, e.g. "us-central1-f"
1126
1127 Returns:
1128 A machine type resource in json.
1129 https://cloud.google.com/compute/docs/reference/latest/
1130 machineTypes#resource
1131 """
1132 api = self.service.machineTypes().get(project=self._project,
1133 zone=zone,
1134 machineType=machine_type)
1135 return self.Execute(api)
1136
Kevin Chengb5963882018-05-09 00:06:27 -07001137 def GetAcceleratorUrl(self, accelerator_type, zone):
1138 """Get URL for a given type of accelator.
1139
1140 Args:
1141 accelerator_type: A string, representing the accelerator, e.g
1142 "nvidia-tesla-k80"
1143 zone: A string representing a zone, e.g. "us-west1-b"
1144
1145 Returns:
1146 A URL that points to the accelerator resource, e.g.
1147 https://www.googleapis.com/compute/v1/projects/<project id>/zones/
1148 us-west1-b/acceleratorTypes/nvidia-tesla-k80
1149 """
1150 api = self.service.acceleratorTypes().get(project=self._project,
1151 zone=zone,
1152 acceleratorType=accelerator_type)
1153 result = self.Execute(api)
1154 return result["selfLink"]
1155
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001156 def GetNetworkUrl(self, network):
1157 """Get URL for a given network.
1158
1159 Args:
1160 network: A string, representing network name, e.g "default"
1161
1162 Returns:
1163 A URL that points to the network resource, e.g.
1164 https://www.googleapis.com/compute/v1/projects/<project id>/
1165 global/networks/default
1166 """
1167 api = self.service.networks().get(project=self._project,
1168 network=network)
1169 result = self.Execute(api)
1170 return result["selfLink"]
1171
1172 def CompareMachineSize(self, machine_type_1, machine_type_2, zone):
1173 """Compare the size of two machine types.
1174
1175 Args:
1176 machine_type_1: A string representing a machine type, e.g. n1-standard-1
1177 machine_type_2: A string representing a machine type, e.g. n1-standard-1
1178 zone: A string representing a zone, e.g. "us-central1-f"
1179
1180 Returns:
1181 1 if size of the first type is greater than the second type.
1182 2 if size of the first type is smaller than the second type.
1183 0 if they are equal.
1184
1185 Raises:
1186 errors.DriverError: For malformed response.
1187 """
1188 machine_info_1 = self.GetMachineType(machine_type_1, zone)
1189 machine_info_2 = self.GetMachineType(machine_type_2, zone)
1190 for metric in self.MACHINE_SIZE_METRICS:
1191 if metric not in machine_info_1 or metric not in machine_info_2:
1192 raise errors.DriverError(
1193 "Malformed machine size record: Can't find '%s' in %s or %s"
1194 % (metric, machine_info_1, machine_info_2))
1195 if machine_info_1[metric] - machine_info_2[metric] > 0:
1196 return 1
1197 elif machine_info_1[metric] - machine_info_2[metric] < 0:
1198 return -1
1199 return 0
1200
1201 def GetSerialPortOutput(self, instance, zone, port=1):
1202 """Get serial port output.
1203
1204 Args:
1205 instance: string, instance name.
1206 zone: string, zone name.
1207 port: int, which COM port to read from, 1-4, default to 1.
1208
1209 Returns:
1210 String, contents of the output.
1211
1212 Raises:
1213 errors.DriverError: For malformed response.
1214 """
1215 api = self.service.instances().getSerialPortOutput(
1216 project=self._project,
1217 zone=zone,
1218 instance=instance,
1219 port=port)
1220 result = self.Execute(api)
1221 if "contents" not in result:
1222 raise errors.DriverError(
1223 "Malformed response for GetSerialPortOutput: %s" % result)
1224 return result["contents"]
1225
1226 def GetInstanceNamesByIPs(self, ips, zone):
1227 """Get Instance names by IPs.
1228
1229 This function will go through all instances, which
1230 could be slow if there are too many instances. However, currently
1231 GCE doesn't support search for instance by IP.
1232
1233 Args:
1234 ips: A set of IPs.
1235 zone: String, name of the zone.
1236
1237 Returns:
1238 A dictionary where key is IP and value is instance name or None
1239 if instance is not found for the given IP.
1240 """
1241 ip_name_map = dict.fromkeys(ips)
1242 for instance in self.ListInstances(zone):
1243 try:
1244 ip = instance["networkInterfaces"][0]["accessConfigs"][0][
1245 "natIP"]
1246 if ip in ips:
1247 ip_name_map[ip] = instance["name"]
1248 except (IndexError, KeyError) as e:
1249 logger.error("Could not get instance names by ips: %s", str(e))
1250 return ip_name_map
1251
1252 def GetInstanceIP(self, instance, zone):
1253 """Get Instance IP given instance name.
1254
1255 Args:
Kevin Chengb5963882018-05-09 00:06:27 -07001256 instance: String, representing instance name.
1257 zone: String, name of the zone.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001258
1259 Returns:
Kevin Chengb5963882018-05-09 00:06:27 -07001260 string, IP of the instance.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001261 """
1262 # TODO(fdeng): This is for accessing external IP.
1263 # We should handle internal IP as well when the script is running
1264 # on a GCE instance in the same network of |instance|.
1265 instance = self.GetInstance(instance, zone)
1266 return instance["networkInterfaces"][0]["accessConfigs"][0]["natIP"]
1267
1268 def SetCommonInstanceMetadata(self, body):
1269 """Set project-wide metadata.
1270
1271 Args:
Kevin Chengb5963882018-05-09 00:06:27 -07001272 body: Metadata body.
1273 metdata is in the following format.
1274 {
1275 "kind": "compute#metadata",
1276 "fingerprint": "a-23icsyx4E=",
1277 "items": [
1278 {
1279 "key": "google-compute-default-region",
1280 "value": "us-central1"
1281 }, ...
1282 ]
1283 }
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001284 """
1285 api = self.service.projects().setCommonInstanceMetadata(
1286 project=self._project, body=body)
1287 operation = self.Execute(api)
1288 self.WaitOnOperation(operation, operation_scope=OperationScope.GLOBAL)
1289
1290 def AddSshRsa(self, user, ssh_rsa_path):
1291 """Add the public rsa key to the project's metadata.
1292
1293 Compute engine instances that are created after will
1294 by default contain the key.
1295
1296 Args:
1297 user: the name of the user which the key belongs to.
1298 ssh_rsa_path: The absolute path to public rsa key.
1299 """
1300 if not os.path.exists(ssh_rsa_path):
1301 raise errors.DriverError("RSA file %s does not exist." %
1302 ssh_rsa_path)
1303
1304 logger.info("Adding ssh rsa key from %s to project %s for user: %s",
1305 ssh_rsa_path, self._project, user)
1306 project = self.GetProject()
1307 with open(ssh_rsa_path) as f:
1308 rsa = f.read()
1309 rsa = rsa.strip() if rsa else rsa
1310 utils.VerifyRsaPubKey(rsa)
1311 metadata = project["commonInstanceMetadata"]
1312 for item in metadata.setdefault("items", []):
1313 if item["key"] == "sshKeys":
1314 sshkey_item = item
1315 break
1316 else:
1317 sshkey_item = {"key": "sshKeys", "value": ""}
1318 metadata["items"].append(sshkey_item)
1319
1320 entry = "%s:%s" % (user, rsa)
1321 logger.debug("New RSA entry: %s", entry)
1322 sshkey_item["value"] = "\n".join([sshkey_item["value"].strip(), entry
1323 ]).strip()
1324 self.SetCommonInstanceMetadata(metadata)
Fang Dengcef4b112017-03-02 11:20:17 -08001325
1326 def CheckAccess(self):
1327 """Check if the user has read access to the cloud project.
1328
1329 Returns:
1330 True if the user has at least read access to the project.
1331 False otherwise.
1332
1333 Raises:
1334 errors.HttpError if other unexpected error happens when
1335 accessing the project.
1336 """
1337 api = self.service.zones().list(project=self._project)
1338 retry_http_codes = copy.copy(self.RETRY_HTTP_CODES)
1339 retry_http_codes.remove(self.ACCESS_DENIED_CODE)
1340 try:
1341 self.Execute(api, retry_http_codes=retry_http_codes)
1342 except errors.HttpError as e:
1343 if e.code == self.ACCESS_DENIED_CODE:
1344 return False
1345 raise
1346 return True