crostestutils: Move lib/gce.py to chromite
lib/gce.py is getting more attraction, so better move it to chromite.lib, which
is the best place for common libraries for cross-repo usage. One immediate use
of it would be the server host for GCE that we are adding to Autotest.
BUG=b:25291982
TEST=au_test_harness/gce_au_worker_unittest.py and trybot against lakitu-pre-cq
CQ-DEPEND=CL:311833
Change-Id: Ic5b773ebb4bbbcc425a941e301d3699e48a70bf4
Reviewed-on: https://chromium-review.googlesource.com/311844
Commit-Ready: Daniel Wang <wonderfly@google.com>
Tested-by: Daniel Wang <wonderfly@google.com>
Reviewed-by: Fang Deng <fdeng@chromium.org>
diff --git a/au_test_harness/gce_au_worker.py b/au_test_harness/gce_au_worker.py
index 678cdb7..f67c86d 100644
--- a/au_test_harness/gce_au_worker.py
+++ b/au_test_harness/gce_au_worker.py
@@ -73,6 +73,7 @@
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
+from chromite.lib import gce
from chromite.lib import gs
from chromite.lib import parallel
from chromite.lib import path_util
@@ -80,7 +81,6 @@
from crostestutils.au_test_harness import au_worker
from crostestutils.au_test_harness import constants
from crostestutils.au_test_harness import update_exception
-from crostestutils.lib import gce
class GCEAUWorker(au_worker.AUWorker):
@@ -89,6 +89,8 @@
Attributes:
gce_context: An utility for GCE operations.
gscontext: An utility for GCS operations.
+ network: Default network to create instances in.
+ machine_type: Default machine type to create instances with.
gcs_bucket: The GCS bucket to upload image tarballs to.
tarball_local: Local path to the tarball of test image.
tarball_remote: GCS path to the tarball of test image.
@@ -114,8 +116,10 @@
"""Processes GCE-specific options."""
super(GCEAUWorker, self).__init__(options, test_results_root)
self.gce_context = gce.GceContext.ForServiceAccountThreadSafe(
- project, zone, network, machine_type, json_key_file=json_key_file)
+ project, zone, json_key_file=json_key_file)
self.gscontext = gs.GSContext()
+ self.network = network
+ self.machine_type = machine_type
self.gcs_bucket = gcs_bucket
self.tarball_local = None
self.tarball_remote = None
@@ -374,7 +378,8 @@
kwargs = test['flags'].copy()
kwargs['description'] = 'For test %s' % test['name']
steps.append(partial(self.gce_context.CreateInstance, instance,
- self.image_link, **kwargs))
+ self.image_link, network=self.network,
+ machine_type=self.machine_type, **kwargs))
self.instances[test['name']] = instance
parallel.RunParallelSteps(steps)
diff --git a/au_test_harness/gce_au_worker_unittest.py b/au_test_harness/gce_au_worker_unittest.py
index 21825d0..9d8f79b 100755
--- a/au_test_harness/gce_au_worker_unittest.py
+++ b/au_test_harness/gce_au_worker_unittest.py
@@ -17,6 +17,7 @@
sys.path.append(constants.CROS_PLATFORM_ROOT)
sys.path.append(constants.SOURCE_ROOT)
+from chromite.lib.gce import GceContext
from chromite.lib import cros_build_lib
from chromite.lib import cros_test_lib
from chromite.lib import osutils
@@ -25,7 +26,6 @@
from chromite.lib import portage_util
from crostestutils.au_test_harness.au_worker import AUWorker
from crostestutils.au_test_harness.gce_au_worker import GCEAUWorker
-from crostestutils.lib.gce import GceContext
class Options(object):
diff --git a/lib/gce.py b/lib/gce.py
deleted file mode 100644
index b5fa0b6..0000000
--- a/lib/gce.py
+++ /dev/null
@@ -1,381 +0,0 @@
-# Copyright 2015 The Chromium OS Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-"""A convinient wrapper of the GCE python API.
-
-Public methods in class GceContext raise HttpError when the underlining call to
-Google API fails, or gce.Error on other failures.
-"""
-
-from __future__ import print_function
-
-import httplib2
-
-from chromite.lib import cros_logging as logging
-from chromite.lib import timeout_util
-from googleapiclient.discovery import build
-from googleapiclient.http import HttpRequest
-from googleapiclient import errors
-from oauth2client.client import GoogleCredentials
-
-
-class Error(Exception):
- """Base exception for this module."""
-
-
-class CredentialsError(Error):
- """Exceptions raised when failed to construct credentials."""
-
-
-class GceGoogleApiError(Error):
- """Wraps exceptions raised by googleapiclient.
-
- We wrap underlining exceptions in order to make the implementation detail
- transparent to users of this library, i.e., in most cases, they just need to
- catch 'gce.Error' and don't have to import googleapiclient.errors.
- """
- def __init__(self, error):
- super(GceGoogleApiError, self).__init__()
- if error is None or not isinstance(error, errors.Error):
- raise ValueError('error must be an instance of '
- 'googleapiclient.errors.Error; got %r' % error)
- self.error = error
-
- def __str__(self):
- return 'GCE API failure. %s: %s' % (type(self.error), str(self.error))
-
-
-class GceContext(object):
- """A convinient wrapper around the GCE Python API."""
-
- GCE_SCOPES = [
- 'https://www.googleapis.com/auth/compute', # CreateInstance, CreateImage
- 'https://www.googleapis.com/auth/devstorage.full_control', # CreateImage
- 'https://www.googleapis.com/auth/devstorage.read_write', # CreateImage
- ]
- DEFAULT_MACHINE_TYPE = 'n1-standard-8'
- DEFAULT_TIMEOUT_SEC = 5 * 60
-
- def __init__(self, project, zone, network, machine_type, credentials,
- thread_safe=False):
- """Initializes GceContext.
-
- Args:
- project: The GCP project to create instances in.
- zone: The default zone to create instances in.
- network: The default network to create instances in.
- machine_type: The default machine type to use.
- credentials: The credentials used to call the GCE API.
- thread_safe: Whether the client is expected to be thread safe.
- """
- self.project = project
- self.zone = zone
- self.network = network
- self.machine_type = machine_type
-
- def BuildRequest(_, *args, **kwargs):
- """Create a new Http() object for every request."""
- http = httplib2.Http()
- http = credentials.authorize(http)
- return HttpRequest(http, *args, **kwargs)
-
- if thread_safe:
- self.gce_client = build('compute', 'v1', credentials=credentials,
- requestBuilder=BuildRequest)
- else:
- self.gce_client = build('compute', 'v1', credentials=credentials)
-
- @classmethod
- def ForServiceAccount(cls, project, zone, network, machine_type,
- json_key_file):
- """Creates a GceContext using service account credentials.
-
- About service account:
- https://developers.google.com/api-client-library/python/auth/service-accounts
-
- Args:
- project: The GCP project to create images and instances in.
- zone: The default zone to create instances in.
- network: The default network to create instances in.
- machine_type: The default machine type to use.
- json_key_file: Path to the service account JSON key.
-
- Returns:
- GceContext.
- """
- credentials = GoogleCredentials.from_stream(json_key_file).create_scoped(
- cls.GCE_SCOPES)
- return GceContext(project, zone, network, machine_type, credentials)
-
- @classmethod
- def ForServiceAccountThreadSafe(cls, project, zone, network, machine_type,
- json_key_file):
- """Creates a thread-safe GceContext using service account credentials.
-
- About service account:
- https://developers.google.com/api-client-library/python/auth/service-accounts
-
- Args:
- project: The GCP project to create images and instances in.
- zone: The default zone to create instances in.
- network: The default network to create instances in.
- machine_type: The default machine type to use.
- json_key_file: Path to the service account JSON key.
-
- Returns:
- GceContext.
- """
- credentials = GoogleCredentials.from_stream(json_key_file).create_scoped(
- cls.GCE_SCOPES)
- return GceContext(project, zone, network, machine_type, credentials,
- thread_safe=True)
-
- def CreateInstance(self, name, image, machine_type=None, network=None,
- zone=None, **kwargs):
- """Creates an instance with the given image and waits until it's ready.
-
- Args:
- name: Instance name.
- image:
- Fully spelled URL of the image, e.g., for private images,
- 'global/images/my-private-image', or for images from a
- publicly-available project,
- 'projects/debian-cloud/global/images/debian-7-wheezy-vYYYYMMDD'.
- Details:
- https://cloud.google.com/compute/docs/reference/latest/instances/insert
- machine_type: The machine type to use.
- network: An existing network to create the instance in.
- zone:
- The zone to create the instance in. Default zone will be used if
- omitted.
- kwargs:
- Other possible Instance Resource properties.
- https://cloud.google.com/compute/docs/reference/latest/instances#resource
-
- Returns:
- URL to the created instance.
- """
- machine_type = 'zones/%s/machineTypes/%s' % (
- zone or self.zone, machine_type or self.machine_type)
- # Allow machineType overriding.
- if 'machineType' in kwargs.keys():
- machine_type = kwargs['machineType']
-
- config = {
- 'name': name,
- 'machineType': machine_type,
- 'disks': [
- {
- 'boot': True,
- 'autoDelete': True,
- 'initializeParams': {
- 'sourceImage': image
- }
- }
- ],
- 'networkInterfaces': [
- {
- 'network': 'global/networks/%s' % (network or self.network),
- 'accessConfigs': [
- {'type': 'ONE_TO_ONE_NAT', 'name': 'External NAT'}
- ]
- }
- ]
- }
- config.update(**kwargs)
- operation = self.gce_client.instances().insert(
- project=self.project,
- zone=zone or self.zone,
- body=config).execute()
- self._WaitForZoneOperation(operation['name'], timeout_handler=lambda:
- self.DeleteInstance(name))
- return operation['targetLink']
-
- def DeleteInstance(self, name, zone=None):
- """Deletes an instance with the name and waits until it's done.
-
- Args:
- name: Name of the instance to delete.
- zone: Zone where the instance is in. Default zone will be used if omitted.
- """
- operation = self.gce_client.instances().delete(
- project=self.project,
- zone=zone or self.zone,
- instance=name).execute()
- self._WaitForZoneOperation(operation['name'])
-
- def CreateImage(self, name, source):
- """Creates an image with the given |source|.
-
- Args:
- name: Name of the image to be created.
- source:
- Google Cloud Storage object of the source disk, e.g.,
- 'https://storage.googleapis.com/my-gcs-bucket/test_image.tar.gz'.
-
- Returns:
- URL to the created image.
- """
- config = {
- 'name': name,
- 'rawDisk': {
- 'source': source
- }
- }
- operation = self.gce_client.images().insert(
- project=self.project,
- body=config).execute()
- self._WaitForGlobalOperation(operation['name'], timeout_handler=lambda:
- self.DeleteImage(name))
- return operation['targetLink']
-
- def DeleteImage(self, name):
- """Deletes an image and waits until it's deleted.
-
- Args:
- name: Name of the image to delete.
- """
- operation = self.gce_client.images().delete(
- project=self.project,
- image=name).execute()
- self._WaitForGlobalOperation(operation['name'])
-
- def ListInstances(self, zone=None):
- """Lists all instances.
-
- Args:
- zone: Zone where the instances are in. Default zone will be used if
- omitted.
- """
- result = self.gce_client.instances().list(project=self.project,
- zone=zone or self.zone).execute()
- try:
- return result['items']
- except KeyError:
- return []
-
- def ListImages(self):
- """Lists all images."""
- result = self.gce_client.images().list(project=self.project).execute()
-
- try:
- return result['items']
- except KeyError:
- return []
-
- def GetInstance(self, instance, zone=None):
- """Gets an Instance Resource by name and zone.
-
- Args:
- instance: Name of the instance.
- zone: Zone where the instance is in. Default zone will be used if omitted.
-
- Returns:
- An Instance Resource.
- """
- result = self.gce_client.instances().get(project=self.project,
- zone=zone or self.zone,
- instance=instance).execute()
- return result
-
- def GetInstanceIP(self, instance, zone=None):
- """Gets the external IP of an instance.
-
- Args:
- instance: Name of the instance to get IP for.
- zone: Zone where the instance is in. Default zone will be used if omitted.
-
- Returns:
- External IP address of the instance.
-
- Raises:
- gce.Error on failures.
- """
- result = self.GetInstance(instance, zone)
- if not result:
- raise Error('Instance %s does not exist in zone %s.'
- % (instance, zone or self.zone))
- try:
- return result['networkInterfaces'][0]['accessConfigs'][0]['natIP']
- except (KeyError, IndexError):
- raise Error('Failed to get IP address for instance %s' % instance)
-
- def GetImage(self, image):
- """Gets an Image Resource by name.
-
- Args:
- image: Name of the image to look for.
-
- Returns:
- An Image Resource.
- """
- result = self.gce_client.images().get(project=self.project,
- image=image).execute()
- return result
-
- def InstanceExists(self, instance, zone=None):
- """Checks if an instance exists in the current project.
-
- Args:
- instance: Name of the instance to check existence of. zone: Zone where
- the instance is in. Default zone will be used if omitted.
-
- Returns:
- True if the instance exists or False otherwise.
- """
- result = self.GetInstance(instance, zone)
- return (result is not None)
-
- def ImageExists(self, image):
- """Checks if an image exists in the current project.
-
- Args:
- instance: Name of the image to check existence of.
-
- Returns:
- True if the instance exists or False otherwise.
- """
- result = self.GetImage(image)
- return (result is not None)
-
- def _WaitForZoneOperation(self, operation, zone=None, timeout_handler=None):
- get_request = self.gce_client.zoneOperations().get(
- project=self.project, zone=zone or self.zone, operation=operation)
- self._WaitForOperation(operation, get_request,
- timeout_handler=timeout_handler)
-
- def _WaitForGlobalOperation(self, operation, timeout_handler=None):
- get_request = self.gce_client.globalOperations().get(project=self.project,
- operation=operation)
- self._WaitForOperation(operation, get_request,
- timeout_handler=timeout_handler)
-
- def _WaitForOperation(self, operation, get_operation_request,
- timeout_handler=None):
- """Waits until timeout or the request gets a response with a 'DONE' status.
-
- Args:
- operation: The GCE operation to wait for.
- get_operation_request:
- The HTTP request to get the operation's status.
- This request will be executed periodically until it returns a status
- 'DONE'.
- timeout_handler: A callable to be executed when times out.
- """
- def _IsDone():
- result = get_operation_request.execute()
- if result['status'] == 'DONE':
- if 'error' in result:
- raise Error(result['error'])
- return True
- return False
-
- try:
- logging.info('Waiting for operation [%s] to complete...' % operation)
- timeout = self.DEFAULT_TIMEOUT_SEC
- timeout_util.WaitForReturnTrue(_IsDone, timeout, period=1)
- except timeout_util.TimeoutError:
- if not timeout_handler:
- timeout_handler()
- raise Error('Timeout wating for operation [%s] to complete' % operation)