# Copyright 2015 Intel Corporation. # All Rights Reserved. # # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import time from heatclient import exc as heatException from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import jsonutils import yaml from tacker.common import log from tacker.common import utils from tacker.extensions import vnfm from tacker.vnfm.infra_drivers import abstract_driver from tacker.vnfm.infra_drivers.openstack import heat_client as hc from tacker.vnfm.infra_drivers.openstack import translate_template from tacker.vnfm.infra_drivers import scale_driver LOG = logging.getLogger(__name__) CONF = cfg.CONF OPTS = [ cfg.IntOpt('stack_retries', default=60, help=_("Number of attempts to retry for stack" " creation/deletion")), cfg.IntOpt('stack_retry_wait', default=10, help=_("Wait time (in seconds) between consecutive stack" " create/delete retries")), ] CONF.register_opts(OPTS, group='openstack_vim') def config_opts(): return [('openstack_vim', OPTS)] # Global map of individual resource type and # incompatible properties, alternate properties pair for # upgrade/downgrade across all Heat template versions (starting Kilo) # # Maintains a dictionary of {"resource type": {dict of "incompatible # property": "alternate_prop"}} HEAT_VERSION_INCOMPATIBILITY_MAP = {'OS::Neutron::Port': { 'port_security_enabled': 'value_specs', }, } HEAT_TEMPLATE_BASE = """ heat_template_version: 2013-05-23 """ OUTPUT_PREFIX = 'mgmt_ip-' ALARMING_POLICY = 'tosca.policies.tacker.Alarming' SCALING_POLICY = 'tosca.policies.tacker.Scaling' def get_scaling_policy_name(action, policy_name): return '%s_scale_%s' % (policy_name, action) class OpenStack(abstract_driver.DeviceAbstractDriver, scale_driver.VnfScaleAbstractDriver): """Openstack infra driver for hosting vnfs""" def __init__(self): super(OpenStack, self).__init__() self.STACK_RETRIES = cfg.CONF.openstack_vim.stack_retries self.STACK_RETRY_WAIT = cfg.CONF.openstack_vim.stack_retry_wait def get_type(self): return 'openstack' def get_name(self): return 'openstack' def get_description(self): return 'Openstack infra driver' @log.log def create(self, plugin, context, vnf, auth_attr): LOG.debug('vnf %s', vnf) region_name = vnf.get('placement_attr', {}).get('region_name', None) heatclient = hc.HeatClient(auth_attr, region_name) tth = translate_template.TOSCAToHOT(vnf, heatclient) tth.generate_hot() stack = self._create_stack(heatclient, tth.vnf, tth.fields) return stack['stack']['id'] @log.log def _create_stack(self, heatclient, vnf, fields): if 'stack_name' not in fields: name = vnf['name'].replace(' ', '_') + '_' + vnf['id'] if vnf['attributes'].get('failure_count'): name += ('-RESPAWN-%s') % str(vnf['attributes'][ 'failure_count']) fields['stack_name'] = name # service context is ignored LOG.debug('service_context: %s', vnf.get('service_context', [])) LOG.debug('fields: %s', fields) LOG.debug('template: %s', fields['template']) stack = heatclient.create(fields) return stack @log.log def create_wait(self, plugin, context, vnf_dict, vnf_id, auth_attr): region_name = vnf_dict.get('placement_attr', {}).get( 'region_name', None) heatclient = hc.HeatClient(auth_attr, region_name) stack = heatclient.get(vnf_id) status = stack.stack_status stack_retries = self.STACK_RETRIES error_reason = None while status == 'CREATE_IN_PROGRESS' and stack_retries > 0: time.sleep(self.STACK_RETRY_WAIT) try: stack = heatclient.get(vnf_id) except Exception: LOG.warning("VNF Instance setup may not have " "happened because Heat API request failed " "while waiting for the stack %(stack)s to be " "created", {'stack': vnf_id}) # continue to avoid temporary connection error to target # VIM status = stack.stack_status LOG.debug('status: %s', status) stack_retries = stack_retries - 1 LOG.debug('stack status: %(stack)s %(status)s', {'stack': str(stack), 'status': status}) if stack_retries == 0 and status != 'CREATE_COMPLETE': error_reason = _("Resource creation is not completed within" " {wait} seconds as creation of stack {stack}" " is not completed").format( wait=(self.STACK_RETRIES * self.STACK_RETRY_WAIT), stack=vnf_id) LOG.warning("VNF Creation failed: %(reason)s", {'reason': error_reason}) raise vnfm.VNFCreateWaitFailed(reason=error_reason) elif stack_retries != 0 and status != 'CREATE_COMPLETE': error_reason = stack.stack_status_reason raise vnfm.VNFCreateWaitFailed(reason=error_reason) def _find_mgmt_ips(outputs): LOG.debug('outputs %s', outputs) mgmt_ips = dict((output['output_key'][len(OUTPUT_PREFIX):], output['output_value']) for output in outputs if output.get('output_key', '').startswith(OUTPUT_PREFIX)) return mgmt_ips # scaling enabled if vnf_dict['attributes'].get('scaling_group_names'): group_names = jsonutils.loads( vnf_dict['attributes'].get('scaling_group_names')).values() mgmt_ips = self._find_mgmt_ips_from_groups(heatclient, vnf_id, group_names) else: mgmt_ips = _find_mgmt_ips(stack.outputs) if mgmt_ips: vnf_dict['mgmt_url'] = jsonutils.dumps(mgmt_ips) @log.log def update(self, plugin, context, vnf_id, vnf_dict, vnf, auth_attr): region_name = vnf_dict.get('placement_attr', {}).get( 'region_name', None) heatclient = hc.HeatClient(auth_attr, region_name) heatclient.get(vnf_id) # update config attribute config_yaml = vnf_dict.get('attributes', {}).get('config', '') update_yaml = vnf['vnf'].get('attributes', {}).get('config', '') LOG.debug('yaml orig %(orig)s update %(update)s', {'orig': config_yaml, 'update': update_yaml}) # If config_yaml is None, yaml.safe_load() will raise Attribute Error. # So set config_yaml to {}, if it is None. if not config_yaml: config_dict = {} else: config_dict = yaml.safe_load(config_yaml) or {} update_dict = yaml.safe_load(update_yaml) if not update_dict: return LOG.debug('dict orig %(orig)s update %(update)s', {'orig': config_dict, 'update': update_dict}) utils.deep_update(config_dict, update_dict) LOG.debug('dict new %(new)s update %(update)s', {'new': config_dict, 'update': update_dict}) new_yaml = yaml.safe_dump(config_dict) vnf_dict.setdefault('attributes', {})['config'] = new_yaml @log.log def update_wait(self, plugin, context, vnf_id, auth_attr, region_name=None): # do nothing but checking if the stack exists at the moment heatclient = hc.HeatClient(auth_attr, region_name) heatclient.get(vnf_id) @log.log def delete(self, plugin, context, vnf_id, auth_attr, region_name=None): heatclient = hc.HeatClient(auth_attr, region_name) heatclient.delete(vnf_id) @log.log def delete_wait(self, plugin, context, vnf_id, auth_attr, region_name=None): heatclient = hc.HeatClient(auth_attr, region_name) stack = heatclient.get(vnf_id) status = stack.stack_status error_reason = None stack_retries = self.STACK_RETRIES while (status == 'DELETE_IN_PROGRESS' and stack_retries > 0): time.sleep(self.STACK_RETRY_WAIT) try: stack = heatclient.get(vnf_id) except heatException.HTTPNotFound: return except Exception: LOG.warning("VNF Instance cleanup may not have " "happened because Heat API request failed " "while waiting for the stack %(stack)s to be " "deleted", {'stack': vnf_id}) # Just like create wait, ignore the exception to # avoid temporary connection error. status = stack.stack_status stack_retries = stack_retries - 1 if stack_retries == 0 and status != 'DELETE_COMPLETE': error_reason = _("Resource cleanup for vnf is" " not completed within {wait} seconds as " "deletion of Stack {stack} is " "not completed").format(stack=vnf_id, wait=(self.STACK_RETRIES * self.STACK_RETRY_WAIT)) LOG.warning(error_reason) raise vnfm.VNFDeleteWaitFailed(reason=error_reason) if stack_retries != 0 and status != 'DELETE_COMPLETE': error_reason = _("VNF {vnf_id} deletion is not completed. " "{stack_status}").format(vnf_id=vnf_id, stack_status=status) LOG.warning(error_reason) raise vnfm.VNFDeleteWaitFailed(reason=error_reason) @classmethod def _find_mgmt_ips_from_groups(cls, heat_client, instance_id, group_names): def _find_mgmt_ips(attributes): mgmt_ips = {} for k, v in attributes.items(): if k.startswith(OUTPUT_PREFIX): mgmt_ips[k.replace(OUTPUT_PREFIX, '')] = v return mgmt_ips mgmt_ips = {} for group_name in group_names: # Get scale group grp = heat_client.resource_get(instance_id, group_name) for rsc in heat_client.resource_get_list(grp.physical_resource_id): # Get list of resources in scale group scale_rsc = heat_client.resource_get(grp.physical_resource_id, rsc.resource_name) # findout the mgmt ips from attributes for k, v in _find_mgmt_ips(scale_rsc.attributes).items(): if k not in mgmt_ips: mgmt_ips[k] = [v] else: mgmt_ips[k].append(v) return mgmt_ips @log.log def scale(self, context, plugin, auth_attr, policy, region_name): heatclient = hc.HeatClient(auth_attr, region_name) policy_rsc = get_scaling_policy_name(policy_name=policy['name'], action=policy['action']) events = heatclient.resource_event_list(policy['instance_id'], policy_rsc, limit=1, sort_dir='desc', sort_keys='event_time') heatclient.resource_signal(policy['instance_id'], policy_rsc) return events[0].id @log.log def scale_wait(self, context, plugin, auth_attr, policy, region_name, last_event_id): heatclient = hc.HeatClient(auth_attr, region_name) # TODO(kanagaraj-manickam) make wait logic into separate utility method # and make use of it here and other actions like create and delete stack_retries = self.STACK_RETRIES while (True): try: time.sleep(self.STACK_RETRY_WAIT) stack_id = policy['instance_id'] policy_name = get_scaling_policy_name( policy_name=policy['name'], action=policy['action']) events = heatclient.resource_event_list(stack_id, policy_name, limit=1, sort_dir='desc', sort_keys='event_time') if events[0].id != last_event_id: if events[0].resource_status == 'SIGNAL_COMPLETE': break except Exception as e: error_reason = _("VNF scaling failed for stack %(stack)s with " "error %(error)s") % { 'stack': policy['instance_id'], 'error': str(e)} LOG.warning(error_reason) raise vnfm.VNFScaleWaitFailed(vnf_id=policy['vnf']['id'], reason=error_reason) if stack_retries == 0: metadata = heatclient.resource_metadata(stack_id, policy_name) if not metadata['scaling_in_progress']: error_reason = _('When signal occurred within cool down ' 'window, no events generated from heat, ' 'so ignore it') LOG.warning(error_reason) break error_reason = _( "VNF scaling failed to complete within %{wait}s seconds " "while waiting for the stack %(stack)s to be " "scaled.") % {'stack': stack_id, 'wait': self.STACK_RETRIES * self.STACK_RETRY_WAIT} LOG.warning(error_reason) raise vnfm.VNFScaleWaitFailed(vnf_id=policy['vnf']['id'], reason=error_reason) stack_retries -= 1 def _fill_scaling_group_name(): vnf = policy['vnf'] scaling_group_names = vnf['attributes']['scaling_group_names'] policy['group_name'] = jsonutils.loads( scaling_group_names)[policy['name']] _fill_scaling_group_name() mgmt_ips = self._find_mgmt_ips_from_groups(heatclient, policy['instance_id'], [policy['group_name']]) return jsonutils.dumps(mgmt_ips) @log.log def get_resource_info(self, plugin, context, vnf_info, auth_attr, region_name=None): instance_id = vnf_info['instance_id'] heatclient = hc.HeatClient(auth_attr, region_name) try: # nested_depth=2 is used to get VDU resources # in case of nested template resources_ids =\ heatclient.resource_get_list(instance_id, nested_depth=2) details_dict = {resource.resource_name: {"id": resource.physical_resource_id, "type": resource.resource_type} for resource in resources_ids} return details_dict # Raise exception when Heat API service is not available except Exception: raise vnfm.InfraDriverUnreachable(service="Heat API service")