xref: /aosp_15_r20/external/autotest/utils/frozen_chromite/lib/gce.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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