blob: e4199b51227cee3855a3a6af66a729faade66d10 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import json
import time
from googleapiclient import (discovery, errors)
import common.google_error_helper as google_error_helper
class GoogleInstanceHelper(object):
"""Helper class for the Google Compute API, allowing to manage groups of
instances more easily. Groups of instances are identified by a tag."""
_COMPUTE_API_ROOT = 'https://www.googleapis.com/compute/v1/projects/'
def __init__(self, credentials, project, logger):
self._compute_api = discovery.build('compute','v1', credentials=credentials)
self._project = project
self._project_api_url = self._COMPUTE_API_ROOT + project
self._region = 'europe-west1'
self._zone = 'europe-west1-c'
self._logger = logger
def _ExecuteApiRequest(self, request, retry_count=3):
""" Executes a Compute API request and returns True on success.
Returns:
(True, Response) in case of success, or (False, error_content) otherwise.
"""
self._logger.info('Compute API request:\n' + request.to_json())
try:
response = request.execute()
self._logger.info('Compute API response:\n' + str(response))
return (True, response)
except errors.HttpError as err:
error_content = google_error_helper.GetErrorContent(err)
error_reason = google_error_helper.GetErrorReason(error_content)
if error_reason == 'resourceNotReady' and retry_count > 0:
# Retry after a delay
delay_seconds = 1
self._logger.info(
'Resource not ready, retrying in %i seconds.' % delay_seconds)
time.sleep(delay_seconds)
return self._ExecuteApiRequest(request, retry_count - 1)
else:
self._logger.error('Compute API error (reason: "%s"):\n%s' % (
error_reason, err))
if error_content:
self._logger.error('Error details:\n%s' % error_content)
return (False, error_content)
def _GetTemplateName(self, tag):
"""Returns the name of the instance template associated with tag."""
return 'template-' + tag
def _GetInstanceGroupName(self, tag):
"""Returns the name of the instance group associated with tag."""
return 'group-' + tag
def CreateTemplate(self, tag, bucket, task_dir):
"""Creates an instance template for instances identified by tag.
Args:
tag: (string) Tag associated to a task.
bucket: (string) Root bucket where the deployment is located.
task_dir: (string) Subdirectory of |bucket| where task data is read and
written.
Returns:
boolean: True if successful.
"""
image_url = self._COMPUTE_API_ROOT + \
'ubuntu-os-cloud/global/images/ubuntu-1404-trusty-v20160406'
request_body = {
'name': self._GetTemplateName(tag),
'properties': {
'machineType': 'n1-standard-1',
'networkInterfaces': [{
'network': self._project_api_url + '/global/networks/default',
'accessConfigs': [{
'name': 'external-IP',
'type': 'ONE_TO_ONE_NAT'
}]}],
'disks': [{
'type': 'PERSISTENT',
'boot': True,
'autoDelete': True,
'mode': 'READ_WRITE',
'initializeParams': {'sourceImage': image_url}}],
'canIpForward': False,
'scheduling': {
'automaticRestart': True,
'onHostMaintenance': 'MIGRATE',
'preemptible': False},
'serviceAccounts': [{
'scopes': [
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/cloud-taskqueue'],
'email': 'default'}],
'metadata': { 'items': [
{'key': 'cloud-storage-path',
'value': bucket},
{'key': 'task-dir',
'value': task_dir},
{'key': 'startup-script-url',
'value': 'gs://%s/deployment/startup-script.sh' % bucket},
{'key': 'taskqueue-tag', 'value': tag}]}}}
request = self._compute_api.instanceTemplates().insert(
project=self._project, body=request_body)
return self._ExecuteApiRequest(request)[0]
def DeleteTemplate(self, tag):
"""Deletes the instance template associated with tag. Returns True if
successful.
"""
template_name = self._GetTemplateName(tag)
request = self._compute_api.instanceTemplates().delete(
project=self._project, instanceTemplate=template_name)
(success, result) = self._ExecuteApiRequest(request)
if success:
return True
if google_error_helper.GetErrorReason(result) == \
google_error_helper.REASON_NOT_FOUND:
# The template does not exist, nothing to do.
self._logger.warning('Template not found: ' + template_name)
return True
return False
def CreateInstances(self, tag, instance_count):
"""Creates an instance group associated with tag. The instance template must
exist for this to succeed. Returns True if successful.
"""
template_url = '%s/global/instanceTemplates/%s' % (
self._project_api_url, self._GetTemplateName(tag))
request_body = {
'zone': self._zone, 'targetSize': instance_count,
'baseInstanceName': 'instance-' + tag,
'instanceTemplate': template_url,
'name': self._GetInstanceGroupName(tag)}
request = self._compute_api.instanceGroupManagers().insert(
project=self._project, zone=self._zone,
body=request_body)
return self._ExecuteApiRequest(request)[0]
def DeleteInstance(self, tag, instance_hostname):
"""Deletes one instance from the instance group identified by tag. Returns
True if successful.
"""
# The instance hostname may be of the form <name>.c.<project>.internal but
# only the <name> part should be passed to the compute API.
name = instance_hostname.split('.')[0]
instance_url = self._project_api_url + (
"/zones/%s/instances/%s" % (self._zone, name))
request = self._compute_api.instanceGroupManagers().deleteInstances(
project=self._project, zone=self._zone,
instanceGroupManager=self._GetInstanceGroupName(tag),
body={'instances': [instance_url]})
return self._ExecuteApiRequest(request)[0]
def DeleteInstanceGroup(self, tag):
"""Deletes the instance group identified by tag. If instances are still
running in this group, they are deleted as well.
"""
group_name = self._GetInstanceGroupName(tag)
request = self._compute_api.instanceGroupManagers().delete(
project=self._project, zone=self._zone,
instanceGroupManager=group_name)
(success, result) = self._ExecuteApiRequest(request)
if success:
return True
if google_error_helper.GetErrorReason(result) == \
google_error_helper.REASON_NOT_FOUND:
# The group does not exist, nothing to do.
self._logger.warning('Instance group not found: ' + group_name)
return True
return False
def GetInstanceCount(self, tag):
"""Returns the number of instances in the instance group identified by
tag, or -1 in case of failure.
"""
request = self._compute_api.instanceGroupManagers().listManagedInstances(
project=self._project, zone=self._zone,
instanceGroupManager=self._GetInstanceGroupName(tag))
(success, response) = self._ExecuteApiRequest(request)
if not success:
return -1
return len(response.get('managedInstances', []))
def GetAvailableInstanceCount(self):
"""Returns the number of instances that can be created, according to the
ComputeEngine quotas, or -1 on failure.
"""
request = self._compute_api.regions().get(project=self._project,
region=self._region)
(success, response) = self._ExecuteApiRequest(request)
if not success:
self._logger.error('Could not get ComputeEngine region information.')
return -1
metric_name = 'IN_USE_ADDRESSES'
for quota in response.get('quotas', []):
if quota['metric'] == metric_name:
return quota['limit'] - quota['usage']
self._logger.error(
metric_name + ' quota not found in ComputeEngine response.')
return -1