From 40e6f7b0ee387144591775974de3f6168d870ec0 Mon Sep 17 00:00:00 2001 From: Sanket Date: Mon, 8 May 2017 11:12:28 +0530 Subject: [PATCH] Added Nova Instace Snapshot Operation and security group rules support 1. Added instance snapshot feature 2. Support for security group rules using GCE firewall rules 2. Added disk, snapshot related operations nova gceutils library 3. Cleanup unwanted operations from glance gceutils library Change-Id: I1777e99cf4e1f7c230c4a9b286e4c01e8deb3d2f Signed-off-by: Sanket --- cinder/requirements-gce.txt | 1 - cinder/volume/drivers/gce/gceutils.py | 45 +++- glance/gce/gceutils.py | 253 ------------------ neutron/gce/neutron/common/gceutils.py | 91 +++++++ .../plugins/ml2/drivers/gce/mech_gce.py | 190 ++++++++++++- neutron/gce/requirements-gce.txt | 1 - nova/gce/driver.py | 203 ++++++++++++-- nova/gce/gceutils.py | 241 +++++++++++++++-- 8 files changed, 726 insertions(+), 299 deletions(-) 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()