1*9c5db199SXin Li# -*- coding: utf-8 -*- 2*9c5db199SXin Li# Copyright 2015 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Li"""A convinient wrapper of the GCE python API. 7*9c5db199SXin Li 8*9c5db199SXin LiPublic methods in class GceContext raise HttpError when the underlining call to 9*9c5db199SXin LiGoogle API fails, or gce.Error on other failures. 10*9c5db199SXin Li""" 11*9c5db199SXin Li 12*9c5db199SXin Lifrom __future__ import print_function 13*9c5db199SXin Li 14*9c5db199SXin Lifrom googleapiclient.discovery import build 15*9c5db199SXin Lifrom googleapiclient.errors import HttpError 16*9c5db199SXin Lifrom googleapiclient.http import HttpRequest 17*9c5db199SXin Liimport httplib2 18*9c5db199SXin Lifrom oauth2client.client import GoogleCredentials 19*9c5db199SXin Li 20*9c5db199SXin Lifrom autotest_lib.utils.frozen_chromite.lib import cros_logging as logging 21*9c5db199SXin Lifrom autotest_lib.utils.frozen_chromite.lib import timeout_util 22*9c5db199SXin Li 23*9c5db199SXin Li 24*9c5db199SXin Liclass Error(Exception): 25*9c5db199SXin Li """Base exception for this module.""" 26*9c5db199SXin Li 27*9c5db199SXin Li 28*9c5db199SXin Liclass ResourceNotFoundError(Error): 29*9c5db199SXin Li """Exceptions raised when requested GCE resource was not found.""" 30*9c5db199SXin Li 31*9c5db199SXin Li 32*9c5db199SXin Liclass RetryOnServerErrorHttpRequest(HttpRequest): 33*9c5db199SXin Li """A HttpRequest that will be retried on server errors automatically.""" 34*9c5db199SXin Li 35*9c5db199SXin Li def __init__(self, num_retries, *args, **kwargs): 36*9c5db199SXin Li """Constructor for RetryOnServerErrorHttpRequest.""" 37*9c5db199SXin Li self.num_retries = num_retries 38*9c5db199SXin Li super(RetryOnServerErrorHttpRequest, self).__init__(*args, **kwargs) 39*9c5db199SXin Li 40*9c5db199SXin Li def execute(self, http=None, num_retries=None): 41*9c5db199SXin Li """Excutes a RetryOnServerErrorHttpRequest. 42*9c5db199SXin Li 43*9c5db199SXin Li HttpRequest.execute() has the option of automatically retrying on server 44*9c5db199SXin Li errors, i.e., 500 status codes. Call it with a non-zero value of 45*9c5db199SXin Li |num_retries| will cause failed requests to be retried. 46*9c5db199SXin Li 47*9c5db199SXin Li Args: 48*9c5db199SXin Li http: The httplib2.http to send this request through. 49*9c5db199SXin Li num_retries: Number of retries. Class default value will be used if 50*9c5db199SXin Li omitted. 51*9c5db199SXin Li 52*9c5db199SXin Li Returns: 53*9c5db199SXin Li A deserialized object model of the response body as determined 54*9c5db199SXin Li by the postproc. See HttpRequest.execute(). 55*9c5db199SXin Li """ 56*9c5db199SXin Li return super(RetryOnServerErrorHttpRequest, self).execute( 57*9c5db199SXin Li http=http, num_retries=num_retries or self.num_retries) 58*9c5db199SXin Li 59*9c5db199SXin Li 60*9c5db199SXin Lidef _GetMetdataValue(metadata, key): 61*9c5db199SXin Li """Finds a value corresponding to a given metadata key. 62*9c5db199SXin Li 63*9c5db199SXin Li Args: 64*9c5db199SXin Li metadata: metadata object, i.e. a dict containing containing 'items' 65*9c5db199SXin Li - a list of key-value pairs. 66*9c5db199SXin Li key: name of the key. 67*9c5db199SXin Li 68*9c5db199SXin Li Returns: 69*9c5db199SXin Li Corresponding value or None if it was not found. 70*9c5db199SXin Li """ 71*9c5db199SXin Li for item in metadata['items']: 72*9c5db199SXin Li if item['key'] == key: 73*9c5db199SXin Li return item['value'] 74*9c5db199SXin Li return None 75*9c5db199SXin Li 76*9c5db199SXin Li 77*9c5db199SXin Lidef _UpdateMetadataValue(metadata, key, value): 78*9c5db199SXin Li """Updates a single key-value pair in a metadata object. 79*9c5db199SXin Li 80*9c5db199SXin Li Args: 81*9c5db199SXin Li metadata: metadata object, i.e. a dict containing containing 'items' 82*9c5db199SXin Li - a list of key-value pairs. 83*9c5db199SXin Li key: name of the key. 84*9c5db199SXin Li value: new value for the key, or None if it should be removed. 85*9c5db199SXin Li """ 86*9c5db199SXin Li items = metadata.setdefault('items', []) 87*9c5db199SXin Li for item in items: 88*9c5db199SXin Li if item['key'] == key: 89*9c5db199SXin Li if value is None: 90*9c5db199SXin Li items.remove(item) 91*9c5db199SXin Li else: 92*9c5db199SXin Li item['value'] = value 93*9c5db199SXin Li return 94*9c5db199SXin Li 95*9c5db199SXin Li if value is not None: 96*9c5db199SXin Li items.append({ 97*9c5db199SXin Li 'key': key, 98*9c5db199SXin Li 'value': value, 99*9c5db199SXin Li }) 100*9c5db199SXin Li 101*9c5db199SXin Li 102*9c5db199SXin Liclass GceContext(object): 103*9c5db199SXin Li """A convinient wrapper around the GCE Python API.""" 104*9c5db199SXin Li 105*9c5db199SXin Li # These constants are made public so that users can customize as they need. 106*9c5db199SXin Li DEFAULT_TIMEOUT_SEC = 5 * 60 107*9c5db199SXin Li INSTANCE_OPERATIONS_TIMEOUT_SEC = 10 * 60 108*9c5db199SXin Li IMAGE_OPERATIONS_TIMEOUT_SEC = 10 * 60 109*9c5db199SXin Li 110*9c5db199SXin Li _GCE_SCOPES = ( 111*9c5db199SXin Li 'https://www.googleapis.com/auth/compute', # CreateInstance, CreateImage 112*9c5db199SXin Li 'https://www.googleapis.com/auth/devstorage.full_control', # CreateImage 113*9c5db199SXin Li ) 114*9c5db199SXin Li _DEFAULT_NETWORK = 'default' 115*9c5db199SXin Li _DEFAULT_MACHINE_TYPE = 'n1-standard-8' 116*9c5db199SXin Li 117*9c5db199SXin Li # Project default service account and scopes. 118*9c5db199SXin Li _DEFAULT_SERVICE_ACCOUNT_EMAIL = 'default' 119*9c5db199SXin Li # The list is in line with what the gcloud cli uses. 120*9c5db199SXin Li # https://cloud.google.com/sdk/gcloud/reference/compute/instances/create 121*9c5db199SXin Li _DEFAULT_INSTANCE_SCOPES = [ 122*9c5db199SXin Li 'https://www.googleapis.com/auth/cloud.useraccounts.readonly', 123*9c5db199SXin Li 'https://www.googleapis.com/auth/devstorage.read_only', 124*9c5db199SXin Li 'https://www.googleapis.com/auth/logging.write', 125*9c5db199SXin Li ] 126*9c5db199SXin Li 127*9c5db199SXin Li # This is made public to allow easy customization of the retry behavior. 128*9c5db199SXin Li RETRIES = 2 129*9c5db199SXin Li 130*9c5db199SXin Li def __init__(self, project, zone, credentials, thread_safe=False): 131*9c5db199SXin Li """Initializes GceContext. 132*9c5db199SXin Li 133*9c5db199SXin Li Args: 134*9c5db199SXin Li project: The GCP project to create instances in. 135*9c5db199SXin Li zone: The default zone to create instances in. 136*9c5db199SXin Li credentials: The credentials used to call the GCE API. 137*9c5db199SXin Li thread_safe: Whether the client is expected to be thread safe. 138*9c5db199SXin Li """ 139*9c5db199SXin Li self.project = project 140*9c5db199SXin Li self.zone = zone 141*9c5db199SXin Li 142*9c5db199SXin Li def _BuildRequest(http, *args, **kwargs): 143*9c5db199SXin Li """Custom request builder.""" 144*9c5db199SXin Li return self._BuildRetriableRequest(self.RETRIES, http, thread_safe, 145*9c5db199SXin Li credentials, *args, **kwargs) 146*9c5db199SXin Li 147*9c5db199SXin Li self.gce_client = build('compute', 'v1', credentials=credentials, 148*9c5db199SXin Li requestBuilder=_BuildRequest) 149*9c5db199SXin Li 150*9c5db199SXin Li self.region = self.GetZoneRegion(zone) 151*9c5db199SXin Li 152*9c5db199SXin Li @classmethod 153*9c5db199SXin Li def ForServiceAccount(cls, project, zone, json_key_file): 154*9c5db199SXin Li """Creates a GceContext using service account credentials. 155*9c5db199SXin Li 156*9c5db199SXin Li About service account: 157*9c5db199SXin Li https://developers.google.com/api-client-library/python/auth/service-accounts 158*9c5db199SXin Li 159*9c5db199SXin Li Args: 160*9c5db199SXin Li project: The GCP project to create images and instances in. 161*9c5db199SXin Li zone: The default zone to create instances in. 162*9c5db199SXin Li json_key_file: Path to the service account JSON key. 163*9c5db199SXin Li 164*9c5db199SXin Li Returns: 165*9c5db199SXin Li GceContext. 166*9c5db199SXin Li """ 167*9c5db199SXin Li credentials = GoogleCredentials.from_stream(json_key_file).create_scoped( 168*9c5db199SXin Li cls._GCE_SCOPES) 169*9c5db199SXin Li return GceContext(project, zone, credentials) 170*9c5db199SXin Li 171*9c5db199SXin Li @classmethod 172*9c5db199SXin Li def ForServiceAccountThreadSafe(cls, project, zone, json_key_file): 173*9c5db199SXin Li """Creates a thread-safe GceContext using service account credentials. 174*9c5db199SXin Li 175*9c5db199SXin Li About service account: 176*9c5db199SXin Li https://developers.google.com/api-client-library/python/auth/service-accounts 177*9c5db199SXin Li 178*9c5db199SXin Li Args: 179*9c5db199SXin Li project: The GCP project to create images and instances in. 180*9c5db199SXin Li zone: The default zone to create instances in. 181*9c5db199SXin Li json_key_file: Path to the service account JSON key. 182*9c5db199SXin Li 183*9c5db199SXin Li Returns: 184*9c5db199SXin Li GceContext. 185*9c5db199SXin Li """ 186*9c5db199SXin Li credentials = GoogleCredentials.from_stream(json_key_file).create_scoped( 187*9c5db199SXin Li cls._GCE_SCOPES) 188*9c5db199SXin Li return GceContext(project, zone, credentials, thread_safe=True) 189*9c5db199SXin Li 190*9c5db199SXin Li def CreateAddress(self, name, region=None): 191*9c5db199SXin Li """Reserves an external IP address. 192*9c5db199SXin Li 193*9c5db199SXin Li Args: 194*9c5db199SXin Li name: The name to assign to the address. 195*9c5db199SXin Li region: Region to reserved the address in. 196*9c5db199SXin Li 197*9c5db199SXin Li Returns: 198*9c5db199SXin Li The reserved address as a string. 199*9c5db199SXin Li """ 200*9c5db199SXin Li body = { 201*9c5db199SXin Li 'name': name, 202*9c5db199SXin Li } 203*9c5db199SXin Li operation = self.gce_client.addresses().insert( 204*9c5db199SXin Li project=self.project, 205*9c5db199SXin Li region=region or self.region, 206*9c5db199SXin Li body=body).execute() 207*9c5db199SXin Li self._WaitForRegionOperation( 208*9c5db199SXin Li operation['name'], region, 209*9c5db199SXin Li timeout_sec=self.INSTANCE_OPERATIONS_TIMEOUT_SEC) 210*9c5db199SXin Li 211*9c5db199SXin Li address = self.gce_client.addresses().get( 212*9c5db199SXin Li project=self.project, 213*9c5db199SXin Li region=region or self.region, 214*9c5db199SXin Li address=name).execute() 215*9c5db199SXin Li 216*9c5db199SXin Li return address['address'] 217*9c5db199SXin Li 218*9c5db199SXin Li def DeleteAddress(self, name, region=None): 219*9c5db199SXin Li """Frees up an external IP address. 220*9c5db199SXin Li 221*9c5db199SXin Li Args: 222*9c5db199SXin Li name: The name of the address. 223*9c5db199SXin Li region: Region of the address. 224*9c5db199SXin Li """ 225*9c5db199SXin Li operation = self.gce_client.addresses().delete( 226*9c5db199SXin Li project=self.project, 227*9c5db199SXin Li region=region or self.region, 228*9c5db199SXin Li address=name).execute() 229*9c5db199SXin Li self._WaitForRegionOperation( 230*9c5db199SXin Li operation['name'], region=region or self.region, 231*9c5db199SXin Li timeout_sec=self.INSTANCE_OPERATIONS_TIMEOUT_SEC) 232*9c5db199SXin Li 233*9c5db199SXin Li def GetZoneRegion(self, zone=None): 234*9c5db199SXin Li """Resolves name of the region that a zone belongs to. 235*9c5db199SXin Li 236*9c5db199SXin Li Args: 237*9c5db199SXin Li zone: The zone to resolve. 238*9c5db199SXin Li 239*9c5db199SXin Li Returns: 240*9c5db199SXin Li Name of the region corresponding to the zone. 241*9c5db199SXin Li """ 242*9c5db199SXin Li zone_resource = self.gce_client.zones().get( 243*9c5db199SXin Li project=self.project, 244*9c5db199SXin Li zone=zone or self.zone).execute() 245*9c5db199SXin Li return zone_resource['region'].split('/')[-1] 246*9c5db199SXin Li 247*9c5db199SXin Li def CreateInstance(self, name, image, zone=None, network=None, subnet=None, 248*9c5db199SXin Li machine_type=None, default_scopes=True, 249*9c5db199SXin Li static_address=None, **kwargs): 250*9c5db199SXin Li """Creates an instance with the given image and waits until it's ready. 251*9c5db199SXin Li 252*9c5db199SXin Li Args: 253*9c5db199SXin Li name: Instance name. 254*9c5db199SXin Li image: Fully spelled URL of the image, e.g., for private images, 255*9c5db199SXin Li 'global/images/my-private-image', or for images from a 256*9c5db199SXin Li publicly-available project, 257*9c5db199SXin Li 'projects/debian-cloud/global/images/debian-7-wheezy-vYYYYMMDD'. 258*9c5db199SXin Li Details: 259*9c5db199SXin Li https://cloud.google.com/compute/docs/reference/latest/instances/insert 260*9c5db199SXin Li zone: The zone to create the instance in. Default zone will be used if 261*9c5db199SXin Li omitted. 262*9c5db199SXin Li network: An existing network to create the instance in. Default network 263*9c5db199SXin Li will be used if omitted. 264*9c5db199SXin Li subnet: The subnet to create the instance in. 265*9c5db199SXin Li machine_type: The machine type to use. Default machine type will be used 266*9c5db199SXin Li if omitted. 267*9c5db199SXin Li default_scopes: If true, the default scopes are added to the instances. 268*9c5db199SXin Li static_address: External IP address to assign to the instance as a string. 269*9c5db199SXin Li If None an emphemeral address will be used. 270*9c5db199SXin Li kwargs: Other possible Instance Resource properties. 271*9c5db199SXin Li https://cloud.google.com/compute/docs/reference/latest/instances#resource 272*9c5db199SXin Li Note that values from kwargs will overrule properties constructed from 273*9c5db199SXin Li positinal arguments, i.e., name, image, zone, network and 274*9c5db199SXin Li machine_type. 275*9c5db199SXin Li 276*9c5db199SXin Li Returns: 277*9c5db199SXin Li URL to the created instance. 278*9c5db199SXin Li """ 279*9c5db199SXin Li logging.info('Creating instance "%s" with image "%s" ...', name, image) 280*9c5db199SXin Li network = 'global/networks/%s' % network or self._DEFAULT_NETWORK 281*9c5db199SXin Li machine_type = 'zones/%s/machineTypes/%s' % ( 282*9c5db199SXin Li zone or self.zone, machine_type or self._DEFAULT_MACHINE_TYPE) 283*9c5db199SXin Li service_accounts = ( 284*9c5db199SXin Li { 285*9c5db199SXin Li 'email': self._DEFAULT_SERVICE_ACCOUNT_EMAIL, 286*9c5db199SXin Li 'scopes': self._DEFAULT_INSTANCE_SCOPES, 287*9c5db199SXin Li }, 288*9c5db199SXin Li ) if default_scopes else () 289*9c5db199SXin Li 290*9c5db199SXin Li config = { 291*9c5db199SXin Li 'name': name, 292*9c5db199SXin Li 'machineType': machine_type, 293*9c5db199SXin Li 'disks': ( 294*9c5db199SXin Li { 295*9c5db199SXin Li 'boot': True, 296*9c5db199SXin Li 'autoDelete': True, 297*9c5db199SXin Li 'initializeParams': { 298*9c5db199SXin Li 'sourceImage': image, 299*9c5db199SXin Li }, 300*9c5db199SXin Li }, 301*9c5db199SXin Li ), 302*9c5db199SXin Li 'networkInterfaces': ( 303*9c5db199SXin Li { 304*9c5db199SXin Li 'network': network, 305*9c5db199SXin Li 'accessConfigs': ( 306*9c5db199SXin Li { 307*9c5db199SXin Li 'type': 'ONE_TO_ONE_NAT', 308*9c5db199SXin Li 'name': 'External NAT', 309*9c5db199SXin Li }, 310*9c5db199SXin Li ), 311*9c5db199SXin Li }, 312*9c5db199SXin Li ), 313*9c5db199SXin Li 'serviceAccounts' : service_accounts, 314*9c5db199SXin Li } 315*9c5db199SXin Li config.update(**kwargs) 316*9c5db199SXin Li if static_address is not None: 317*9c5db199SXin Li config['networkInterfaces'][0]['accessConfigs'][0]['natIP'] = ( 318*9c5db199SXin Li static_address) 319*9c5db199SXin Li if subnet is not None: 320*9c5db199SXin Li region = self.GetZoneRegion(zone) 321*9c5db199SXin Li config['networkInterfaces'][0]['subnetwork'] = ( 322*9c5db199SXin Li 'regions/%s/subnetworks/%s' % (region, subnet) 323*9c5db199SXin Li ) 324*9c5db199SXin Li operation = self.gce_client.instances().insert( 325*9c5db199SXin Li project=self.project, 326*9c5db199SXin Li zone=zone or self.zone, 327*9c5db199SXin Li body=config).execute() 328*9c5db199SXin Li self._WaitForZoneOperation( 329*9c5db199SXin Li operation['name'], 330*9c5db199SXin Li timeout_sec=self.INSTANCE_OPERATIONS_TIMEOUT_SEC, 331*9c5db199SXin Li timeout_handler=lambda: self.DeleteInstance(name)) 332*9c5db199SXin Li return operation['targetLink'] 333*9c5db199SXin Li 334*9c5db199SXin Li def DeleteInstance(self, name, zone=None): 335*9c5db199SXin Li """Deletes an instance with the name and waits until it's done. 336*9c5db199SXin Li 337*9c5db199SXin Li Args: 338*9c5db199SXin Li name: Name of the instance to delete. 339*9c5db199SXin Li zone: Zone where the instance is in. Default zone will be used if omitted. 340*9c5db199SXin Li """ 341*9c5db199SXin Li logging.info('Deleting instance "%s" ...', name) 342*9c5db199SXin Li operation = self.gce_client.instances().delete( 343*9c5db199SXin Li project=self.project, 344*9c5db199SXin Li zone=zone or self.zone, 345*9c5db199SXin Li instance=name).execute() 346*9c5db199SXin Li self._WaitForZoneOperation( 347*9c5db199SXin Li operation['name'], timeout_sec=self.INSTANCE_OPERATIONS_TIMEOUT_SEC) 348*9c5db199SXin Li 349*9c5db199SXin Li def StartInstance(self, name, zone=None): 350*9c5db199SXin Li """Starts an instance with the name and waits until it's done. 351*9c5db199SXin Li 352*9c5db199SXin Li Args: 353*9c5db199SXin Li name: Name of the instance to start. 354*9c5db199SXin Li zone: Zone where the instance is in. Default zone will be used if omitted. 355*9c5db199SXin Li """ 356*9c5db199SXin Li logging.info('Starting instance "%s" ...', name) 357*9c5db199SXin Li operation = self.gce_client.instances().start( 358*9c5db199SXin Li project=self.project, 359*9c5db199SXin Li zone=zone or self.zone, 360*9c5db199SXin Li instance=name).execute() 361*9c5db199SXin Li self._WaitForZoneOperation( 362*9c5db199SXin Li operation['name'], timeout_sec=self.INSTANCE_OPERATIONS_TIMEOUT_SEC) 363*9c5db199SXin Li 364*9c5db199SXin Li def StopInstance(self, name, zone=None): 365*9c5db199SXin Li """Stops an instance with the name and waits until it's done. 366*9c5db199SXin Li 367*9c5db199SXin Li Args: 368*9c5db199SXin Li name: Name of the instance to stop. 369*9c5db199SXin Li zone: Zone where the instance is in. Default zone will be used if omitted. 370*9c5db199SXin Li """ 371*9c5db199SXin Li logging.info('Stopping instance "%s" ...', name) 372*9c5db199SXin Li operation = self.gce_client.instances().stop( 373*9c5db199SXin Li project=self.project, 374*9c5db199SXin Li zone=zone or self.zone, 375*9c5db199SXin Li instance=name).execute() 376*9c5db199SXin Li self._WaitForZoneOperation( 377*9c5db199SXin Li operation['name'], timeout_sec=self.INSTANCE_OPERATIONS_TIMEOUT_SEC) 378*9c5db199SXin Li 379*9c5db199SXin Li def CreateImage(self, name, source): 380*9c5db199SXin Li """Creates an image with the given |source|. 381*9c5db199SXin Li 382*9c5db199SXin Li Args: 383*9c5db199SXin Li name: Name of the image to be created. 384*9c5db199SXin Li source: 385*9c5db199SXin Li Google Cloud Storage object of the source disk, e.g., 386*9c5db199SXin Li 'https://storage.googleapis.com/my-gcs-bucket/test_image.tar.gz'. 387*9c5db199SXin Li 388*9c5db199SXin Li Returns: 389*9c5db199SXin Li URL to the created image. 390*9c5db199SXin Li """ 391*9c5db199SXin Li logging.info('Creating image "%s" with "source" %s ...', name, source) 392*9c5db199SXin Li config = { 393*9c5db199SXin Li 'name': name, 394*9c5db199SXin Li 'rawDisk': { 395*9c5db199SXin Li 'source': source, 396*9c5db199SXin Li }, 397*9c5db199SXin Li } 398*9c5db199SXin Li operation = self.gce_client.images().insert( 399*9c5db199SXin Li project=self.project, 400*9c5db199SXin Li body=config).execute() 401*9c5db199SXin Li self._WaitForGlobalOperation(operation['name'], 402*9c5db199SXin Li timeout_sec=self.IMAGE_OPERATIONS_TIMEOUT_SEC, 403*9c5db199SXin Li timeout_handler=lambda: self.DeleteImage(name)) 404*9c5db199SXin Li return operation['targetLink'] 405*9c5db199SXin Li 406*9c5db199SXin Li def DeleteImage(self, name): 407*9c5db199SXin Li """Deletes an image and waits until it's deleted. 408*9c5db199SXin Li 409*9c5db199SXin Li Args: 410*9c5db199SXin Li name: Name of the image to delete. 411*9c5db199SXin Li """ 412*9c5db199SXin Li logging.info('Deleting image "%s" ...', name) 413*9c5db199SXin Li operation = self.gce_client.images().delete( 414*9c5db199SXin Li project=self.project, 415*9c5db199SXin Li image=name).execute() 416*9c5db199SXin Li self._WaitForGlobalOperation(operation['name'], 417*9c5db199SXin Li timeout_sec=self.IMAGE_OPERATIONS_TIMEOUT_SEC) 418*9c5db199SXin Li 419*9c5db199SXin Li def ListInstances(self, zone=None): 420*9c5db199SXin Li """Lists all instances. 421*9c5db199SXin Li 422*9c5db199SXin Li Args: 423*9c5db199SXin Li zone: Zone where the instances are in. Default zone will be used if 424*9c5db199SXin Li omitted. 425*9c5db199SXin Li 426*9c5db199SXin Li Returns: 427*9c5db199SXin Li A list of Instance Resources if found, or an empty list otherwise. 428*9c5db199SXin Li """ 429*9c5db199SXin Li result = self.gce_client.instances().list(project=self.project, 430*9c5db199SXin Li zone=zone or self.zone).execute() 431*9c5db199SXin Li return result.get('items', []) 432*9c5db199SXin Li 433*9c5db199SXin Li def ListImages(self): 434*9c5db199SXin Li """Lists all images. 435*9c5db199SXin Li 436*9c5db199SXin Li Returns: 437*9c5db199SXin Li A list of Image Resources if found, or an empty list otherwise. 438*9c5db199SXin Li """ 439*9c5db199SXin Li result = self.gce_client.images().list(project=self.project).execute() 440*9c5db199SXin Li return result.get('items', []) 441*9c5db199SXin Li 442*9c5db199SXin Li def GetInstance(self, instance, zone=None): 443*9c5db199SXin Li """Gets an Instance Resource by name and zone. 444*9c5db199SXin Li 445*9c5db199SXin Li Args: 446*9c5db199SXin Li instance: Name of the instance. 447*9c5db199SXin Li zone: Zone where the instance is in. Default zone will be used if omitted. 448*9c5db199SXin Li 449*9c5db199SXin Li Returns: 450*9c5db199SXin Li An Instance Resource. 451*9c5db199SXin Li 452*9c5db199SXin Li Raises: 453*9c5db199SXin Li ResourceNotFoundError if instance was not found, or HttpError on other 454*9c5db199SXin Li HTTP failures. 455*9c5db199SXin Li """ 456*9c5db199SXin Li try: 457*9c5db199SXin Li return self.gce_client.instances().get(project=self.project, 458*9c5db199SXin Li zone=zone or self.zone, 459*9c5db199SXin Li instance=instance).execute() 460*9c5db199SXin Li except HttpError as e: 461*9c5db199SXin Li if e.resp.status == 404: 462*9c5db199SXin Li raise ResourceNotFoundError( 463*9c5db199SXin Li 'Instance "%s" for project "%s" in zone "%s" was not found.' % 464*9c5db199SXin Li (instance, self.project, zone or self.zone)) 465*9c5db199SXin Li else: 466*9c5db199SXin Li raise 467*9c5db199SXin Li 468*9c5db199SXin Li def GetInstanceIP(self, instance, zone=None): 469*9c5db199SXin Li """Gets the external IP of an instance. 470*9c5db199SXin Li 471*9c5db199SXin Li Args: 472*9c5db199SXin Li instance: Name of the instance to get IP for. 473*9c5db199SXin Li zone: Zone where the instance is in. Default zone will be used if omitted. 474*9c5db199SXin Li 475*9c5db199SXin Li Returns: 476*9c5db199SXin Li External IP address of the instance. 477*9c5db199SXin Li 478*9c5db199SXin Li Raises: 479*9c5db199SXin Li Error: Something went wrong when trying to get IP for the instance. 480*9c5db199SXin Li """ 481*9c5db199SXin Li result = self.GetInstance(instance, zone) 482*9c5db199SXin Li try: 483*9c5db199SXin Li return result['networkInterfaces'][0]['accessConfigs'][0]['natIP'] 484*9c5db199SXin Li except (KeyError, IndexError): 485*9c5db199SXin Li raise Error('Failed to get IP address for instance %s' % instance) 486*9c5db199SXin Li 487*9c5db199SXin Li def GetInstanceInternalIP(self, instance, zone=None): 488*9c5db199SXin Li """Gets the internal IP of an instance.""" 489*9c5db199SXin Li result = self.GetInstance(instance, zone) 490*9c5db199SXin Li try: 491*9c5db199SXin Li return result['networkInterfaces'][0]['networkIP'] 492*9c5db199SXin Li except (KeyError, IndexError): 493*9c5db199SXin Li raise Error('Failed to get internal IP for instance %s' % instance) 494*9c5db199SXin Li 495*9c5db199SXin Li def GetImage(self, image): 496*9c5db199SXin Li """Gets an Image Resource by name. 497*9c5db199SXin Li 498*9c5db199SXin Li Args: 499*9c5db199SXin Li image: Name of the image to look for. 500*9c5db199SXin Li 501*9c5db199SXin Li Returns: 502*9c5db199SXin Li An Image Resource. 503*9c5db199SXin Li 504*9c5db199SXin Li Raises: 505*9c5db199SXin Li ResourceNotFoundError: The requested image was not found. 506*9c5db199SXin Li """ 507*9c5db199SXin Li try: 508*9c5db199SXin Li return self.gce_client.images().get(project=self.project, 509*9c5db199SXin Li image=image).execute() 510*9c5db199SXin Li except HttpError as e: 511*9c5db199SXin Li if e.resp.status == 404: 512*9c5db199SXin Li raise ResourceNotFoundError('Image "%s" for project "%s" was not found.' 513*9c5db199SXin Li % (image, self.project)) 514*9c5db199SXin Li else: 515*9c5db199SXin Li raise 516*9c5db199SXin Li 517*9c5db199SXin Li def InstanceExists(self, instance, zone=None): 518*9c5db199SXin Li """Checks if an instance exists in the current project. 519*9c5db199SXin Li 520*9c5db199SXin Li Args: 521*9c5db199SXin Li instance: Name of the instance to check existence of. 522*9c5db199SXin Li zone: Zone where the instance is in. Default zone will be used if omitted. 523*9c5db199SXin Li 524*9c5db199SXin Li Returns: 525*9c5db199SXin Li True if the instance exists or False otherwise. 526*9c5db199SXin Li """ 527*9c5db199SXin Li try: 528*9c5db199SXin Li return self.GetInstance(instance, zone) is not None 529*9c5db199SXin Li except ResourceNotFoundError: 530*9c5db199SXin Li return False 531*9c5db199SXin Li 532*9c5db199SXin Li def ImageExists(self, image): 533*9c5db199SXin Li """Checks if an image exists in the current project. 534*9c5db199SXin Li 535*9c5db199SXin Li Args: 536*9c5db199SXin Li image: Name of the image to check existence of. 537*9c5db199SXin Li 538*9c5db199SXin Li Returns: 539*9c5db199SXin Li True if the instance exists or False otherwise. 540*9c5db199SXin Li """ 541*9c5db199SXin Li try: 542*9c5db199SXin Li return self.GetImage(image) is not None 543*9c5db199SXin Li except ResourceNotFoundError: 544*9c5db199SXin Li return False 545*9c5db199SXin Li 546*9c5db199SXin Li def GetCommonInstanceMetadata(self, key): 547*9c5db199SXin Li """Looks up a single project metadata value. 548*9c5db199SXin Li 549*9c5db199SXin Li Args: 550*9c5db199SXin Li key: Metadata key name. 551*9c5db199SXin Li 552*9c5db199SXin Li Returns: 553*9c5db199SXin Li Metadata value corresponding to the key, or None if it was not found. 554*9c5db199SXin Li """ 555*9c5db199SXin Li projects_data = self.gce_client.projects().get( 556*9c5db199SXin Li project=self.project).execute() 557*9c5db199SXin Li metadata = projects_data['commonInstanceMetadata'] 558*9c5db199SXin Li return _GetMetdataValue(metadata, key) 559*9c5db199SXin Li 560*9c5db199SXin Li def SetCommonInstanceMetadata(self, key, value): 561*9c5db199SXin Li """Sets a single project metadata value. 562*9c5db199SXin Li 563*9c5db199SXin Li Args: 564*9c5db199SXin Li key: Metadata key to be set. 565*9c5db199SXin Li value: New value, or None if the given key should be removed. 566*9c5db199SXin Li """ 567*9c5db199SXin Li projects_data = self.gce_client.projects().get( 568*9c5db199SXin Li project=self.project).execute() 569*9c5db199SXin Li metadata = projects_data['commonInstanceMetadata'] 570*9c5db199SXin Li _UpdateMetadataValue(metadata, key, value) 571*9c5db199SXin Li operation = self.gce_client.projects().setCommonInstanceMetadata( 572*9c5db199SXin Li project=self.project, 573*9c5db199SXin Li body=metadata).execute() 574*9c5db199SXin Li self._WaitForGlobalOperation(operation['name']) 575*9c5db199SXin Li 576*9c5db199SXin Li def GetInstanceMetadata(self, instance, key): 577*9c5db199SXin Li """Looks up instance's metadata value. 578*9c5db199SXin Li 579*9c5db199SXin Li Args: 580*9c5db199SXin Li instance: Name of the instance. 581*9c5db199SXin Li key: Metadata key name. 582*9c5db199SXin Li 583*9c5db199SXin Li Returns: 584*9c5db199SXin Li Metadata value corresponding to the key, or None if it was not found. 585*9c5db199SXin Li """ 586*9c5db199SXin Li instance_data = self.GetInstance(instance) 587*9c5db199SXin Li metadata = instance_data['metadata'] 588*9c5db199SXin Li return self._GetMetdataValue(metadata, key) 589*9c5db199SXin Li 590*9c5db199SXin Li def SetInstanceMetadata(self, instance, key, value): 591*9c5db199SXin Li """Sets a single instance metadata value. 592*9c5db199SXin Li 593*9c5db199SXin Li Args: 594*9c5db199SXin Li instance: Name of the instance. 595*9c5db199SXin Li key: Metadata key to be set. 596*9c5db199SXin Li value: New value, or None if the given key should be removed. 597*9c5db199SXin Li """ 598*9c5db199SXin Li instance_data = self.GetInstance(instance) 599*9c5db199SXin Li metadata = instance_data['metadata'] 600*9c5db199SXin Li _UpdateMetadataValue(metadata, key, value) 601*9c5db199SXin Li operation = self.gce_client.instances().setMetadata( 602*9c5db199SXin Li project=self.project, 603*9c5db199SXin Li zone=self.zone, 604*9c5db199SXin Li instance=instance, 605*9c5db199SXin Li body=metadata).execute() 606*9c5db199SXin Li self._WaitForZoneOperation(operation['name']) 607*9c5db199SXin Li 608*9c5db199SXin Li def _WaitForZoneOperation(self, operation, zone=None, timeout_sec=None, 609*9c5db199SXin Li timeout_handler=None): 610*9c5db199SXin Li """Waits until a GCE ZoneOperation is finished or timed out. 611*9c5db199SXin Li 612*9c5db199SXin Li Args: 613*9c5db199SXin Li operation: The GCE operation to wait for. 614*9c5db199SXin Li zone: The zone that |operation| belongs to. 615*9c5db199SXin Li timeout_sec: The maximum number of seconds to wait for. 616*9c5db199SXin Li timeout_handler: A callable to be executed when timeout happens. 617*9c5db199SXin Li 618*9c5db199SXin Li Raises: 619*9c5db199SXin Li Error when timeout happens or the operation fails. 620*9c5db199SXin Li """ 621*9c5db199SXin Li get_request = self.gce_client.zoneOperations().get( 622*9c5db199SXin Li project=self.project, zone=zone or self.zone, operation=operation) 623*9c5db199SXin Li self._WaitForOperation(operation, get_request, timeout_sec, 624*9c5db199SXin Li timeout_handler=timeout_handler) 625*9c5db199SXin Li 626*9c5db199SXin Li def _WaitForRegionOperation(self, operation, region, timeout_sec=None, 627*9c5db199SXin Li timeout_handler=None): 628*9c5db199SXin Li """Waits until a GCE RegionOperation is finished or timed out. 629*9c5db199SXin Li 630*9c5db199SXin Li Args: 631*9c5db199SXin Li operation: The GCE operation to wait for. 632*9c5db199SXin Li region: The region that |operation| belongs to. 633*9c5db199SXin Li timeout_sec: The maximum number of seconds to wait for. 634*9c5db199SXin Li timeout_handler: A callable to be executed when timeout happens. 635*9c5db199SXin Li 636*9c5db199SXin Li Raises: 637*9c5db199SXin Li Error when timeout happens or the operation fails. 638*9c5db199SXin Li """ 639*9c5db199SXin Li get_request = self.gce_client.regionOperations().get( 640*9c5db199SXin Li project=self.project, region=region or self.region, operation=operation) 641*9c5db199SXin Li self._WaitForOperation(operation, get_request, timeout_sec, 642*9c5db199SXin Li timeout_handler=timeout_handler) 643*9c5db199SXin Li 644*9c5db199SXin Li def _WaitForGlobalOperation(self, operation, timeout_sec=None, 645*9c5db199SXin Li timeout_handler=None): 646*9c5db199SXin Li """Waits until a GCE GlobalOperation is finished or timed out. 647*9c5db199SXin Li 648*9c5db199SXin Li Args: 649*9c5db199SXin Li operation: The GCE operation to wait for. 650*9c5db199SXin Li timeout_sec: The maximum number of seconds to wait for. 651*9c5db199SXin Li timeout_handler: A callable to be executed when timeout happens. 652*9c5db199SXin Li 653*9c5db199SXin Li Raises: 654*9c5db199SXin Li Error when timeout happens or the operation fails. 655*9c5db199SXin Li """ 656*9c5db199SXin Li get_request = self.gce_client.globalOperations().get(project=self.project, 657*9c5db199SXin Li operation=operation) 658*9c5db199SXin Li self._WaitForOperation(operation, get_request, timeout_sec=timeout_sec, 659*9c5db199SXin Li timeout_handler=timeout_handler) 660*9c5db199SXin Li 661*9c5db199SXin Li def _WaitForOperation(self, operation, get_operation_request, 662*9c5db199SXin Li timeout_sec=None, timeout_handler=None): 663*9c5db199SXin Li """Waits until timeout or the request gets a response with a 'DONE' status. 664*9c5db199SXin Li 665*9c5db199SXin Li Args: 666*9c5db199SXin Li operation: The GCE operation to wait for. 667*9c5db199SXin Li get_operation_request: 668*9c5db199SXin Li The HTTP request to get the operation's status. 669*9c5db199SXin Li This request will be executed periodically until it returns a status 670*9c5db199SXin Li 'DONE'. 671*9c5db199SXin Li timeout_sec: The maximum number of seconds to wait for. 672*9c5db199SXin Li timeout_handler: A callable to be executed when times out. 673*9c5db199SXin Li 674*9c5db199SXin Li Raises: 675*9c5db199SXin Li Error when timeout happens or the operation fails. 676*9c5db199SXin Li """ 677*9c5db199SXin Li def _IsDone(): 678*9c5db199SXin Li result = get_operation_request.execute() 679*9c5db199SXin Li if result['status'] == 'DONE': 680*9c5db199SXin Li if 'error' in result: 681*9c5db199SXin Li raise Error(result['error']) 682*9c5db199SXin Li return True 683*9c5db199SXin Li return False 684*9c5db199SXin Li 685*9c5db199SXin Li try: 686*9c5db199SXin Li timeout = timeout_sec or self.DEFAULT_TIMEOUT_SEC 687*9c5db199SXin Li logging.info('Waiting up to %d seconds for operation [%s] to complete...', 688*9c5db199SXin Li timeout, operation) 689*9c5db199SXin Li timeout_util.WaitForReturnTrue(_IsDone, timeout, period=1) 690*9c5db199SXin Li except timeout_util.TimeoutError: 691*9c5db199SXin Li if timeout_handler: 692*9c5db199SXin Li timeout_handler() 693*9c5db199SXin Li raise Error('Timeout wating for operation [%s] to complete' % operation) 694*9c5db199SXin Li 695*9c5db199SXin Li def _BuildRetriableRequest(self, num_retries, http, thread_safe=False, 696*9c5db199SXin Li credentials=None, *args, **kwargs): 697*9c5db199SXin Li """Builds a request that will be automatically retried on server errors. 698*9c5db199SXin Li 699*9c5db199SXin Li Args: 700*9c5db199SXin Li num_retries: The maximum number of times to retry until give up. 701*9c5db199SXin Li http: An httplib2.Http object that this request will be executed through. 702*9c5db199SXin Li thread_safe: Whether or not the request needs to be thread-safe. 703*9c5db199SXin Li credentials: Credentials to apply to the request. 704*9c5db199SXin Li *args: Optional positional arguments. 705*9c5db199SXin Li **kwargs: Optional keyword arguments. 706*9c5db199SXin Li 707*9c5db199SXin Li Returns: 708*9c5db199SXin Li RetryOnServerErrorHttpRequest: A request that will automatically retried 709*9c5db199SXin Li on server errors. 710*9c5db199SXin Li """ 711*9c5db199SXin Li if thread_safe: 712*9c5db199SXin Li # Create a new http object for every request. 713*9c5db199SXin Li http = credentials.authorize(httplib2.Http()) 714*9c5db199SXin Li return RetryOnServerErrorHttpRequest(num_retries, http, *args, **kwargs) 715