omni/nova/virt/gce/gceutils.py

539 lines
21 KiB
Python

# Copyright (c) 2017 Platform9 Systems Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time
import six
from oslo_log import log as logging
from nova.i18n import _LI, _
from googleapiclient.discovery import build
from oauth2client.client import GoogleCredentials
from oslo_service import loopingcall
from oslo_utils import reflection
from six.moves import urllib
LOG = logging.getLogger(__name__)
class _FixedIntervalWithTimeoutLoopingCall(loopingcall.LoopingCallBase):
"""A fixed interval looping call with timeout checking mechanism."""
_RUN_ONLY_ONE_MESSAGE = _("A fixed interval looping call with timeout"
" checking and can only run one function at"
" at a time")
_KIND = _('Fixed interval looping call with timeout checking.')
def start(self, interval, initial_delay=None, stop_on_exception=True,
timeout=0):
start_time = time.time()
def _idle_for(result, elapsed):
delay = round(elapsed - interval, 2)
if delay > 0:
func_name = reflection.get_callable_name(self.f)
LOG.warning('Function %(func_name)r run outlasted '
'interval by %(delay).2f sec',
{'func_name': func_name,
'delay': delay})
elapsed_time = time.time() - start_time
if timeout > 0 and elapsed_time > timeout:
raise loopingcall.LoopingCallTimeOut(
_('Looping call timed out after %.02f seconds') %
elapsed_time)
return -delay if delay < 0 else 0
return self._start(_idle_for, initial_delay=initial_delay,
stop_on_exception=stop_on_exception)
# Currently, default oslo.service version(newton) is 1.16.0.
# Once we upgrade oslo.service >= 1.19.0, we can remove temporary
# definition _FixedIntervalWithTimeoutLoopingCall
if not hasattr(loopingcall, 'FixedIntervalWithTimeoutLoopingCall'):
loopingcall.FixedIntervalWithTimeoutLoopingCall = \
_FixedIntervalWithTimeoutLoopingCall
class GceOperationError(Exception):
pass
def list_instances(compute, project, zone):
"""Returns list of GCE instance resources for specified project
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
"""
result = compute.instances().list(project=project, zone=zone).execute()
if 'items' not in result:
return []
return result['items']
def get_instance(compute, project, zone, instance):
"""Get GCE instance information
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param instance: string, Name of the GCE instance resource
"""
result = compute.instances().get(project=project, zone=zone,
instance=instance).execute()
return result
def get_instance_metadata(compute, project, zone, instance):
"""Returns specified instance's metadata
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param instance: string or instance resource, Name of the GCE instance
resource or GCE instance resource
"""
if isinstance(instance, six.string_types):
instance = get_instance(compute, project, zone, instance)
return instance['metadata']
def get_instances_metadata_key(compute, project, zone, instance, key):
"""Returns particular key information for specified instance
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param instance: string or instance resource, Name of the GCE instance
resource or GCE instance resource
:param key: string, Key to retrieved from the instance metadata
"""
metadata = get_instance_metadata(compute, project, zone, instance)
if 'items' in metadata:
for item in metadata['items']:
if item['key'] == key:
return item['value']
return None
def get_external_ip(compute, project, zone, instance):
""" Return external IP of GCE instance return empty string otherwise
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param instance: string or instance resource, Name of the GCE instance
resource or GCE instance resource
"""
if isinstance(instance, six.string_types):
instance = get_instance(compute, project, zone, instance)
for interface in instance.get('networkInterfaces', []):
for config in interface.get('accessConfigs', []):
if config['type'] == 'ONE_TO_ONE_NAT' and 'natIP' in config:
return config['natIP']
return ''
def set_instance_metadata(compute, project, zone, instance, items,
operation='add'):
"""Perform specified operation on GCE instance metadata
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param instance: string or instance resource, Name of the GCE instance
resource or GCE instance resource
:param items: list, List of items where each item is dictionary having
'key' and 'value' as its members
Refer following sample list,
[ {'key': 'openstack_id', 'value': '1224555'}, ]
:param operation: string, Operation to perform on instance metadata
"""
if not isinstance(items, list):
raise TypeError(
"set_instance_metadata: items should be instance of list")
metadata = get_instance_metadata(compute, project, zone, instance)
if operation == 'add':
if 'items' in metadata:
metadata['items'].extend(items)
else:
metadata['items'] = items
LOG.info("Adding metadata %s" % (metadata, ))
# TODO: Add del operation if required
return compute.instances().setMetadata(project=project, zone=zone,
instance=instance,
body=metadata).execute()
def create_instance(compute, project, zone, name, image_link, machine_link,
network_interfaces):
"""Create GCE instance
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, Name of instance to be launched
:param image_link: url, GCE Image link for instance launch
:param machine_link: url, GCE Machine link for instance launch
"""
LOG.info(
_LI("Launching instance %s with image %s, machine %s and network %s") %
(name, image_link, machine_link, network_interfaces))
config = {
'kind': 'compute#instance',
'name': name,
'machineType': machine_link,
'networkInterfaces': network_interfaces,
# Specify the boot disk and the image to use as a source.
'disks': [{
'boot': True,
'autoDelete': True,
'initializeParams': {
'sourceImage': image_link,
}
}],
# Allow the instance to access cloud storage and logging.
'serviceAccounts': [{
'email':
'default',
'scopes': [
'https://www.googleapis.com/auth/devstorage.full_control',
'https://www.googleapis.com/auth/logging.write',
'https://www.googleapis.com/auth/compute'
]
}],
} # yapf:disable
return compute.instances().insert(project=project, zone=zone,
body=config).execute()
def delete_instance(compute, project, zone, name):
"""Delete GCE instance
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, Name of the GCE instance
"""
return compute.instances().delete(project=project, zone=zone,
instance=name).execute()
def stop_instance(compute, project, zone, name):
"""Stop GCE instance
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, Name of the GCE instance
"""
return compute.instances().stop(project=project, zone=zone,
instance=name).execute()
def start_instance(compute, project, zone, name):
"""Start GCE instance
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, Name of the GCE instance
"""
return compute.instances().start(project=project, zone=zone,
instance=name).execute()
def reset_instance(compute, project, zone, name):
"""Hard reset GCE instance
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, Name of the GCE instance
"""
return compute.instances().reset(project=project, zone=zone,
instance=name).execute()
def wait_for_operation(compute, project, operation, interval=1, timeout=60):
"""Wait for GCE operation to complete, raise error if operation failure
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param operation: object, Operation resource obtained by calling GCE asynchronous API
All GCE asynchronous API's return operation resource to followup there completion.
:param interval: int, Time period(seconds) between two GCE operation checks
:param timeout: int, Absoulte time period(seconds) to monitor GCE operation
"""
def watch_operation(name, request):
result = request.execute()
if result['status'] == 'DONE':
LOG.info(
_LI("Operation %s status is %s") % (name, result['status']))
if 'error' in result:
raise GceOperationError(result['error'])
raise loopingcall.LoopingCallDone()
operation_name = operation['name']
if 'zone' in operation:
zone = operation['zone'].split('/')[-1]
monitor_request = compute.zoneOperations().get(
project=project, zone=zone, operation=operation_name)
elif 'region' in operation:
region = operation['region'].split('/')[-1]
monitor_request = compute.regionOperations().get(
project=project, region=region, operation=operation_name)
else:
monitor_request = compute.globalOperations().get(
project=project, operation=operation_name)
timer = loopingcall.FixedIntervalWithTimeoutLoopingCall(
watch_operation, operation_name, monitor_request)
timer.start(interval=interval, timeout=timeout).wait()
def get_gce_service(service_key):
"""Returns GCE compute resource object for interacting with GCE API
:param service_key: string, Path of service key obtained from
https://console.cloud.google.com/apis/credentials
"""
credentials = GoogleCredentials.from_stream(service_key)
service = build('compute', 'v1', credentials=credentials)
return service
def get_machines_info(compute, project, zone):
"""Return machine type info from GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
"""
response = compute.machineTypes().list(project=project,
zone=zone).execute()
GCE_MAP = {
machine_type['name']: {
'memory_mb': machine_type['memoryMb'],
'vcpus': machine_type['guestCpus']
}
for machine_type in response['items']
}
return GCE_MAP
def get_images(compute, project):
"""Return public images info from GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
"""
response = compute.images().list(project=project,
filter="status eq READY").execute()
if 'items' not in response:
return []
imgs = filter(lambda img: 'deprecated' not in img, response['items'])
return imgs
def get_image(compute, project, name):
"""Return public images info from GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
"""
result = compute.images().get(project=project, image=name).execute()
return result
def delete_image(compute, project, name):
"""Delete image from GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param name: string, GCE image name
:return: Operation information
:rtype: dict
"""
result = compute.images().delete(project=project, image=name).execute()
return result
def get_network(compute, project, name):
"""Return network info
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param name: string, GCE network name
"""
result = compute.networks().get(project=project, network=name).execute()
return result
def attach_disk(compute, project, zone, instance_name, disk_name, disk_link):
"""Attach disk to instance
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param instance_name: string, GCE instance name
:param disk_name: string, GCE disk name
:param disk_link: url, GCE disk link
:return: Operation information
:rtype: dict
"""
body = {
"type": "PERSISTENT",
"mode": "READ_WRITE",
"source": disk_link,
"deviceName": disk_name,
"boot": False,
"autoDelete": False,
"interface": "SCSI"
}
return compute.instances().attachDisk(project=project, zone=zone,
instance=instance_name,
body=body).execute()
def detach_disk(compute, project, zone, instance_name, disk_name):
"""Detach disk from instance
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param instance_name: string, GCE instance name
:param disk_name: string, GCE disk name
:return: Operation information
:rtype: dict
"""
return compute.instances().detachDisk(project=project, zone=zone,
instance=instance_name,
deviceName=disk_name).execute()
def get_instance_boot_disk(compute, project, zone, instance):
"""Return boot disk info for instance
"""
gce_instance = get_instance(compute, project, zone, instance)
for disk in gce_instance['disks']:
if disk['boot']:
disk_url = disk['source']
# Extracting disk details from disk URL,
# Eg. projects/<project>/zones/<zone>/disks/<disk_name>
items = urllib.parse.urlparse(disk_url).path.strip('/').split('/')
if len(items) < 4 or items[-2] != 'disks':
LOG.error(_LI('Invalid disk URL %s') % (disk_url))
disk_name, zone = items[-1], items[-3]
disk_info = get_disk(compute, project, zone, disk_name)
return disk_info
# We should never reach here
raise AssertionError("Boot disk not found for instance %s" % instance)
def create_disk(compute, project, zone, name, size):
"""Create disk in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, GCE disk name
:param size: int, size of disk inn Gb
:return: Operation information
:rtype: dict
"""
body = {
"name": name,
"zone": "projects/%s/zones/%s" % (project, zone),
"type": "projects/%s/zones/%s/diskTypes/pd-standard" % (project, zone),
"sizeGb": size
}
return compute.disks().insert(project=project, zone=zone, body=body,
sourceImage=None).execute()
def delete_disk(compute, project, zone, name):
"""Delete disk in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, GCE disk name
:return: Operation information
:rtype: dict
"""
return compute.disks().delete(project=project, zone=zone,
disk=name).execute()
def get_disk(compute, project, zone, name):
"""Get info of disk in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, GCE disk name
:return: GCE disk information
:rtype: dict
"""
return compute.disks().get(project=project, zone=zone, disk=name).execute()
def snapshot_disk(compute, project, zone, name, snapshot_name):
"""Create snapshot of disk in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, GCE disk name
:param snapshot_name: string, GCE snapshot name
:return: Operation information
:rtype: dict
"""
body = {"name": snapshot_name}
return compute.disks().createSnapshot(project=project, zone=zone,
disk=name, body=body).execute()
def get_snapshot(compute, project, name):
"""Get info of snapshot in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param name: string, GCE snapshot name
:return: GCE snapshot information
:rtype: dict
"""
return compute.snapshots().get(project=project, snapshot=name).execute()
def delete_snapshot(compute, project, name):
"""Delete snapshot in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param name: string, GCE snapshot name
:return: Operation information
:rtype: dict
"""
return compute.snapshots().delete(project=project, snapshot=name).execute()
def create_disk_from_snapshot(compute, project, zone, name, snapshot_name,
disk_type="pd-standard"):
"""Create disk from snapshot in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, GCE disk name
:param snapshot_name: string, GCE snapshot name
:param disk_type: string, Disk type from (pd-standard, pd-sdd, local-ssd)
:return: Operation information
:rtype: dict
"""
gce_snapshot = get_snapshot(compute, project, snapshot_name)
body = {
"name": name,
"zone": "projects/%s/zones/%s" % (project, zone),
"type": "projects/%s/zones/%s/diskTypes/%s" % (project, zone,
disk_type),
"sourceSnapshot": gce_snapshot["selfLink"],
"sizeGb": gce_snapshot["diskSizeGb"]
}
return compute.disks().insert(project=project, zone=zone, body=body,
sourceImage=None).execute()
def create_image_from_disk(compute, project, name, disk_link):
body = {"sourceDisk": disk_link, "name": name, "rawDisk": {}}
return compute.images().insert(project=project, body=body).execute()