blob: 76e3bf9ab4ed332b9770c5b40705052c0c034d9e [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"""
29import functools
30import logging
31import os
32
33import google3
34
35from acloud.internal.lib import base_cloud_client
36from acloud.internal.lib import utils
37from acloud.public import errors
38
39logger = logging.getLogger(__name__)
40
41
42class OperationScope(object):
43 """Represents operation scope enum."""
44 ZONE = "zone"
45 REGION = "region"
46 GLOBAL = "global"
47
48
49class ComputeClient(base_cloud_client.BaseCloudApiClient):
50 """Client that manages GCE."""
51
52 # API settings, used by BaseCloudApiClient.
53 API_NAME = "compute"
54 API_VERSION = "v1"
55 SCOPE = " ".join(["https://www.googleapis.com/auth/compute",
56 "https://www.googleapis.com/auth/devstorage.read_write"])
57 # Default settings for gce operations
58 DEFAULT_INSTANCE_SCOPE = [
59 "https://www.googleapis.com/auth/devstorage.read_only",
60 "https://www.googleapis.com/auth/logging.write"
61 ]
62 OPERATION_TIMEOUT_SECS = 15 * 60 # 15 mins
63 OPERATION_POLL_INTERVAL_SECS = 5
64 MACHINE_SIZE_METRICS = ["guestCpus", "memoryMb"]
65
66 def __init__(self, acloud_config, oauth2_credentials):
67 """Initialize.
68
69 Args:
70 acloud_config: An AcloudConfig object.
71 oauth2_credentials: An oauth2client.OAuth2Credentials instance.
72 """
73 super(ComputeClient, self).__init__(oauth2_credentials)
74 self._project = acloud_config.project
75
76 def _GetOperationStatus(self, operation, operation_scope, scope_name=None):
77 """Get status of an operation.
78
79 Args:
80 operation: An Operation resource in the format of json.
81 operation_scope: A value from OperationScope, "zone", "region",
82 or "global".
83 scope_name: If operation_scope is "zone" or "region", this should be
84 the name of the zone or region, e.g. "us-central1-f".
85
86 Returns:
87 Status of the operation, one of "DONE", "PENDING", "RUNNING".
88
89 Raises:
90 errors.DriverError: if the operation fails.
91 """
92 operation_name = operation["name"]
93 if operation_scope == OperationScope.GLOBAL:
94 api = self.service.globalOperations().get(project=self._project,
95 operation=operation_name)
96 result = self.Execute(api)
97 elif operation_scope == OperationScope.ZONE:
98 api = self.service.zoneOperations().get(project=self._project,
99 operation=operation_name,
100 zone=scope_name)
101 result = self.Execute(api)
102 elif operation_scope == OperationScope.REGION:
103 api = self.service.regionOperations().get(project=self._project,
104 operation=operation_name,
105 region=scope_name)
106 result = self.Execute(api)
107
108 if result.get("error"):
109 errors_list = result["error"]["errors"]
110 raise errors.DriverError("Get operation state failed, errors: %s" %
111 str(errors_list))
112 return result["status"]
113
114 def WaitOnOperation(self, operation, operation_scope, scope_name=None):
115 """Wait for an operation to finish.
116
117 Args:
118 operation: An Operation resource in the format of json.
119 operation_scope: A value from OperationScope, "zone", "region",
120 or "global".
121 scope_name: If operation_scope is "zone" or "region", this should be
122 the name of the zone or region, e.g. "us-central1-f".
123 """
124 timeout_exception = errors.GceOperationTimeoutError(
125 "Operation hits timeout, did not complete within %d secs." %
126 self.OPERATION_TIMEOUT_SECS)
127 utils.PollAndWait(
128 func=self._GetOperationStatus,
129 expected_return="DONE",
130 timeout_exception=timeout_exception,
131 timeout_secs=self.OPERATION_TIMEOUT_SECS,
132 sleep_interval_secs=self.OPERATION_POLL_INTERVAL_SECS,
133 operation=operation,
134 operation_scope=operation_scope,
135 scope_name=scope_name)
136
137 def GetProject(self):
138 """Get project information.
139
140 Returns:
141 A project resource in json.
142 """
143 api = self.service.projects().get(project=self._project)
144 return self.Execute(api)
145
146 def GetDisk(self, disk_name, zone):
147 """Get disk information.
148
149 Args:
150 disk_name: A string.
151 zone: String, name of zone.
152
153 Returns:
154 An disk resource in json.
155 https://cloud.google.com/compute/docs/reference/latest/disks#resource
156 """
157 api = self.service.disks().get(project=self._project,
158 zone=zone,
159 disk=disk_name)
160 return self.Execute(api)
161
162 def CheckDiskExists(self, disk_name, zone):
163 """Check if disk exists.
164
165 Args:
166 disk_name: A string
167 zone: String, name of zone.
168
169 Returns:
170 True if disk exists, otherwise False.
171 """
172 try:
173 self.GetDisk(disk_name, zone)
174 exists = True
175 except errors.ResourceNotFoundError:
176 exists = False
177 logger.debug("CheckDiskExists: disk_name: %s, result: %s", disk_name,
178 exists)
179 return exists
180
181 def CreateDisk(self, disk_name, source_image, size_gb, zone):
182 """Create a gce disk.
183
184 Args:
185 disk_name: A string
186 source_image: A stirng, name of the image.
187 size_gb: Integer, size in gb.
188 zone: Name of the zone, e.g. us-central1-b.
189 """
190 logger.info("Creating disk %s, size_gb: %d", disk_name, size_gb)
191 source_image = "projects/%s/global/images/%s" % (
192 self._project, source_image) if source_image else None
193 body = {
194 "name": disk_name,
195 "sizeGb": size_gb,
196 "type": "projects/%s/zones/%s/diskTypes/pd-standard" % (
197 self._project, zone),
198 }
199 api = self.service.disks().insert(project=self._project,
200 sourceImage=source_image,
201 zone=zone,
202 body=body)
203 operation = self.Execute(api)
204 try:
205 self.WaitOnOperation(operation=operation,
206 operation_scope=OperationScope.ZONE,
207 scope_name=zone)
208 except errors.DriverError:
209 logger.error("Creating disk failed, cleaning up: %s", disk_name)
210 if self.CheckDiskExists(disk_name, zone):
211 self.DeleteDisk(disk_name, zone)
212 raise
213 logger.info("Disk %s has been created.", disk_name)
214
215 def DeleteDisk(self, disk_name, zone):
216 """Delete a gce disk.
217
218 Args:
219 disk_name: A string, name of disk.
220 zone: A string, name of zone.
221 """
222 logger.info("Deleting disk %s", disk_name)
223 api = self.service.disks().delete(project=self._project,
224 zone=zone,
225 disk=disk_name)
226 operation = self.Execute(api)
227 self.WaitOnOperation(operation=operation,
228 operation_scope=OperationScope.GLOBAL)
229 logger.info("Deleted disk %s", disk_name)
230
231 def DeleteDisks(self, disk_names, zone):
232 """Delete multiple disks.
233
234 Args:
235 disk_names: A list of disk names.
236 zone: A string, name of zone.
237
238 Returns:
239 A tuple, (deleted, failed, error_msgs)
240 deleted: A list of names of disks that have been deleted.
241 failed: A list of names of disks that we fail to delete.
242 error_msgs: A list of failure messages.
243 """
244 if not disk_names:
245 logger.warn("Nothing to delete. Arg disk_names is not provided.")
246 return [], [], []
247 # Batch send deletion requests.
248 logger.info("Deleting disks: %s", disk_names)
249 delete_requests = {}
250 for disk_name in set(disk_names):
251 request = self.service.disks().delete(project=self._project,
252 disk=disk_name,
253 zone=zone)
254 delete_requests[disk_name] = request
255 return self._BatchExecuteAndWait(delete_requests,
256 OperationScope.GLOBAL)
257
258 def ListDisks(self, zone, disk_filter=None):
259 """List disks.
260
261 Args:
262 zone: A string, representing zone name. e.g. "us-central1-f"
263 disk_filter: A string representing a filter in format of
264 FIELD_NAME COMPARISON_STRING LITERAL_STRING
265 e.g. "name ne example-instance"
266 e.g. "name eq "example-instance-[0-9]+""
267
268 Returns:
269 A list of disks.
270 """
271 return self.ListWithMultiPages(api_resource=self.service.disks().list,
272 project=self._project,
273 zone=zone,
274 filter=disk_filter)
275
276 def CreateImage(self, image_name, source_uri):
277 """Create a Gce image.
278
279 Args:
280 image_name: A string
281 source_uri: Full Google Cloud Storage URL where the disk image is
282 stored. e.g. "https://storage.googleapis.com/my-bucket/
283 avd-system-2243663.tar.gz"
284 """
285 logger.info("Creating image %s, source_uri %s", image_name, source_uri)
286 body = {"name": image_name, "rawDisk": {"source": source_uri, }, }
287 api = self.service.images().insert(project=self._project, body=body)
288 operation = self.Execute(api)
289 try:
290 self.WaitOnOperation(operation=operation,
291 operation_scope=OperationScope.GLOBAL)
292 except errors.DriverError:
293 logger.error("Creating image failed, cleaning up: %s", image_name)
294 if self.CheckImageExists(image_name):
295 self.DeleteImage(image_name)
296 raise
297 logger.info("Image %s has been created.", image_name)
298
299 def CheckImageExists(self, image_name):
300 """Check if image exists.
301
302 Args:
303 image_name: A string
304
305 Returns:
306 True if image exists, otherwise False.
307 """
308 try:
309 self.GetImage(image_name)
310 exists = True
311 except errors.ResourceNotFoundError:
312 exists = False
313 logger.debug("CheckImageExists: image_name: %s, result: %s",
314 image_name, exists)
315 return exists
316
317 def GetImage(self, image_name):
318 """Get image information.
319
320 Args:
321 image_name: A string
322
323 Returns:
324 An image resource in json.
325 https://cloud.google.com/compute/docs/reference/latest/images#resource
326 """
327 api = self.service.images().get(project=self._project,
328 image=image_name)
329 return self.Execute(api)
330
331 def DeleteImage(self, image_name):
332 """Delete an image.
333
334 Args:
335 image_name: A string
336 """
337 logger.info("Deleting image %s", image_name)
338 api = self.service.images().delete(project=self._project,
339 image=image_name)
340 operation = self.Execute(api)
341 self.WaitOnOperation(operation=operation,
342 operation_scope=OperationScope.GLOBAL)
343 logger.info("Deleted image %s", image_name)
344
345 def DeleteImages(self, image_names):
346 """Delete multiple images.
347
348 Args:
349 image_names: A list of image names.
350
351 Returns:
352 A tuple, (deleted, failed, error_msgs)
353 deleted: A list of names of images that have been deleted.
354 failed: A list of names of images that we fail to delete.
355 error_msgs: A list of failure messages.
356 """
357 if not image_names:
358 return [], [], []
359 # Batch send deletion requests.
360 logger.info("Deleting images: %s", image_names)
361 delete_requests = {}
362 for image_name in set(image_names):
363 request = self.service.images().delete(project=self._project,
364 image=image_name)
365 delete_requests[image_name] = request
366 return self._BatchExecuteAndWait(delete_requests,
367 OperationScope.GLOBAL)
368
369 def ListImages(self, image_filter=None):
370 """List images.
371
372 Args:
373 image_filter: A string representing a filter in format of
374 FIELD_NAME COMPARISON_STRING LITERAL_STRING
375 e.g. "name ne example-image"
376 e.g. "name eq "example-image-[0-9]+""
377
378 Returns:
379 A list of images.
380 """
381 return self.ListWithMultiPages(api_resource=self.service.images().list,
382 project=self._project,
383 filter=image_filter)
384
385 def GetInstance(self, instance, zone):
386 """Get information about an instance.
387
388 Args:
389 instance: A string, representing instance name.
390 zone: A string, representing zone name. e.g. "us-central1-f"
391
392 Returns:
393 An instance resource in json.
394 https://cloud.google.com/compute/docs/reference/latest/instances#resource
395 """
396 api = self.service.instances().get(project=self._project,
397 zone=zone,
398 instance=instance)
399 return self.Execute(api)
400
401 def StartInstance(self, instance, zone):
402 """Start |instance| in |zone|.
403
404 Args:
405 instance: A string, representing instance name.
406 zone: A string, representing zone name. e.g. "us-central1-f"
407
408 Raises:
409 errors.GceOperationTimeoutError: Operation takes too long to finish.
410 """
411 api = self.service.instances().start(project=self._project,
412 zone=zone,
413 instance=instance)
414 operation = self.Execute(api)
415 try:
416 self.WaitOnOperation(operation=operation,
417 operation_scope=OperationScope.ZONE,
418 scope_name=zone)
419 except errors.GceOperationTimeoutError:
420 logger.error("Start instance failed: %s", instance)
421 raise
422 logger.info("Instance %s has been started.", instance)
423
424 def StartInstances(self, instances, zone):
425 """Start |instances| in |zone|.
426
427 Args:
428 instances: A list of strings, representing instance names's list.
429 zone: A string, representing zone name. e.g. "us-central1-f"
430
431 Returns:
432 A tuple, (done, failed, error_msgs)
433 done: A list of string, representing the names of instances that
434 have been executed.
435 failed: A list of string, representing the names of instances that
436 we failed to execute.
437 error_msgs: A list of string, representing the failure messages.
438 """
439 action = functools.partial(self.service.instances().start,
440 project=self._project,
441 zone=zone)
442 return self._BatchExecuteOnInstances(instances, zone, action)
443
444 def StopInstance(self, instance, zone):
445 """Stop |instance| in |zone|.
446
447 Args:
448 instance: A string, representing instance name.
449 zone: A string, representing zone name. e.g. "us-central1-f"
450
451 Raises:
452 errors.GceOperationTimeoutError: Operation takes too long to finish.
453 """
454 api = self.service.instances().stop(project=self._project,
455 zone=zone,
456 instance=instance)
457 operation = self.Execute(api)
458 try:
459 self.WaitOnOperation(operation=operation,
460 operation_scope=OperationScope.ZONE,
461 scope_name=zone)
462 except errors.GceOperationTimeoutError:
463 logger.error("Stop instance failed: %s", instance)
464 raise
465 logger.info("Instance %s has been terminated.", instance)
466
467 def StopInstances(self, instances, zone):
468 """Stop |instances| in |zone|.
469
470 Args:
471 instances: A list of strings, representing instance names's list.
472 zone: A string, representing zone name. e.g. "us-central1-f"
473
474 Returns:
475 A tuple, (done, failed, error_msgs)
476 done: A list of string, representing the names of instances that
477 have been executed.
478 failed: A list of string, representing the names of instances that
479 we failed to execute.
480 error_msgs: A list of string, representing the failure messages.
481 """
482 action = functools.partial(self.service.instances().stop,
483 project=self._project,
484 zone=zone)
485 return self._BatchExecuteOnInstances(instances, zone, action)
486
487 def SetScheduling(self,
488 instance,
489 zone,
490 automatic_restart=True,
491 on_host_maintenance="MIGRATE"):
492 """Update scheduling config |automatic_restart| and |on_host_maintenance|.
493
494 See //cloud/cluster/api/mixer_instances.proto Scheduling for config option.
495
496 Args:
497 instance: A string, representing instance name.
498 zone: A string, representing zone name. e.g. "us-central1-f".
499 automatic_restart: Boolean, determine whether the instance will
500 automatically restart if it crashes or not,
501 default to True.
502 on_host_maintenance: enum["MIGRATE", "TERMINATED]
503 The instance's maintenance behavior, which
504 determines whether the instance is live
505 "MIGRATE" or "TERMINATED" when there is
506 a maintenance event.
507
508 Raises:
509 errors.GceOperationTimeoutError: Operation takes too long to finish.
510 """
511 body = {"automaticRestart": automatic_restart,
512 "OnHostMaintenance": on_host_maintenance}
513 api = self.service.instances().setScheduling(project=self._project,
514 zone=zone,
515 instance=instance,
516 body=body)
517 operation = self.Execute(api)
518 try:
519 self.WaitOnOperation(operation=operation,
520 operation_scope=OperationScope.ZONE,
521 scope_name=zone)
522 except errors.GceOperationTimeoutError:
523 logger.error("Set instance scheduling failed: %s", instance)
524 raise
525 logger.info("Instance scheduling changed:\n"
526 " automaticRestart: %s\n"
527 " onHostMaintenance: %s\n",
528 str(automatic_restart).lower(), on_host_maintenance)
529
530 def ListInstances(self, zone, instance_filter=None):
531 """List instances.
532
533 Args:
534 zone: A string, representing zone name. e.g. "us-central1-f"
535 instance_filter: A string representing a filter in format of
536 FIELD_NAME COMPARISON_STRING LITERAL_STRING
537 e.g. "name ne example-instance"
538 e.g. "name eq "example-instance-[0-9]+""
539
540 Returns:
541 A list of instances.
542 """
543 return self.ListWithMultiPages(
544 api_resource=self.service.instances().list,
545 project=self._project,
546 zone=zone,
547 filter=instance_filter)
548
549 def SetSchedulingInstances(self,
550 instances,
551 zone,
552 automatic_restart=True,
553 on_host_maintenance="MIGRATE"):
554 """Update scheduling config |automatic_restart| and |on_host_maintenance|.
555
556 See //cloud/cluster/api/mixer_instances.proto Scheduling for config option.
557
558 Args:
559 instances: A list of string, representing instance names.
560 zone: A string, representing zone name. e.g. "us-central1-f".
561 automatic_restart: Boolean, determine whether the instance will
562 automatically restart if it crashes or not,
563 default to True.
564 on_host_maintenance: enum["MIGRATE", "TERMINATED]
565 The instance's maintenance behavior, which
566 determines whether the instance is live
567 "MIGRATE" or "TERMINATED" when there is
568 a maintenance event.
569
570 Returns:
571 A tuple, (done, failed, error_msgs)
572 done: A list of string, representing the names of instances that
573 have been executed.
574 failed: A list of string, representing the names of instances that
575 we failed to execute.
576 error_msgs: A list of string, representing the failure messages.
577 """
578 body = {"automaticRestart": automatic_restart,
579 "OnHostMaintenance": on_host_maintenance}
580 action = functools.partial(self.service.instances().setScheduling,
581 project=self._project,
582 zone=zone,
583 body=body)
584 return self._BatchExecuteOnInstances(instances, zone, action)
585
586 def _BatchExecuteOnInstances(self, instances, zone, action):
587 """Batch processing operations requiring computing time.
588
589 Args:
590 instances: A list of instance names.
591 zone: A string, e.g. "us-central1-f".
592 action: partial func, all kwargs for this gcloud action has been
593 defined in the caller function (e.g. See "StartInstances")
594 except 'instance' which will be defined by iterating the
595 |instances|.
596
597 Returns:
598 A tuple, (done, failed, error_msgs)
599 done: A list of string, representing the names of instances that
600 have been executed.
601 failed: A list of string, representing the names of instances that
602 we failed to execute.
603 error_msgs: A list of string, representing the failure messages.
604 """
605 if not instances:
606 return [], [], []
607 # Batch send requests.
608 logger.info("Batch executing instances: %s", instances)
609 requests = {}
610 for instance_name in set(instances):
611 requests[instance_name] = action(instance=instance_name)
612 return self._BatchExecuteAndWait(requests,
613 operation_scope=OperationScope.ZONE,
614 scope_name=zone)
615
616 def _BatchExecuteAndWait(self, requests, operation_scope, scope_name=None):
617 """Batch processing requests and wait on the operation.
618
619 Args:
620 requests: A dictionary. The key is a string representing the resource
621 name. For example, an instance name, or an image name.
622 operation_scope: A value from OperationScope, "zone", "region",
623 or "global".
624 scope_name: If operation_scope is "zone" or "region", this should be
625 the name of the zone or region, e.g. "us-central1-f".
626
627 Returns:
628 A tuple, (done, failed, error_msgs)
629 done: A list of string, representing the resource names that have
630 been executed.
631 failed: A list of string, representing resource names that
632 we failed to execute.
633 error_msgs: A list of string, representing the failure messages.
634 """
635 results = self.BatchExecute(requests)
636 # Initialize return values
637 failed = []
638 error_msgs = []
639 for resource_name, (_, error) in results.iteritems():
640 if error is not None:
641 failed.append(resource_name)
642 error_msgs.append(str(error))
643 done = []
644 # Wait for the executing operations to finish.
645 logger.info("Waiting for executing operations")
646 for resource_name in requests.iterkeys():
647 operation, _ = results[resource_name]
648 if operation:
649 try:
650 self.WaitOnOperation(operation, operation_scope,
651 scope_name)
652 done.append(resource_name)
653 except errors.DriverError as exc:
654 failed.append(resource_name)
655 error_msgs.append(str(exc))
656 return done, failed, error_msgs
657
658 def ListZones(self):
659 """List all zone instances in the project.
660
661 Returns:
662 Gcompute response instance. For example:
663 {
664 "id": "projects/google.com%3Aandroid-build-staging/zones",
665 "kind": "compute#zoneList",
666 "selfLink": "https://www.googleapis.com/compute/v1/projects/"
667 "google.com:android-build-staging/zones"
668 "items": [
669 {
670 'creationTimestamp': '2014-07-15T10:44:08.663-07:00',
671 'description': 'asia-east1-c',
672 'id': '2222',
673 'kind': 'compute#zone',
674 'name': 'asia-east1-c',
675 'region': 'https://www.googleapis.com/compute/v1/projects/'
676 'google.com:android-build-staging/regions/asia-east1',
677 'selfLink': 'https://www.googleapis.com/compute/v1/projects/'
678 'google.com:android-build-staging/zones/asia-east1-c',
679 'status': 'UP'
680 }, {
681 'creationTimestamp': '2014-05-30T18:35:16.575-07:00',
682 'description': 'asia-east1-b',
683 'id': '2221',
684 'kind': 'compute#zone',
685 'name': 'asia-east1-b',
686 'region': 'https://www.googleapis.com/compute/v1/projects/'
687 'google.com:android-build-staging/regions/asia-east1',
688 'selfLink': 'https://www.googleapis.com/compute/v1/projects'
689 '/google.com:android-build-staging/zones/asia-east1-b',
690 'status': 'UP'
691 }]
692 }
693 See cloud cluster's api/mixer_zones.proto
694 """
695 api = self.service.zones().list(project=self._project)
696 return self.Execute(api)
697
698 def _GetNetworkArgs(self, network):
699 """Helper to generate network args that is used to create an instance.
700
701 Args:
702 network: A string, e.g. "default".
703
704 Returns:
705 A dictionary representing network args.
706 """
707 return {
708 "network": self.GetNetworkUrl(network),
709 "accessConfigs": [{"name": "External NAT",
710 "type": "ONE_TO_ONE_NAT"}]
711 }
712
713 def _GetDiskArgs(self, disk_name, image_name):
714 """Helper to generate disk args that is used to create an instance.
715
716 Args:
717 disk_name: A string
718 image_name: A string
719
720 Returns:
721 A dictionary representing disk args.
722 """
723 return [{
724 "type": "PERSISTENT",
725 "boot": True,
726 "mode": "READ_WRITE",
727 "autoDelete": True,
728 "initializeParams": {
729 "diskName": disk_name,
730 "sourceImage": self.GetImage(image_name)["selfLink"],
731 },
732 }]
733
734 def CreateInstance(self,
735 instance,
736 image_name,
737 machine_type,
738 metadata,
739 network,
740 zone,
741 disk_args=None):
742 """Create a gce instance with a gce image.
743
744 Args:
745 instance: instance name.
746 image_name: A source image used to create this disk.
747 machine_type: A string, representing machine_type, e.g. "n1-standard-1"
748 metadata: A dictionary that maps a metadata name to its value.
749 network: A string, representing network name, e.g. "default"
750 zone: A string, representing zone name, e.g. "us-central1-f"
751 disk_args: A list of extra disk args, see _GetDiskArgs for example,
752 if None, will create a disk using the given image.
753 """
754 disk_args = (disk_args or self._GetDiskArgs(instance, image_name))
755 body = {
756 "machineType": self.GetMachineType(machine_type, zone)["selfLink"],
757 "name": instance,
758 "networkInterfaces": [self._GetNetworkArgs(network)],
759 "disks": disk_args,
760 "serviceAccounts": [
761 {"email": "default",
762 "scopes": self.DEFAULT_INSTANCE_SCOPE}
763 ],
764 }
765
766 if metadata:
767 metadata_list = [{"key": key,
768 "value": val}
769 for key, val in metadata.iteritems()]
770 body["metadata"] = {"items": metadata_list}
771 logger.info("Creating instance: project %s, zone %s, body:%s",
772 self._project, zone, body)
773 api = self.service.instances().insert(project=self._project,
774 zone=zone,
775 body=body)
776 operation = self.Execute(api)
777 self.WaitOnOperation(operation,
778 operation_scope=OperationScope.ZONE,
779 scope_name=zone)
780 logger.info("Instance %s has been created.", instance)
781
782 def DeleteInstance(self, instance, zone):
783 """Delete a gce instance.
784
785 Args:
786 instance: A string, instance name.
787 zone: A string, e.g. "us-central1-f"
788 """
789 logger.info("Deleting instance: %s", instance)
790 api = self.service.instances().delete(project=self._project,
791 zone=zone,
792 instance=instance)
793 operation = self.Execute(api)
794 self.WaitOnOperation(operation,
795 operation_scope=OperationScope.ZONE,
796 scope_name=zone)
797 logger.info("Deleted instance: %s", instance)
798
799 def DeleteInstances(self, instances, zone):
800 """Delete multiple instances.
801
802 Args:
803 instances: A list of instance names.
804 zone: A string, e.g. "us-central1-f".
805
806 Returns:
807 A tuple, (deleted, failed, error_msgs)
808 deleted: A list of names of instances that have been deleted.
809 failed: A list of names of instances that we fail to delete.
810 error_msgs: A list of failure messages.
811 """
812 action = functools.partial(self.service.instances().delete,
813 project=self._project,
814 zone=zone)
815 return self._BatchExecuteOnInstances(instances, zone, action)
816
817 def ResetInstance(self, instance, zone):
818 """Reset the gce instance.
819
820 Args:
821 instance: A string, instance name.
822 zone: A string, e.g. "us-central1-f".
823 """
824 logger.info("Resetting instance: %s", instance)
825 api = self.service.instances().reset(project=self._project,
826 zone=zone,
827 instance=instance)
828 operation = self.Execute(api)
829 self.WaitOnOperation(operation,
830 operation_scope=OperationScope.ZONE,
831 scope_name=zone)
832 logger.info("Instance has been reset: %s", instance)
833
834 def GetMachineType(self, machine_type, zone):
835 """Get URL for a given machine typle.
836
837 Args:
838 machine_type: A string, name of the machine type.
839 zone: A string, e.g. "us-central1-f"
840
841 Returns:
842 A machine type resource in json.
843 https://cloud.google.com/compute/docs/reference/latest/
844 machineTypes#resource
845 """
846 api = self.service.machineTypes().get(project=self._project,
847 zone=zone,
848 machineType=machine_type)
849 return self.Execute(api)
850
851 def GetNetworkUrl(self, network):
852 """Get URL for a given network.
853
854 Args:
855 network: A string, representing network name, e.g "default"
856
857 Returns:
858 A URL that points to the network resource, e.g.
859 https://www.googleapis.com/compute/v1/projects/<project id>/
860 global/networks/default
861 """
862 api = self.service.networks().get(project=self._project,
863 network=network)
864 result = self.Execute(api)
865 return result["selfLink"]
866
867 def CompareMachineSize(self, machine_type_1, machine_type_2, zone):
868 """Compare the size of two machine types.
869
870 Args:
871 machine_type_1: A string representing a machine type, e.g. n1-standard-1
872 machine_type_2: A string representing a machine type, e.g. n1-standard-1
873 zone: A string representing a zone, e.g. "us-central1-f"
874
875 Returns:
876 1 if size of the first type is greater than the second type.
877 2 if size of the first type is smaller than the second type.
878 0 if they are equal.
879
880 Raises:
881 errors.DriverError: For malformed response.
882 """
883 machine_info_1 = self.GetMachineType(machine_type_1, zone)
884 machine_info_2 = self.GetMachineType(machine_type_2, zone)
885 for metric in self.MACHINE_SIZE_METRICS:
886 if metric not in machine_info_1 or metric not in machine_info_2:
887 raise errors.DriverError(
888 "Malformed machine size record: Can't find '%s' in %s or %s"
889 % (metric, machine_info_1, machine_info_2))
890 if machine_info_1[metric] - machine_info_2[metric] > 0:
891 return 1
892 elif machine_info_1[metric] - machine_info_2[metric] < 0:
893 return -1
894 return 0
895
896 def GetSerialPortOutput(self, instance, zone, port=1):
897 """Get serial port output.
898
899 Args:
900 instance: string, instance name.
901 zone: string, zone name.
902 port: int, which COM port to read from, 1-4, default to 1.
903
904 Returns:
905 String, contents of the output.
906
907 Raises:
908 errors.DriverError: For malformed response.
909 """
910 api = self.service.instances().getSerialPortOutput(
911 project=self._project,
912 zone=zone,
913 instance=instance,
914 port=port)
915 result = self.Execute(api)
916 if "contents" not in result:
917 raise errors.DriverError(
918 "Malformed response for GetSerialPortOutput: %s" % result)
919 return result["contents"]
920
921 def GetInstanceNamesByIPs(self, ips, zone):
922 """Get Instance names by IPs.
923
924 This function will go through all instances, which
925 could be slow if there are too many instances. However, currently
926 GCE doesn't support search for instance by IP.
927
928 Args:
929 ips: A set of IPs.
930 zone: String, name of the zone.
931
932 Returns:
933 A dictionary where key is IP and value is instance name or None
934 if instance is not found for the given IP.
935 """
936 ip_name_map = dict.fromkeys(ips)
937 for instance in self.ListInstances(zone):
938 try:
939 ip = instance["networkInterfaces"][0]["accessConfigs"][0][
940 "natIP"]
941 if ip in ips:
942 ip_name_map[ip] = instance["name"]
943 except (IndexError, KeyError) as e:
944 logger.error("Could not get instance names by ips: %s", str(e))
945 return ip_name_map
946
947 def GetInstanceIP(self, instance, zone):
948 """Get Instance IP given instance name.
949
950 Args:
951 instance: String, representing instance name.
952 zone: String, name of the zone.
953
954 Returns:
955 string, IP of the instance.
956 """
957 # TODO(fdeng): This is for accessing external IP.
958 # We should handle internal IP as well when the script is running
959 # on a GCE instance in the same network of |instance|.
960 instance = self.GetInstance(instance, zone)
961 return instance["networkInterfaces"][0]["accessConfigs"][0]["natIP"]
962
963 def SetCommonInstanceMetadata(self, body):
964 """Set project-wide metadata.
965
966 Args:
967 body: Metadata body.
968 metdata is in the following format.
969 {
970 "kind": "compute#metadata",
971 "fingerprint": "a-23icsyx4E=",
972 "items": [
973 {
974 "key": "google-compute-default-region",
975 "value": "us-central1"
976 }, ...
977 ]
978 }
979 """
980 api = self.service.projects().setCommonInstanceMetadata(
981 project=self._project, body=body)
982 operation = self.Execute(api)
983 self.WaitOnOperation(operation, operation_scope=OperationScope.GLOBAL)
984
985 def AddSshRsa(self, user, ssh_rsa_path):
986 """Add the public rsa key to the project's metadata.
987
988 Compute engine instances that are created after will
989 by default contain the key.
990
991 Args:
992 user: the name of the user which the key belongs to.
993 ssh_rsa_path: The absolute path to public rsa key.
994 """
995 if not os.path.exists(ssh_rsa_path):
996 raise errors.DriverError("RSA file %s does not exist." %
997 ssh_rsa_path)
998
999 logger.info("Adding ssh rsa key from %s to project %s for user: %s",
1000 ssh_rsa_path, self._project, user)
1001 project = self.GetProject()
1002 with open(ssh_rsa_path) as f:
1003 rsa = f.read()
1004 rsa = rsa.strip() if rsa else rsa
1005 utils.VerifyRsaPubKey(rsa)
1006 metadata = project["commonInstanceMetadata"]
1007 for item in metadata.setdefault("items", []):
1008 if item["key"] == "sshKeys":
1009 sshkey_item = item
1010 break
1011 else:
1012 sshkey_item = {"key": "sshKeys", "value": ""}
1013 metadata["items"].append(sshkey_item)
1014
1015 entry = "%s:%s" % (user, rsa)
1016 logger.debug("New RSA entry: %s", entry)
1017 sshkey_item["value"] = "\n".join([sshkey_item["value"].strip(), entry
1018 ]).strip()
1019 self.SetCommonInstanceMetadata(metadata)