# vim: tabstop=4 shiftwidth=4 softtabstop=4 # # 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 sys import time import yaml from heatclient import client as heat_client from heatclient import exc as heatException from keystoneclient.v2_0 import client as ks_client from oslo_config import cfg from toscaparser.utils import yamlparser from tacker.common import log from tacker.extensions import vnfm from tacker.openstack.common import jsonutils from tacker.openstack.common import log as logging from tacker.vm.drivers import abstract_driver LOG = logging.getLogger(__name__) CONF = cfg.CONF OPTS = [ cfg.StrOpt('heat_uri', default='http://localhost:8004/v1', help=_("Heat server address to create services " "specified in the service chain.")), cfg.IntOpt('stack_retries', default=10, help=_("Number of attempts to retry for stack deletion")), cfg.IntOpt('stack_retry_wait', default=5, help=_("Wait time between two successive stack delete " "retries")), ] CONF.register_opts(OPTS, group='servicevm_heat') STACK_RETRIES = cfg.CONF.servicevm_heat.stack_retries STACK_RETRY_WAIT = cfg.CONF.servicevm_heat.stack_retry_wait HEAT_TEMPLATE_BASE = """ heat_template_version: 2013-05-23 """ class DeviceHeat(abstract_driver.DeviceAbstractDriver): """Heat driver of hosting device.""" def __init__(self): super(DeviceHeat, self).__init__() def get_type(self): return 'heat' def get_name(self): return 'heat' def get_description(self): return 'Heat infra driver' @log.log def create_device_template_pre(self, plugin, context, device_template): device_template_dict = device_template['device_template'] vnfd_yaml = device_template_dict['attributes'].get('vnfd') if vnfd_yaml is None: return vnfd_dict = yaml.load(vnfd_yaml) KEY_LIST = (('name', 'template_name'), ('description', 'description')) device_template_dict.update( dict((key, vnfd_dict[vnfd_key]) for (key, vnfd_key) in KEY_LIST if ((not key in device_template_dict or device_template_dict[key] == '') and vnfd_key in vnfd_dict and vnfd_dict[vnfd_key] != ''))) service_types = vnfd_dict.get('service_properties', {}).get('type', []) if service_types: device_template_dict.setdefault('service_types', []).extend( [{'service_type': service_type} for service_type in service_types]) for vdu in vnfd_dict.get('vdus', {}).values(): mgmt_driver = vdu.get('mgmt_driver') if mgmt_driver: device_template_dict['mgmt_driver'] = mgmt_driver LOG.debug(_('device_template %s'), device_template) @log.log def _update_params(self, original, paramvalues, match=False): for key, value in original.iteritems(): if not isinstance(value, dict) or 'get_input' not in str(value): pass elif isinstance(value, dict): if not match: if key in paramvalues and 'param' in paramvalues[key]: self._update_params(value, paramvalues[key]['param'], True) elif key in paramvalues: self._update_params(value, paramvalues[key], False) else: LOG.debug('Key missing Value: %s', key) raise vnfm.InputValuesMissing() elif 'get_input' in value: if value['get_input'] in paramvalues: original[key] = paramvalues[value['get_input']] else: LOG.debug('Key missing Value: %s', key) raise vnfm.InputValuesMissing() else: self._update_params(value, paramvalues, True) @log.log def _process_parameterized_input(self, dev_attrs, vnfd_dict): param_vattrs_yaml = dev_attrs.pop('param_values', None) if param_vattrs_yaml: try: param_vattrs_dict = yaml.load(param_vattrs_yaml) LOG.debug('param_vattrs_yaml', param_vattrs_dict) except Exception as e: LOG.debug("Not Well Formed: %s", str(e)) raise vnfm.ParamYAMLNotWellFormed( error_msg_details=str(e)) else: self._update_params(vnfd_dict, param_vattrs_dict) else: raise vnfm.ParamYAMLInputMissing() @log.log def _process_vdu_network_interfaces(self, vdu_id, vdu_dict, properties, template_dict): def make_port_dict(): port_dict = { 'type': 'OS::Neutron::Port', 'properties': { 'port_security_enabled': False } } port_dict['properties'].setdefault('fixed_ips', []) return port_dict def make_mgmt_outputs_dict(port): mgmt_ip = 'mgmt_ip-%s' % vdu_id outputs_dict[mgmt_ip] = { 'description': 'management ip address', 'value': { 'get_attr': [port, 'fixed_ips', 0, 'ip_address'] } } def handle_port_creation(network_param, ip_list=[], mgmt_port=False): port = '%s-%s-port' % (vdu_id, network_param['network']) port_dict = make_port_dict() if mgmt_port: make_mgmt_outputs_dict(port) for ip in ip_list: port_dict['properties']['fixed_ips'].append({"ip_address": ip}) port_dict['properties'].update(network_param) template_dict['resources'][port] = port_dict return port networks_list = [] outputs_dict = template_dict['outputs'] properties['networks'] = networks_list for network_param in vdu_dict[ 'network_interfaces'].values(): port = None if 'addresses' in network_param: ip_list = network_param.pop('addresses', []) if not isinstance(ip_list, list): raise vnfm.IPAddrInvalidInput() mgmt_flag = network_param.pop('management', False) port = handle_port_creation(network_param, ip_list, mgmt_flag) if network_param.pop('management', False): port = handle_port_creation(network_param, [], True) if port is not None: network_param = { 'port': {'get_resource': port} } networks_list.append(dict(network_param)) @log.log def create(self, plugin, context, device): LOG.debug(_('device %s'), device) heatclient_ = HeatClient(context) attributes = device['device_template']['attributes'].copy() vnfd_yaml = attributes.pop('vnfd', None) fields = dict((key, attributes.pop(key)) for key in ('stack_name', 'template_url', 'template') if key in attributes) for key in ('files', 'parameters'): if key in attributes: fields[key] = jsonutils.loads(attributes.pop(key)) # overwrite parameters with given dev_attrs for device creation dev_attrs = device['attributes'].copy() fields.update(dict((key, dev_attrs.pop(key)) for key in ('stack_name', 'template_url', 'template') if key in dev_attrs)) for key in ('files', 'parameters'): if key in dev_attrs: fields.setdefault(key, {}).update( jsonutils.loads(dev_attrs.pop(key))) LOG.debug('vnfd_yaml %s', vnfd_yaml) if vnfd_yaml is not None: assert 'template' not in fields assert 'template_url' not in fields template_dict = yaml.load(HEAT_TEMPLATE_BASE) outputs_dict = {} template_dict['outputs'] = outputs_dict vnfd_dict = yamlparser.simple_ordered_parse(vnfd_yaml) LOG.debug('vnfd_dict %s', vnfd_dict) if 'get_input' in vnfd_yaml: self._process_parameterized_input(dev_attrs, vnfd_dict) KEY_LIST = (('description', 'description'), ) for (key, vnfd_key) in KEY_LIST: if vnfd_key in vnfd_dict: template_dict[key] = vnfd_dict[vnfd_key] monitoring_dict = {'vdus': {}} for vdu_id, vdu_dict in vnfd_dict.get('vdus', {}).items(): template_dict.setdefault('resources', {})[vdu_id] = { "type": "OS::Nova::Server" } resource_dict = template_dict['resources'][vdu_id] KEY_LIST = (('image', 'vm_image'), ('flavor', 'instance_type')) resource_dict['properties'] = {} properties = resource_dict['properties'] for (key, vdu_key) in KEY_LIST: properties[key] = vdu_dict[vdu_key] if 'network_interfaces' in vdu_dict: self._process_vdu_network_interfaces(vdu_id, vdu_dict, properties, template_dict) if 'user_data' in vdu_dict and 'user_data_format' in vdu_dict: properties['user_data_format'] = vdu_dict[ 'user_data_format'] properties['user_data'] = vdu_dict['user_data'] elif 'user_data' in vdu_dict or 'user_data_format' in vdu_dict: raise vnfm.UserDataFormatNotFound() if ('placement_policy' in vdu_dict and 'availability_zone' in vdu_dict['placement_policy']): properties['availability_zone'] = vdu_dict[ 'placement_policy']['availability_zone'] if 'config' in vdu_dict: properties['config_drive'] = True metadata = properties.setdefault('metadata', {}) metadata.update(vdu_dict['config']) for key, value in metadata.items(): metadata[key] = value[:255] monitoring_policy = vdu_dict.get('monitoring_policy', 'noop') failure_policy = vdu_dict.get('failure_policy', 'noop') # Convert the old monitoring specification to the new format # This should be removed after Mitaka if monitoring_policy == 'ping' and failure_policy == 'respawn': vdu_dict['monitoring_policy'] = {'ping': { 'actions': { 'failure': 'respawn' }}} vdu_dict.pop('failure_policy') if monitoring_policy != 'noop': monitoring_dict['vdus'][vdu_id] = \ vdu_dict['monitoring_policy'] # to pass necessary parameters to plugin upwards. for key in ('service_type',): if key in vdu_dict: device.setdefault( 'attributes', {})[vdu_id] = jsonutils.dumps( {key: vdu_dict[key]}) if monitoring_dict.keys(): device['attributes']['monitoring_policy'] = jsonutils.dumps( monitoring_dict) heat_template_yaml = yaml.dump(template_dict) fields['template'] = heat_template_yaml if not device['attributes'].get('heat_template'): device['attributes']['heat_template'] = heat_template_yaml if 'stack_name' not in fields: name = (__name__ + '_' + self.__class__.__name__ + '-' + device['id']) if device['attributes'].get('failure_count'): name += ('-%s') % str(device['attributes']['failure_count']) fields['stack_name'] = name # service context is ignored LOG.debug(_('service_context: %s'), device.get('service_context', [])) LOG.debug(_('fields: %s'), fields) LOG.debug(_('template: %s'), fields['template']) stack = heatclient_.create(fields) return stack['stack']['id'] def create_wait(self, plugin, context, device_dict, device_id): heatclient_ = HeatClient(context) stack = heatclient_.get(device_id) status = stack.stack_status stack_retries = STACK_RETRIES while status == 'CREATE_IN_PROGRESS' and stack_retries > 0: time.sleep(STACK_RETRY_WAIT) try: stack = heatclient_.get(device_id) except Exception: LOG.exception(_("Device Instance cleanup may not have " "happened because Heat API request failed " "while waiting for the stack %(stack)s to be " "deleted"), {'stack': device_id}) break 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: LOG.warn(_("Resource creation is" " not completed within %(wait)s seconds as " "creation of Stack %(stack)s is not completed"), {'wait': (STACK_RETRIES * STACK_RETRY_WAIT), 'stack': device_id}) if status != 'CREATE_COMPLETE': raise vnfm.DeviceCreateWaitFailed(device_id=device_id) outputs = stack.outputs LOG.debug(_('outputs %s'), outputs) PREFIX = 'mgmt_ip-' mgmt_ips = dict((output['output_key'][len(PREFIX):], output['output_value']) for output in outputs if output.get('output_key', '').startswith(PREFIX)) if mgmt_ips: device_dict['mgmt_url'] = jsonutils.dumps(mgmt_ips) @log.log def update(self, plugin, context, device_id, device_dict, device): # checking if the stack exists at the moment heatclient_ = HeatClient(context) heatclient_.get(device_id) # update config attribute config_yaml = device_dict.get('attributes', {}).get('config', '') update_yaml = device['device'].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.load() will raise Attribute Error. # So set config_yaml to {}, if it is None. if not config_yaml: config_dict = {} else: config_dict = yaml.load(config_yaml) or {} update_dict = yaml.load(update_yaml) if not update_dict: return @log.log def deep_update(orig_dict, new_dict): for key, value in new_dict.items(): if isinstance(value, dict): if key in orig_dict and isinstance(orig_dict[key], dict): deep_update(orig_dict[key], value) continue orig_dict[key] = value LOG.debug('dict orig %(orig)s update %(update)s', {'orig': config_dict, 'update': update_dict}) deep_update(config_dict, update_dict) LOG.debug('dict new %(new)s update %(update)s', {'new': config_dict, 'update': update_dict}) new_yaml = yaml.dump(config_dict) device_dict.setdefault('attributes', {})['config'] = new_yaml def update_wait(self, plugin, context, device_id): # do nothing but checking if the stack exists at the moment heatclient_ = HeatClient(context) heatclient_.get(device_id) def delete(self, plugin, context, device_id): heatclient_ = HeatClient(context) heatclient_.delete(device_id) @log.log def delete_wait(self, plugin, context, device_id): heatclient_ = HeatClient(context) stack = heatclient_.get(device_id) status = stack.stack_status stack_retries = STACK_RETRIES while (status == 'DELETE_IN_PROGRESS' and stack_retries > 0): time.sleep(STACK_RETRY_WAIT) try: stack = heatclient_.get(device_id) except heatException.HTTPNotFound: return except Exception: LOG.exception(_("Device Instance cleanup may not have " "happened because Heat API request failed " "while waiting for the stack %(stack)s to be " "deleted"), {'stack': device_id}) break status = stack.stack_status stack_retries = stack_retries - 1 if stack_retries == 0: LOG.warn(_("Resource cleanup for device is" " not completed within %(wait)s seconds as " "deletion of Stack %(stack)s is not completed"), {'wait': (STACK_RETRIES * STACK_RETRY_WAIT), 'stack': device_id}) if status != 'DELETE_COMPLETE': LOG.warn(_("device (%(device_id)d) deletion is not completed. " "%(stack_status)s"), {'device_id': device_id, 'stack_status': status}) @log.log def attach_interface(self, plugin, context, device_id, port_id): raise NotImplementedError() @log.log def dettach_interface(self, plugin, context, device_id, port_id): raise NotImplementedError() class HeatClient: def __init__(self, context, password=None): # context, password are unused auth_url = CONF.keystone_authtoken.auth_uri + '/v2.0' authtoken = CONF.keystone_authtoken kc = ks_client.Client( tenant_name=authtoken.project_name, username=authtoken.username, password=authtoken.password, auth_url=auth_url) token = kc.service_catalog.get_token() api_version = "1" endpoint = "%s/%s" % (cfg.CONF.servicevm_heat.heat_uri, token['tenant_id']) kwargs = { 'token': token['id'], 'tenant_name': authtoken.project_name, 'username': authtoken.username, } self.client = heat_client.Client(api_version, endpoint, **kwargs) self.stacks = self.client.stacks def create(self, fields): fields = fields.copy() fields.update({ 'timeout_mins': 10, 'disable_rollback': True}) if 'password' in fields.get('template', {}): fields['password'] = fields['template']['password'] try: return self.stacks.create(**fields) except heatException.HTTPException: type_, value, tb = sys.exc_info() raise vnfm.HeatClientException(msg=value) def delete(self, stack_id): try: self.stacks.delete(stack_id) except heatException.HTTPNotFound: LOG.warn(_("Stack %(stack)s created by service chain driver is " "not found at cleanup"), {'stack': stack_id}) def get(self, stack_id): return self.stacks.get(stack_id)