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