diff --git a/cinder/requirements-gce.txt b/cinder/requirements-gce.txt index 5dcdd6f..d8055e0 100644 --- a/cinder/requirements-gce.txt +++ b/cinder/requirements-gce.txt @@ -1,2 +1 @@ google-api-python-client -oslo.service>=1.19.0 diff --git a/cinder/volume/drivers/gce/gceutils.py b/cinder/volume/drivers/gce/gceutils.py index 37a19d0..6814757 100644 --- a/cinder/volume/drivers/gce/gceutils.py +++ b/cinder/volume/drivers/gce/gceutils.py @@ -12,12 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. +import time + from googleapiclient.discovery import build from oauth2client.client import GoogleCredentials from oslo_log import log as logging -from cinder.i18n import _LI +from cinder.i18n import _LI, _ from oslo_service import loopingcall +from oslo_utils import reflection LOG = logging.getLogger(__name__) @@ -26,6 +29,46 @@ class GceOperationError(Exception): pass +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 + + 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 diff --git a/glance/gce/gceutils.py b/glance/gce/gceutils.py index abe9a90..dfd406e 100644 --- a/glance/gce/gceutils.py +++ b/glance/gce/gceutils.py @@ -12,8 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six -import time from oslo_log import log as logging from googleapiclient.discovery import build @@ -22,239 +20,6 @@ from oauth2client.client import GoogleCredentials LOG = logging.getLogger(__name__) -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): - """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 - """ - # source_disk_image = "projects/%s/global/images/%s" % ( - # "debian-cloud", "debian-8-jessie-v20170327") - # machine_link = "zones/%s/machineTypes/n1-standard-1" % zone - LOG.info("Launching instance %s with image %s and machine %s" % - (name, image_link, machine_link)) - - config = { - 'kind': - 'compute#instance', - 'name': - name, - 'machineType': - machine_link, - - # Specify the boot disk and the image to use as a source. - 'disks': [{ - 'boot': True, - 'autoDelete': True, - 'initializeParams': { - 'sourceImage': image_link, - } - }], - - # Specify a network interface with NAT to access the public - # internet. - 'networkInterfaces': [{ - 'network': - 'global/networks/default', - 'accessConfigs': [{ - 'type': 'ONE_TO_ONE_NAT', - 'name': 'External NAT' - }] - }], - - # 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' - ] - }], - } - - 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, zone, 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 zone: string, GCE Name of zone - :param operation: object, Operation resource obtained by calling GCE API - :param interval: int, Time period(seconds) between two GCE operation checks - :param timeout: int, Absoulte time period(seconds) to monitor GCE operation - """ - operation_name = operation['name'] - if interval < 1: - raise ValueError("wait_for_operation: Interval should be positive") - iterations = timeout / interval - for i in range(iterations): - result = compute.zoneOperations().get( - project=project, zone=zone, operation=operation_name).execute() - if result['status'] == 'DONE': - LOG.info("Operation %s status is %s" % (operation_name, - result['status'])) - if 'error' in result: - raise Exception(result['error']) - return result - time.sleep(interval) - raise Exception( - "wait_for_operation: Operation %s failed to perform in timeout %s" % - (operation_name, timeout)) - - 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 @@ -265,24 +30,6 @@ def get_gce_service(service_key): 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 diff --git a/neutron/gce/neutron/common/gceutils.py b/neutron/gce/neutron/common/gceutils.py index 2a6385f..c1aa3fe 100644 --- a/neutron/gce/neutron/common/gceutils.py +++ b/neutron/gce/neutron/common/gceutils.py @@ -13,10 +13,13 @@ # under the License. import uuid +import time from googleapiclient.discovery import build +from googleapiclient.errors import HttpError from oauth2client.client import GoogleCredentials from oslo_log import log as logging +from oslo_utils import reflection from neutron_lib import exceptions as e from neutron._i18n import _LI, _ @@ -26,6 +29,46 @@ 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 @@ -333,3 +376,51 @@ def release_floatingip(compute, project, zone, floatingip): accessConfig=accessconfig['name'], networkInterface=interface['name']).execute() wait_for_operation(compute, project, operation) + + +def create_firewall_rule(compute, project, body): + """Create firewall rule in GCE + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + :param body: dict, Information required for creating firewall + Refer format at https://developers.google.com/resources/api-libraries/documentation/compute/beta/python/latest/compute_beta.firewalls.html#insert + :return: Operation information + :rtype: dict + """ + return compute.firewalls().insert(project=project, body=body).execute() + + +def update_firewall_rule(compute, project, name, body): + """Update existing firewall rule in GCE + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + :param name: string, GCE firewall name + :param body: dict, Information required for updating firewall + Refer format at https://developers.google.com/resources/api-libraries/documentation/compute/beta/python/latest/compute_beta.firewalls.html#update + :return: Operation information + :rtype: dict + """ + return compute.firewalls().update(project=project, firewall=name, + body=body).execute() + + +def delete_firewall_rule(compute, project, name): + """Delete firewall rule in GCE + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + :param name: string, GCE firewall name + :return: Operation information + :rtype: dict + """ + return compute.firewalls().delete(project=project, firewall=name).execute() + + +def get_firewall_rule(compute, project, name): + """Get firewall rule info in GCE + :param compute: GCE compute resource object using googleapiclient.discovery + :param project: string, GCE Project Id + :param name: string, GCE firewall name + :return: Firewall info + :rtype: dict + """ + return compute.firewalls().get(project=project, firewall=name).execute() diff --git a/neutron/gce/neutron/plugins/ml2/drivers/gce/mech_gce.py b/neutron/gce/neutron/plugins/ml2/drivers/gce/mech_gce.py index 5a65031..73fb595 100644 --- a/neutron/gce/neutron/plugins/ml2/drivers/gce/mech_gce.py +++ b/neutron/gce/neutron/plugins/ml2/drivers/gce/mech_gce.py @@ -17,14 +17,25 @@ import random from oslo_log import log import ipaddr +from neutron._i18n import _LI, _ +from neutron.callbacks import events +from neutron.callbacks import registry +from neutron.callbacks import resources from neutron.common import gceconf from neutron.common import gceutils -from neutron._i18n import _LI +from neutron.manager import NeutronManager from neutron.plugins.ml2 import driver_api as api +from neutron_lib import exceptions as e +from neutron.extensions import securitygroup as sg LOG = log.getLogger(__name__) +class SecurityGroupInvalidDirection(e.InvalidInput): + message = _("Security group rule for direction '%(direction)s' not " + "supported. Allowed values are %(values)s.") + + class GceMechanismDriver(api.MechanismDriver): """Ml2 Mechanism driver for GCE""" @@ -40,6 +51,18 @@ class GceMechanismDriver(api.MechanismDriver): LOG.info( _LI("GCE Mechanism driver init with %s project, %s region") % (self.gce_project, self.gce_region)) + self._subscribe_events() + + def _subscribe_events(self): + registry.subscribe(self.secgroup_callback, resources.SECURITY_GROUP, + events.BEFORE_DELETE) + registry.subscribe(self.secgroup_callback, resources.SECURITY_GROUP, + events.BEFORE_UPDATE) + + registry.subscribe(self.secgroup_callback, + resources.SECURITY_GROUP_RULE, events.BEFORE_DELETE) + registry.subscribe(self.secgroup_callback, + resources.SECURITY_GROUP_RULE, events.BEFORE_UPDATE) def _gce_network_name(self, context): return 'net-' + context.current[api.ID] @@ -116,12 +139,175 @@ class GceMechanismDriver(api.MechanismDriver): LOG.info( _LI("Deleted subnet %s in region %s on GCE") % (name, region)) + def _gce_secgrp_id(self, openstack_id): + return "secgrp-" + openstack_id + + def _convert_secgrp_rule_to_gce(self, rule, network_link): + if rule['ethertype'] != 'IPv4': + raise sg.SecurityGroupRuleInvalidEtherType( + ethertype=rule['ethertype'], values=('IPv4', )) + + gce_rule = { + 'sourceRanges': [], + 'sourceTags': [], + 'targetTags': [], + 'allowed': [{}], + 'destinationRanges': [], + } + gce_rule['name'] = self._gce_secgrp_id(rule['id']) + gce_rule['network'] = network_link + + directions = { + 'ingress': 'INGRESS', + } + gce_protocols = ('tcp', 'udp', 'icmp', 'esp', 'ah', 'sctp') + + if rule['direction'] in directions: + gce_rule['direction'] = directions[rule['direction']] + else: + raise SecurityGroupInvalidDirection(direction=rule['direction'], + values=directions.keys()) + + protocol = rule['protocol'] + if protocol is None: + gce_rule['allowed'][0]['IPProtocol'] = 'all' + elif protocol in gce_protocols: + gce_rule['allowed'][0]['IPProtocol'] = protocol + # GCE allows port specification for tcp and udp only + if protocol in ('tcp', 'udp'): + ports = [] + port_range_max = rule['port_range_max'] + port_range_min = rule['port_range_min'] + if port_range_max is None or port_range_min is None: + ports.append('0-65535') + elif port_range_max == port_range_min: + ports.append(str(port_range_max)) + else: + ports.append("%s-%s" % (port_range_min, port_range_max)) + gce_rule['allowed'][0]['ports'] = ports + else: + raise sg.SecurityGroupRuleInvalidProtocol(protocol=protocol, + values=gce_protocols) + + if rule['remote_ip_prefix'] is None: + gce_rule['sourceRanges'].append('0.0.0.0/0') + else: + gce_rule['sourceRanges'].append(rule['remote_ip_prefix']) + return gce_rule + + def _create_secgrp_rule(self, context, rule, network_link): + compute, project = self.gce_svc, self.gce_project + try: + gce_rule = self._convert_secgrp_rule_to_gce(rule, network_link) + except Exception as e: + LOG.exception( + "An error occured while creating security group: %s" % e) + return + LOG.info(_LI("Create GCE firewall rule %s") % gce_rule) + operation = gceutils.create_firewall_rule(compute, project, gce_rule) + gceutils.wait_for_operation(compute, project, operation) + + def _update_secgrp_rule(self, context, rule_id): + compute, project = self.gce_svc, self.gce_project + name = self._gce_secgrp_id(rule_id) + try: + gce_firewall_info = gceutils.get_firewall_rule( + compute, project, name) + except gceutils.HttpError: + return + + core_plugin = NeutronManager.get_plugin() + rule = core_plugin.get_security_group_rule(context, rule_id) + + network_link = gce_firewall_info['network'] + try: + gce_rule = self._convert_secgrp_rule_to_gce(rule, network_link) + LOG.info(_LI("Update GCE firewall rule %s") % name) + operation = gceutils.update_firewall_rule(compute, project, name, + gce_rule) + gceutils.wait_for_operation(compute, project, operation) + except Exception as e: + LOG.exception( + _LI("An error occured while updating security group: %s") % e) + LOG.error(_LI("Deleting existing GCE firewall rule %s") % name) + operation = gceutils.delete_firewall_rule(compute, project, name) + gceutils.wait_for_operation(compute, project, operation) + + def _delete_secgrp_rule(self, context, rule_id): + name = self._gce_secgrp_id(rule_id) + compute, project = self.gce_svc, self.gce_project + try: + LOG.warn( + _LI("Delete existing GCE firewall rule %s," + "as firewall rule update not GCE compatible.") % name) + operation = gceutils.delete_firewall_rule(compute, project, name) + gceutils.wait_for_operation(compute, project, operation) + except gceutils.HttpError: + pass + + def _create_secgrp_rules_if_needed(self, context, secgrp_ids): + core_plugin = NeutronManager.get_plugin() + secgrp_rules = [] + for secgrp_id in secgrp_ids: + secgrp = core_plugin.get_security_group(context._plugin_context, + secgrp_id) + secgrp_rules.extend(secgrp['security_group_rules']) + if secgrp_rules: + network_name = self._gce_subnet_network_name(context) + compute, project = self.gce_svc, self.gce_project + network = gceutils.get_network(compute, project, network_name) + network_link = network['selfLink'] + for secgrp_rule in secgrp_rules: + try: + gce_rule_name = self._gce_secgrp_id(secgrp_rule['id']) + gceutils.get_firewall_rule(compute, project, gce_rule_name) + except gceutils.HttpError: + self._create_secgrp_rule(context, secgrp_rule, + network_link) + + def _update_secgrp(self, context, secgrp_id): + core_plugin = NeutronManager.get_plugin() + secgrp = core_plugin.get_security_group(context, secgrp_id) + secgrp_rules = secgrp['security_group_rules'] + for secgrp_rule in secgrp_rules: + self._update_secgrp_rule(context, secgrp_rule['id']) + + def _delete_secgrp(self, context, secgrp_id): + core_plugin = NeutronManager.get_plugin() + secgrp = core_plugin.get_security_group(context, secgrp_id) + secgrp_rules = secgrp['security_group_rules'] + for secgrp_rule in secgrp_rules: + self._delete_secgrp_rule(context, secgrp_rule['id']) + def bind_port(self, context): fixed_ip_dict = dict() if 'fixed_ips' in context.current: - if len(context.current['fixed_ips']) > 0: + if len(context.current['fixed_ips']): fixed_ip_dict = context.current['fixed_ips'][0] + secgrp_ids = context.current['security_groups'] + if secgrp_ids: + self._create_secgrp_rules_if_needed(context, secgrp_ids) + segment_id = random.choice(context.segments_to_bind)[api.ID] context.set_binding(segment_id, "vip_type_a", fixed_ip_dict, status='ACTIVE') return True + + def secgroup_callback(self, resource, event, trigger, **kwargs): + if resource == resources.SECURITY_GROUP_RULE: + context = kwargs['context'] + if event == events.BEFORE_DELETE: + rule_id = kwargs['security_group_rule_id'] + self._delete_secgrp_rule(context, rule_id) + elif event == events.BEFORE_UPDATE: + rule_id = kwargs['security_group_rule_id'] + self._update_secgrp_rule(context, rule_id) + elif resource == resources.SECURITY_GROUP: + if event == events.BEFORE_DELETE: + context = kwargs['context'] + security_group_id = kwargs.get('security_group_id') + if security_group_id: + self._delete_secgrp(context, security_group_id) + else: + LOG.warn( + _LI("Security group ID not found in delete request")) diff --git a/neutron/gce/requirements-gce.txt b/neutron/gce/requirements-gce.txt index 3d53898..7a9eb42 100644 --- a/neutron/gce/requirements-gce.txt +++ b/neutron/gce/requirements-gce.txt @@ -1,3 +1,2 @@ google-api-python-client ipaddr -oslo.service>=1.19.0 diff --git a/nova/gce/driver.py b/nova/gce/driver.py index 16509b1..782ce4a 100644 --- a/nova/gce/driver.py +++ b/nova/gce/driver.py @@ -15,20 +15,23 @@ import hashlib import uuid +import time import nova.conf from nova import exception +from nova.image import glance from nova.i18n import _LI from nova.virt import driver, hardware from oslo_config import cfg from oslo_log import log as logging +from nova.compute import task_states import gceutils +from googleapiclient.errors import HttpError from nova.virt.gce.constants import GCE_STATE_MAP LOG = logging.getLogger(__name__) - gce_group = cfg.OptGroup(name='GCE', title='Options to connect to Google cloud') @@ -227,11 +230,12 @@ class GCEDriver(driver.ComputeDriver): attached to the instance. """ compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone - # TODO: Use instance id as instance name - instance_name = instance.display_name + # GCE expects instance name in format "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?" + # So we need to construct it for GCE from uuid + gce_instance_name = 'inst-' + instance.uuid LOG.info( _LI("Creating instance %s as %s on GCE.") % (instance.display_name, - instance_name)) + gce_instance_name)) # Image Info image_link = instance.system_metadata['image_gce_link'] # Flavor Info @@ -241,11 +245,11 @@ class GCEDriver(driver.ComputeDriver): network_interfaces = self._process_network_info(network_info) # Create Instance operation = gceutils.create_instance(compute, project, zone, - instance_name, image_link, + gce_instance_name, image_link, flavor_link, network_interfaces) - gceutils.wait_for_operation(compute, project, zone, operation) + gceutils.wait_for_operation(compute, project, operation) gce_instance = gceutils.get_instance(compute, project, zone, - instance_name) + gce_instance_name) # Update GCE info in openstack instance metadata instance.metadata.update({'gce_id': gce_instance['name']}) gce_metadata = [ @@ -260,17 +264,161 @@ class GCEDriver(driver.ComputeDriver): operation = gceutils.set_instance_metadata( compute, project, zone, gce_instance['name'], gce_metadata, operation='add') - gceutils.wait_for_operation(compute, project, zone, operation) + gceutils.wait_for_operation(compute, project, operation) self._uuid_to_gce_instance[instance.uuid] = gceutils.get_instance( - compute, project, zone, instance_name) + compute, project, zone, gce_instance_name) def snapshot(self, context, instance, image_id, update_task_state): """Snapshot an image of the specified instance :param context: security context :param instance: nova.objects.instance.Instance :param image_id: Reference to a pre-created image holding the snapshot. + + Steps: + 1. Find boot disk + 2. Stop instance + 3. Create temporary boot disk snapshot + 4. Start instance + 5. Create temporary disk from snapshot + 6. Create image from disk + 7. Add Image info to glance + 8. Delete temporary disk + 9. Delete temporary snapshot """ - raise NotImplementedError() + instance_stopped = False + temp_disk_snapshot = False + temp_disk_from_snapshot = False + image_created = False + + compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone + + try: + gce_id = self._get_gce_id_from_instance(instance) + LOG.info(_LI("Taking snapshot of instance %s") % instance.uuid) + try: + boot_disk = gceutils.get_instance_boot_disk( + compute, project, zone, gce_id) + except AssertionError: + reason = "Unable to find boot disk from instance metadata %s" % instance.uuid + raise exception.InvalidMetadata(reason=reason) + disk_name = boot_disk['name'] + LOG.debug( + _LI("1. Found boot disk %s for instance %s") % (disk_name, + instance.uuid)) + + operation = gceutils.stop_instance(compute, project, zone, gce_id) + gceutils.wait_for_operation(compute, project, operation) + instance_stopped = True + LOG.debug( + _LI("2. Temporarily stopped instance %s") % instance.uuid) + + snapshot_name = 'novasnap-' + disk_name + time.strftime("%s") + operation = gceutils.snapshot_disk( + compute, project, zone, boot_disk['name'], snapshot_name) + gceutils.wait_for_operation(compute, project, operation) + temp_disk_snapshot = True + LOG.debug(_LI("3. Created boot disk snapshot %s") % snapshot_name) + + operation = gceutils.start_instance(compute, project, zone, gce_id) + gceutils.wait_for_operation(compute, project, operation) + instance_stopped = False + LOG.debug( + _LI("4. Restart instance after disk snapshot %s") % + instance.uuid) + + snapshot_disk_name = 'vol-' + snapshot_name + operation = gceutils.create_disk_from_snapshot( + compute, project, zone, snapshot_disk_name, snapshot_name) + gceutils.wait_for_operation(compute, project, operation) + snapshot_disk_info = gceutils.get_disk(compute, project, zone, + snapshot_disk_name) + temp_disk_from_snapshot = True + LOG.debug( + _LI("5. Created disk %s from snapshot %s") % + (snapshot_disk_name, snapshot_name)) + + update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD) + image_api = glance.get_default_image_service() + image_data = image_api.show(context, image_id) + name = image_data['name'] + operation = gceutils.create_image_from_disk( + compute, project, name, snapshot_disk_info['selfLink']) + gceutils.wait_for_operation(compute, project, operation, + timeout=120) + image_created = True + LOG.debug( + _LI("6. Created image %s from disk %s") % (name, + snapshot_disk_name)) + LOG.info( + _LI("Created GCE image %s from instance %s") % (name, + instance.uuid)) + + update_task_state(task_state=task_states.IMAGE_UPLOADING, + expected_state=task_states.IMAGE_PENDING_UPLOAD) + gce_img_data = gceutils.get_image(compute, project, name) + image_metadata = { + 'name': name, + 'container_format': 'bare', + 'disk_format': 'raw', + 'is_public': False, + 'status': 'active', + 'properties': { + 'image_state': 'available', + 'owner_id': instance.project_id, + 'ramdisk_id': instance.ramdisk_id, + 'location': 'gce://%s/%s/%s' % (project, name, image_id), + 'gce_image_id': gce_img_data['id'], + 'gce_link': gce_img_data['selfLink'], + 'gce_size': gce_img_data['diskSizeGb'] + }, + } + image_api.update(context, image_id, image_metadata) + LOG.debug(_LI("7. Added image to glance %s") % name) + + disk_operation = gceutils.delete_disk(compute, project, zone, + snapshot_disk_name) + snap_operation = gceutils.delete_snapshot(compute, project, + snapshot_name) + gceutils.wait_for_operation(compute, project, disk_operation) + temp_disk_from_snapshot = False + LOG.debug(_LI("8. Delete temporary disk %s") % snapshot_disk_name) + + gceutils.wait_for_operation(compute, project, snap_operation) + temp_disk_snapshot = False + LOG.debug( + _LI("9. Delete temporary disk snapshot %s") % snapshot_name) + LOG.info(_LI("Completed snapshot for instance %s") % instance.uuid) + + except Exception as e: + LOG.exception("An error occured during image creation: %s" % e) + if instance_stopped: + operation = gceutils.start_instance(compute, project, zone, + gce_id) + gceutils.wait_for_operation(compute, project, operation) + LOG.debug( + _LI("Restart instance after disk snapshot %s") % + instance.uuid) + if image_created: + LOG.info( + _LI("Rollback snapshot for instance %s, deleting image %s from GCE" + ) % (instance.uuid, name)) + operation = gceutils.delete_image(compute, project, name) + gceutils.wait_for_operation(compute, project, operation) + if temp_disk_from_snapshot: + disk_operation = gceutils.delete_disk(compute, project, zone, + snapshot_disk_name) + gceutils.wait_for_operation(compute, project, disk_operation) + LOG.debug( + _LI("Rollback snapshot for instace %s, delete temporary disk %s" + ) % (instance.uuid, snapshot_disk_name)) + if temp_disk_snapshot: + snap_operation = gceutils.delete_snapshot( + compute, project, snapshot_name) + gceutils.wait_for_operation(compute, project, snap_operation) + LOG.debug( + _LI("Rollback snapshot for instance %s, delete temporary disk snapshot %s" + ) % (instance.uuid, snapshot_name)) + raise e def reboot(self, context, instance, network_info, reboot_type, block_device_info=None, bad_volumes_callback=None): @@ -303,10 +451,10 @@ class GCEDriver(driver.ComputeDriver): gce_id = self._get_gce_id_from_instance(instance) LOG.info(_LI('Stopping instance %s') % instance.uuid) operation = gceutils.stop_instance(compute, project, zone, gce_id) - gceutils.wait_for_operation(compute, project, zone, operation) + gceutils.wait_for_operation(compute, project, operation) LOG.info(_LI('Starting instance %s') % instance.uuid) operation = gceutils.start_instance(compute, project, zone, gce_id) - gceutils.wait_for_operation(compute, project, zone, operation) + gceutils.wait_for_operation(compute, project, operation) LOG.info(_LI('Soft Reboot Complete for instance %s') % instance.uuid) def _hard_reboot(self, context, instance, network_info, @@ -315,7 +463,7 @@ class GCEDriver(driver.ComputeDriver): gce_id = self._get_gce_id_from_instance(instance) LOG.info(_LI('Resetting instance %s') % instance.uuid) operation = gceutils.reset_instance(compute, project, zone, gce_id) - gceutils.wait_for_operation(compute, project, zone, operation) + gceutils.wait_for_operation(compute, project, operation) LOG.info(_LI('Hard Reboot Complete %s') % instance.uuid) @staticmethod @@ -370,7 +518,7 @@ class GCEDriver(driver.ComputeDriver): gce_id = self._get_gce_id_from_instance(instance) LOG.info(_LI('Stopping instance %s') % instance.uuid) operation = gceutils.stop_instance(compute, project, zone, gce_id) - gceutils.wait_for_operation(compute, project, zone, operation) + gceutils.wait_for_operation(compute, project, operation) LOG.info(_LI('Power off complete %s') % instance.uuid) def power_on(self, context, instance, network_info, block_device_info): @@ -379,7 +527,7 @@ class GCEDriver(driver.ComputeDriver): gce_id = self._get_gce_id_from_instance(instance) LOG.info(_LI('Starting instance %s') % instance.uuid) operation = gceutils.start_instance(compute, project, zone, gce_id) - gceutils.wait_for_operation(compute, project, zone, operation) + gceutils.wait_for_operation(compute, project, operation) LOG.info(_LI("Power on Complete %s") % instance.uuid) def soft_delete(self, instance): @@ -449,10 +597,25 @@ class GCEDriver(driver.ComputeDriver): :param migrate_data: implementation specific params """ compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone - gce_id = self._get_gce_id_from_instance(instance) LOG.info(_LI('Deleting instance %s') % instance.uuid) - operation = gceutils.delete_instance(compute, project, zone, gce_id) - gceutils.wait_for_operation(compute, project, zone, operation) + try: + gce_id = self._get_gce_id_from_instance(instance) + except exception.InstanceNotFound: + LOG.error( + _LI("Unable to find GCE mapping for instance %s") % + instance.uuid) + return + try: + operation = gceutils.delete_instance(compute, project, zone, + gce_id) + except HttpError: + # Sometimes instance may not exist in GCE, in that case we just + # allow deleting VM from openstack + LOG.error( + _LI("Instance %s not found in GCE, removing from openstack.") % + instance.uuid) + return + gceutils.wait_for_operation(compute, project, operation) LOG.info(_LI("Destroy Complete %s") % instance.uuid) def attach_volume(self, context, connection_info, instance, mountpoint, @@ -466,7 +629,7 @@ class GCEDriver(driver.ComputeDriver): disk_link = gce_volume['selfLink'] operation = gceutils.attach_disk(compute, project, zone, gce_id, disk_name, disk_link) - gceutils.wait_for_operation(compute, project, zone, operation) + gceutils.wait_for_operation(compute, project, operation) LOG.info( _LI("Volume %s attached to instace %s") % (disk_name, instance.uuid)) @@ -481,7 +644,7 @@ class GCEDriver(driver.ComputeDriver): disk_name = gce_volume['name'] operation = gceutils.detach_disk(compute, project, zone, gce_id, disk_name) - gceutils.wait_for_operation(compute, project, zone, operation) + gceutils.wait_for_operation(compute, project, operation) LOG.info( _LI("Volume %s detached from instace %s") % (disk_name, instance.uuid)) diff --git a/nova/gce/gceutils.py b/nova/gce/gceutils.py index 0f29cf1..1812163 100644 --- a/nova/gce/gceutils.py +++ b/nova/gce/gceutils.py @@ -12,17 +12,64 @@ # License for the specific language governing permissions and limitations # under the License. -import six import time +import six from oslo_log import log as logging -from nova.i18n import _LI +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 @@ -210,33 +257,42 @@ def reset_instance(compute, project, zone, name): instance=name).execute() -def wait_for_operation(compute, project, zone, operation, interval=1, - timeout=60): +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 zone: string, GCE Name of zone - :param operation: object, Operation resource obtained by calling GCE API + :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 """ - operation_name = operation['name'] - if interval < 1: - raise ValueError("wait_for_operation: Interval should be positive") - iterations = timeout / interval - for i in range(iterations): - result = compute.zoneOperations().get( - project=project, zone=zone, operation=operation_name).execute() + + def watch_operation(name, request): + result = request.execute() if result['status'] == 'DONE': - LOG.info("Operation %s status is %s" % (operation_name, - result['status'])) + LOG.info( + _LI("Operation %s status is %s") % (name, result['status'])) if 'error' in result: - raise Exception(result['error']) - return result - time.sleep(interval) - raise Exception( - "wait_for_operation: Operation %s failed to perform in timeout %s" % - (operation_name, timeout)) + 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): @@ -289,6 +345,18 @@ def get_image(compute, project, name): 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 @@ -337,3 +405,134 @@ def detach_disk(compute, project, zone, instance_name, disk_name): 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//zones//disks/ + 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()