diff --git a/requirements.txt b/requirements.txt index 151d9af4d..44604de85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,3 +41,4 @@ tosca-parser>=0.7.0 # Apache-2.0 heat-translator>=0.4.0 # Apache-2.0 cryptography!=1.3.0,>=1.0 # BSD/Apache-2.0 paramiko>=2.0 # LGPLv2.1+ +python-mistralclient>=2.0.0 # Apache-2.0 diff --git a/tacker/db/migration/alembic_migrations/versions/0ad3bbce1c18_create_of_network_service_tables.py b/tacker/db/migration/alembic_migrations/versions/0ad3bbce1c18_create_of_network_service_tables.py new file mode 100644 index 000000000..f8e80abb8 --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/0ad3bbce1c18_create_of_network_service_tables.py @@ -0,0 +1,73 @@ +# Copyright 2016 OpenStack Foundation +# +# 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. +# + +"""create of Network service tables + +Revision ID: 0ad3bbce1c18 +Revises: 0ae5b1ce3024 +Create Date: 2016-12-17 19:41:01.906138 + +""" + +# revision identifiers, used by Alembic. +revision = '0ad3bbce1c18' +down_revision = '8f7145914cb0' + +from alembic import op +import sqlalchemy as sa + +from tacker.db import types + + +def upgrade(active_plugins=None, options=None): + op.create_table('nsd', + sa.Column('tenant_id', sa.String(length=64), nullable=False), + sa.Column('id', types.Uuid(length=36), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('vnfds', types.Json, nullable=True), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + op.create_table('ns', + sa.Column('tenant_id', sa.String(length=64), nullable=False), + sa.Column('id', types.Uuid(length=36), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('nsd_id', types.Uuid(length=36), nullable=True), + sa.Column('vim_id', sa.String(length=64), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('vnf_ids', sa.TEXT(length=65535), nullable=True), + sa.Column('mgmt_urls', sa.TEXT(length=65535), nullable=True), + sa.Column('status', sa.String(length=64), nullable=False), + sa.Column('error_reason', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['nsd_id'], ['nsd.id'], ), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + op.create_table('nsd_attribute', + sa.Column('id', types.Uuid(length=36), nullable=False), + sa.Column('nsd_id', types.Uuid(length=36), nullable=False), + sa.Column('key', sa.String(length=255), nullable=False), + sa.Column('value', sa.TEXT(length=65535), nullable=True), + sa.ForeignKeyConstraint(['nsd_id'], ['nsd.id'], ), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index aae89762b..1482302b3 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -8f7145914cb0 \ No newline at end of file +0ad3bbce1c18 diff --git a/tacker/db/migration/models/head.py b/tacker/db/migration/models/head.py index 27f4e1fd6..755f73dea 100644 --- a/tacker/db/migration/models/head.py +++ b/tacker/db/migration/models/head.py @@ -23,6 +23,7 @@ Based on this comparison database can be healed with healing migration. from tacker.db import model_base from tacker.db.nfvo import nfvo_db # noqa +from tacker.db.nfvo import ns_db # noqa from tacker.db.nfvo import vnffg_db # noqa from tacker.db.vnfm import vnfm_db # noqa diff --git a/tacker/db/nfvo/ns_db.py b/tacker/db/nfvo/ns_db.py new file mode 100644 index 000000000..20feffe74 --- /dev/null +++ b/tacker/db/nfvo/ns_db.py @@ -0,0 +1,192 @@ +# 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 uuid + +from oslo_log import log as logging +from oslo_utils import timeutils + +import sqlalchemy as sa +from sqlalchemy import orm + +from tacker.db.common_services import common_services_db +from tacker.db import db_base +from tacker.db import model_base +from tacker.db import models_v1 +from tacker.db import types +from tacker.extensions.nfvo_plugins import network_service +from tacker.plugins.common import constants + +LOG = logging.getLogger(__name__) +_ACTIVE_UPDATE = (constants.ACTIVE, constants.PENDING_UPDATE) +_ACTIVE_UPDATE_ERROR_DEAD = ( + constants.PENDING_CREATE, constants.ACTIVE, constants.PENDING_UPDATE, + constants.ERROR, constants.DEAD) +CREATE_STATES = (constants.PENDING_CREATE, constants.DEAD) + + +########################################################################### +# db tables + +class NSD(model_base.BASE, models_v1.HasId, models_v1.HasTenant, + models_v1.Audit): + """Represents NSD to create NS.""" + + __tablename__ = 'nsd' + # Descriptive name + name = sa.Column(sa.String(255), nullable=False) + description = sa.Column(sa.Text) + vnfds = sa.Column(types.Json, nullable=True) + + # (key, value) pair to spin up + attributes = orm.relationship('NSDAttribute', + backref='nsd') + + +class NSDAttribute(model_base.BASE, models_v1.HasId): + """Represents attributes necessary for creation of ns in (key, value) pair + + """ + + __tablename__ = 'nsd_attribute' + nsd_id = sa.Column(types.Uuid, sa.ForeignKey('nsd.id'), + nullable=False) + key = sa.Column(sa.String(255), nullable=False) + value = sa.Column(sa.TEXT(65535), nullable=True) + + +class NS(model_base.BASE, models_v1.HasId, models_v1.HasTenant, + models_v1.Audit): + """Represents network services that deploys services. + + """ + + __tablename__ = 'ns' + nsd_id = sa.Column(types.Uuid, sa.ForeignKey('nsd.id')) + nsd = orm.relationship('NSD') + + name = sa.Column(sa.String(255), nullable=False) + description = sa.Column(sa.Text, nullable=True) + + # Dict of VNF details that network service launches + vnf_ids = sa.Column(sa.TEXT(65535), nullable=True) + + # Dict of mgmt urls that network servic launches + mgmt_urls = sa.Column(sa.TEXT(65535), nullable=True) + + status = sa.Column(sa.String(64), nullable=False) + vim_id = sa.Column(types.Uuid, sa.ForeignKey('vims.id'), nullable=False) + error_reason = sa.Column(sa.Text, nullable=True) + + +class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): + + def __init__(self): + super(NSPluginDb, self).__init__() + self._cos_db_plg = common_services_db.CommonServicesPluginDb() + + def _make_attributes_dict(self, attributes_db): + return dict((attr.key, attr.value) for attr in attributes_db) + + def _make_nsd_dict(self, nsd, fields=None): + res = { + 'attributes': self._make_attributes_dict(nsd['attributes']), + } + key_list = ('id', 'tenant_id', 'name', 'description', + 'created_at', 'updated_at', 'vnfds') + res.update((key, nsd[key]) for key in key_list) + return self._fields(res, fields) + + def create_nsd(self, context, nsd): + vnfds = nsd['vnfds'] + nsd = nsd['nsd'] + LOG.debug(_('nsd %s'), nsd) + tenant_id = self._get_tenant_id_for_create(context, nsd) + + with context.session.begin(subtransactions=True): + nsd_id = str(uuid.uuid4()) + nsd_db = NSD( + id=nsd_id, + tenant_id=tenant_id, + name=nsd.get('name'), + vnfds=vnfds, + description=nsd.get('description')) + context.session.add(nsd_db) + for (key, value) in nsd.get('attributes', {}).items(): + attribute_db = NSDAttribute( + id=str(uuid.uuid4()), + nsd_id=nsd_id, + key=key, + value=value) + context.session.add(attribute_db) + + LOG.debug(_('nsd_db %(nsd_db)s %(attributes)s '), + {'nsd_db': nsd_db, + 'attributes': nsd_db.attributes}) + nsd_dict = self._make_nsd_dict(nsd_db) + LOG.debug(_('nsd_dict %s'), nsd_dict) + self._cos_db_plg.create_event( + context, res_id=nsd_dict['id'], + res_type=constants.RES_TYPE_NSD, + res_state=constants.RES_EVT_ONBOARDED, + evt_type=constants.RES_EVT_CREATE, + tstamp=nsd_dict[constants.RES_EVT_CREATED_FLD]) + return nsd_dict + + def delete_nsd(self, + context, + nsd_id, + soft_delete=True): + with context.session.begin(subtransactions=True): + nss_db = context.session.query(NS).filter_by( + nsd_id=nsd_id).first() + if nss_db is not None and nss_db.deleted_at is None: + raise network_service.NSDInUse( + nsd_id=nsd_id) + + nsd_db = self._get_resource(context, NSD, + nsd_id) + if soft_delete: + nsd_db.update({'deleted_at': timeutils.utcnow()}) + self._cos_db_plg.create_event( + context, res_id=nsd_db['id'], + res_type=constants.RES_TYPE_NSD, + res_state=constants.RES_EVT_NA_STATE, + evt_type=constants.RES_EVT_DELETE, + tstamp=nsd_db[constants.RES_EVT_DELETED_FLD]) + else: + context.session.query(NSDAttribute).filter_by( + nsd_id=nsd_id).delete() + context.session.delete(nsd_db) + + def get_nsd(self, context, nsd_id, fields=None): + nsd_db = self._get_resource(context, NSD, nsd_id) + return self._make_nsd_dict(nsd_db) + + def get_nsds(self, context, filters, fields=None): + return self._get_collection(context, NSD, + self._make_nsd_dict, + filters=filters, fields=fields) + + # reference implementation. needs to be overrided by subclass + def create_ns(self, context, ns): + return {'nsd': {}} + + # reference implementation. needs to be overrided by subclass + def delete_ns(self, context, ns_id, soft_delete=True): + pass + + def get_ns(self, context, ns_id, fields=None): + pass + + def get_nss(self, context, filters=None, fields=None): + pass diff --git a/tacker/db/vnfm/vnfm_db.py b/tacker/db/vnfm/vnfm_db.py index 85b1d6d36..d47409a72 100644 --- a/tacker/db/vnfm/vnfm_db.py +++ b/tacker/db/vnfm/vnfm_db.py @@ -18,6 +18,7 @@ import uuid from oslo_log import log as logging from oslo_utils import timeutils +from oslo_utils import uuidutils import sqlalchemy as sa from sqlalchemy import orm @@ -156,7 +157,9 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin): def _get_resource(self, context, model, id): try: - return self._get_by_id(context, model, id) + if uuidutils.is_uuid_like(id): + return self._get_by_id(context, model, id) + return self._get_by_name(context, model, id) except orm_exc.NoResultFound: if issubclass(model, VNFD): raise vnfm.VNFDNotFound(vnfd_id=id) @@ -254,7 +257,7 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin): self._cos_db_plg.create_event( context, res_id=vnfd_dict['id'], res_type=constants.RES_TYPE_VNFD, - res_state=constants.RES_EVT_VNFD_ONBOARDED, + res_state=constants.RES_EVT_ONBOARDED, evt_type=constants.RES_EVT_CREATE, tstamp=vnfd_dict[constants.RES_EVT_CREATED_FLD]) return vnfd_dict @@ -270,7 +273,7 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin): self._cos_db_plg.create_event( context, res_id=vnfd_dict['id'], res_type=constants.RES_TYPE_VNFD, - res_state=constants.RES_EVT_VNFD_NA_STATE, + res_state=constants.RES_EVT_NA_STATE, evt_type=constants.RES_EVT_UPDATE, tstamp=vnfd_dict[constants.RES_EVT_UPDATED_FLD]) return vnfd_dict @@ -295,7 +298,7 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin): self._cos_db_plg.create_event( context, res_id=vnfd_db['id'], res_type=constants.RES_TYPE_VNFD, - res_state=constants.RES_EVT_VNFD_NA_STATE, + res_state=constants.RES_EVT_NA_STATE, evt_type=constants.RES_EVT_DELETE, tstamp=vnfd_db[constants.RES_EVT_DELETED_FLD]) else: diff --git a/tacker/extensions/nfvo.py b/tacker/extensions/nfvo.py index a3d9d1ee3..a16d03488 100644 --- a/tacker/extensions/nfvo.py +++ b/tacker/extensions/nfvo.py @@ -209,6 +209,9 @@ class ClassifierNotFoundException(exceptions.NotFound): message = _('Classifier %(classifier_id)s could not be found') +class NSDInUse(exceptions.InUse): + message = _('NSD %(nsd_id)s is still in use') + RESOURCE_ATTRIBUTE_MAP = { 'vims': { @@ -549,7 +552,152 @@ RESOURCE_ATTRIBUTE_MAP = { 'allow_put': False, 'is_visible': True, }, - } + }, + + 'nsds': { + 'id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True, + }, + 'tenant_id': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'required_by_policy': True, + 'is_visible': True, + }, + 'name': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, + }, + 'description': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': '', + }, + 'created_at': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'updated_at': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'attributes': { + 'allow_post': True, + 'allow_put': False, + 'convert_to': attr.convert_none_to_empty_dict, + 'validate': {'type:dict_or_nodata': None}, + 'is_visible': True, + 'default': None, + }, + + }, + + 'nss': { + 'id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True, + }, + 'tenant_id': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'required_by_policy': True, + 'is_visible': True, + }, + 'name': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, + }, + 'description': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': '', + }, + 'created_at': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'updated_at': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'vnf_ids': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': '', + }, + 'nsd_id': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + }, + 'vim_id': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': '', + }, + 'status': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'error_reason': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'created_at': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'updated_at': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'attributes': { + 'allow_post': True, + 'allow_put': False, + 'convert_to': attr.convert_none_to_empty_dict, + 'validate': {'type:dict_or_nodata': None}, + 'is_visible': True, + 'default': None, + }, + 'mgmt_urls': { + 'allow_post': False, + 'allow_put': False, + 'convert_to': attr.convert_none_to_empty_dict, + 'validate': {'type:dict_or_nodata': None}, + 'is_visible': True, + }, + }, + } diff --git a/tacker/extensions/nfvo_plugins/network_service.py b/tacker/extensions/nfvo_plugins/network_service.py new file mode 100644 index 000000000..0101389c5 --- /dev/null +++ b/tacker/extensions/nfvo_plugins/network_service.py @@ -0,0 +1,52 @@ +# 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 abc +import six + +from tacker.services import service_base + + +@six.add_metaclass(abc.ABCMeta) +class NSPluginBase(service_base.NFVPluginBase): + + @abc.abstractmethod + def create_nsd(self, context, nsd): + pass + + @abc.abstractmethod + def delete_nsd(self, context, nsd_id): + pass + + @abc.abstractmethod + def get_nsd(self, context, nsd_id, fields=None): + pass + + @abc.abstractmethod + def get_nsds(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def create_ns(self, context, ns): + pass + + @abc.abstractmethod + def get_nss(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def get_ns(self, context, ns_id, fields=None): + pass + + @abc.abstractmethod + def delete_ns(self, context, ns_id): + pass diff --git a/tacker/extensions/vnfm.py b/tacker/extensions/vnfm.py index 5da36c955..ac7a56282 100644 --- a/tacker/extensions/vnfm.py +++ b/tacker/extensions/vnfm.py @@ -149,6 +149,21 @@ class MetadataNotMatched(exceptions.InvalidInput): message = _("Metadata for alarm policy is not matched") +class InvalidSubstitutionMapping(exceptions.InvalidInput): + message = _("Input for substitution mapping requirements are not" + " valid for %(requirement)s. They must be in the form" + " of list with two entries") + + +class SMRequirementMissing(exceptions.InvalidInput): + message = _("All the requirements for substitution_mappings are not" + " provided. Missing requirement for %(requirement)s") + + +class InvalidParamsForSM(exceptions.InvalidInput): + message = _("Please provide parameters for substitution mappings") + + def _validate_service_type_list(data, valid_values=None): if not isinstance(data, list): msg = _("invalid data format for service list: '%s'") % data diff --git a/tacker/nfvo/nfvo_plugin.py b/tacker/nfvo/nfvo_plugin.py index 9c56b72f7..4f316907d 100644 --- a/tacker/nfvo/nfvo_plugin.py +++ b/tacker/nfvo/nfvo_plugin.py @@ -18,12 +18,15 @@ import os import threading import time import uuid +import yaml from cryptography import fernet from oslo_config import cfg from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import strutils +from tempfile import mkstemp +from toscaparser.tosca_template import ToscaTemplate from tacker._i18n import _ from tacker.common import driver_manager @@ -32,6 +35,7 @@ from tacker.common import log from tacker.common import utils from tacker import context as t_context from tacker.db.nfvo import nfvo_db +from tacker.db.nfvo import ns_db from tacker.db.nfvo import vnffg_db from tacker.extensions import nfvo from tacker import manager @@ -48,7 +52,8 @@ def config_opts(): return [('nfvo_vim', NfvoPlugin.OPTS)] -class NfvoPlugin(nfvo_db.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin): +class NfvoPlugin(nfvo_db.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, + ns_db.NSPluginDb): """NFVO reference plugin for NFVO extension Implements the NFVO extension and defines public facing APIs for VIM @@ -404,3 +409,74 @@ class NfvoPlugin(nfvo_db.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin): vim_auth=vim_obj['auth_cred'], resource_type=resource, resource_name=name) + + @log.log + def create_nsd(self, context, nsd): + nsd_data = nsd['nsd'] + template = nsd_data['attributes'].get('nsd') + if isinstance(template, dict): + nsd_data['attributes']['nsd'] = yaml.safe_dump( + template) + LOG.debug(_('nsd %s'), nsd_data) + + name = nsd_data['name'] + if self._get_by_name(context, ns_db.NSD, name): + raise exceptions.DuplicateResourceName(resource='NSD', name=name) + + self._parse_template_input(context, nsd) + return super(NfvoPlugin, self).create_nsd( + context, nsd) + + def _parse_template_input(self, context, nsd): + nsd_dict = nsd['nsd'] + nsd_yaml = nsd_dict['attributes'].get('nsd') + inner_nsd_dict = yaml.load(nsd_yaml) + nsd['vnfds'] = dict() + LOG.debug(_('nsd_dict: %s'), inner_nsd_dict) + + vnfm_plugin = manager.TackerManager.get_service_plugins()['VNFM'] + vnfd_imports = inner_nsd_dict['imports'] + inner_nsd_dict['imports'] = [] + new_files = [] + for vnfd_name in vnfd_imports: + vnfd = vnfm_plugin.get_vnfd(context, vnfd_name) + # Copy VNF types and VNF names + sm_dict = yaml.load(vnfd['attributes']['vnfd'])[ + 'topology_template'][ + 'substitution_mappings'] + nsd['vnfds'][sm_dict['node_type']] = vnfd['name'] + # Ugly Hack to validate the child templates + # TODO(tbh): add support in tosca-parser to pass child + # templates as dict + fd, temp_path = mkstemp() + with open(temp_path, 'w') as fp: + fp.write(vnfd['attributes']['vnfd']) + os.close(fd) + new_files.append(temp_path) + inner_nsd_dict['imports'].append(temp_path) + # Prepend the tacker_defs.yaml import file with the full + # path to the file + toscautils.updateimports(inner_nsd_dict) + + try: + ToscaTemplate(a_file=False, + yaml_dict_tpl=inner_nsd_dict) + except Exception as e: + LOG.exception(_("tosca-parser error: %s"), str(e)) + raise nfvo.ToscaParserFailed(error_msg_details=str(e)) + finally: + for file_path in new_files: + os.remove(file_path) + inner_nsd_dict['imports'] = vnfd_imports + + if ('description' not in nsd_dict or + nsd_dict['description'] == ''): + nsd_dict['description'] = inner_nsd_dict.get( + 'description', '') + if (('name' not in nsd_dict or + not len(nsd_dict['name'])) and + 'metadata' in inner_nsd_dict): + nsd_dict['name'] = inner_nsd_dict['metadata'].get( + 'template_name', '') + + LOG.debug(_('nsd %s'), nsd) diff --git a/tacker/plugins/common/constants.py b/tacker/plugins/common/constants.py index b590eb4ae..6af1471a2 100644 --- a/tacker/plugins/common/constants.py +++ b/tacker/plugins/common/constants.py @@ -56,6 +56,7 @@ POLICY_ALARMING = 'tosca.policies.tacker.Alarming' DEFAULT_ALARM_ACTIONS = ['respawn', 'log', 'log_and_kill', 'notify'] RES_TYPE_VNFD = "vnfd" +RES_TYPE_NSD = "nsd" RES_TYPE_VNF = "vnf" RES_TYPE_VIM = "vim" @@ -64,8 +65,8 @@ RES_EVT_DELETE = "DELETE" RES_EVT_UPDATE = "UPDATE" RES_EVT_MONITOR = "MONITOR" RES_EVT_SCALE = "SCALE" -RES_EVT_VNFD_NA_STATE = "Not Applicable" -RES_EVT_VNFD_ONBOARDED = "OnBoarded" +RES_EVT_NA_STATE = "Not Applicable" +RES_EVT_ONBOARDED = "OnBoarded" RES_EVT_CREATED_FLD = "created_at" RES_EVT_DELETED_FLD = "deleted_at" diff --git a/tacker/tests/functional/vnfm/test_tosca_vnfd.py b/tacker/tests/functional/vnfm/test_tosca_vnfd.py index 12ad0c9ee..04763be62 100644 --- a/tacker/tests/functional/vnfm/test_tosca_vnfd.py +++ b/tacker/tests/functional/vnfm/test_tosca_vnfd.py @@ -37,14 +37,14 @@ class VnfdTestCreate(base.BaseTackerTest): vnfd_id = vnfd_instance['vnfd']['id'] self.verify_vnfd_events( vnfd_id, evt_constants.RES_EVT_CREATE, - evt_constants.RES_EVT_VNFD_ONBOARDED) + evt_constants.RES_EVT_ONBOARDED) try: self.client.delete_vnfd(vnfd_id) except Exception: assert False, "vnfd Delete failed" self.verify_vnfd_events(vnfd_id, evt_constants.RES_EVT_DELETE, - evt_constants.RES_EVT_VNFD_NA_STATE) + evt_constants.RES_EVT_NA_STATE) def test_tosca_vnfd(self): self._test_create_list_delete_tosca_vnfd('sample-tosca-vnfd.yaml', diff --git a/tacker/tests/functional/vnfm/test_vnfm_param.py b/tacker/tests/functional/vnfm/test_vnfm_param.py index e151678a9..935701385 100644 --- a/tacker/tests/functional/vnfm/test_vnfm_param.py +++ b/tacker/tests/functional/vnfm/test_vnfm_param.py @@ -33,7 +33,7 @@ class VnfmTestParam(base.BaseTackerTest): self.assertIsNotNone(vnfd_id) self.verify_vnfd_events( vnfd_id, evt_constants.RES_EVT_CREATE, - evt_constants.RES_EVT_VNFD_ONBOARDED) + evt_constants.RES_EVT_ONBOARDED) return vnfd_instance def _test_vnfd_delete(self, vnfd_instance): @@ -45,7 +45,7 @@ class VnfmTestParam(base.BaseTackerTest): except Exception: assert False, "vnfd Delete failed" self.verify_vnfd_events(vnfd_id, evt_constants.RES_EVT_DELETE, - evt_constants.RES_EVT_VNFD_NA_STATE) + evt_constants.RES_EVT_NA_STATE) try: vnfd_d = self.client.show_vnfd(vnfd_id) except Exception: diff --git a/tacker/tests/unit/db/utils.py b/tacker/tests/unit/db/utils.py index 740142399..33c1116e1 100644 --- a/tacker/tests/unit/db/utils.py +++ b/tacker/tests/unit/db/utils.py @@ -41,6 +41,7 @@ vnfd_alarm_respawn_tosca_template = _get_template( 'test_tosca_vnfd_alarm_respawn.yaml') vnfd_alarm_scale_tosca_template = _get_template( 'test_tosca_vnfd_alarm_scale.yaml') +nsd_tosca_template = yaml.load(_get_template('tosca_nsd_template.yaml')) def get_dummy_vnfd_obj(): @@ -177,3 +178,11 @@ def get_dummy_vnffg_obj_vnf_mapping(): 'VNF3': '7168062e-9fa1-4203-8cb7-f5c99ff3ee1b' }, 'symmetrical': False}} + + +def get_dummy_nsd_obj(): + return {'nsd': {'description': 'dummy nsd description', + 'name': 'dummy_NSD', + 'tenant_id': u'8819a1542a5948b68f94d4be0fd50496', + 'template': {}, + 'attributes': {u'nsd': nsd_tosca_template}}} diff --git a/tacker/tests/unit/vm/infra_drivers/openstack/data/tosca_nsd_template.yaml b/tacker/tests/unit/vm/infra_drivers/openstack/data/tosca_nsd_template.yaml new file mode 100644 index 000000000..e8f81e3f5 --- /dev/null +++ b/tacker/tests/unit/vm/infra_drivers/openstack/data/tosca_nsd_template.yaml @@ -0,0 +1,38 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 +imports: + - VNF1 + - VNF2 + +topology_template: + inputs: + vl1_name: + type: string + description: name of VL1 virtuallink + default: net_mgmt + vl2_name: + type: string + description: name of VL2 virtuallink + default: net0 + + node_templates: + VNF1: + type: tosca.nodes.nfv.VNF1 + requirements: + - virtualLink1: VL1 + - virtualLink2: VL2 + + VNF2: + type: tosca.nodes.nfv.VNF2 + + VL1: + type: tosca.nodes.nfv.VL + properties: + network_name: {get_input: vl1_name} + vendor: tacker + + VL2: + type: tosca.nodes.nfv.VL + properties: + network_name: {get_input: vl2_name} + vendor: tacker + diff --git a/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py b/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py index f1c64aab5..a8a855c76 100644 --- a/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py +++ b/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py @@ -69,7 +69,7 @@ class FakeVNFMPlugin(mock.Mock): elif {'name': ['VNF3']} in args: return [{'id': self.vnf3_vnfd_id}] else: - return None + return [] def get_vnfs(self, *args, **kwargs): if {'vnfd_id': [self.vnf1_vnfd_id]} in args: @@ -432,3 +432,13 @@ class TestNfvoPlugin(db_base.SqlTestCase): self._driver_manager.invoke.assert_called_with(mock.ANY, mock.ANY, fc_id=mock.ANY, auth_attr=mock.ANY) + +# def test_create_nsd(self): +# nsd_obj = utils.get_dummy_nsd_obj() +# with patch.object(TackerManager, 'get_service_plugins') as \ +# mock_plugins: +# mock_plugins.return_value = {'VNFM': FakeVNFMPlugin()} +# mock.patch('tacker.common.driver_manager.DriverManager', +# side_effect=FakeDriverManager()).start() +# result = self.nfvo_plugin.create_nsd(self.context, nsd_obj) +# self.assertIsNotNone(result) diff --git a/tacker/tests/unit/vm/test_plugin.py b/tacker/tests/unit/vm/test_plugin.py index b232c781e..8bcd4a5af 100644 --- a/tacker/tests/unit/vm/test_plugin.py +++ b/tacker/tests/unit/vm/test_plugin.py @@ -226,7 +226,7 @@ class TestVNFMPlugin(db_base.SqlTestCase): mock_update_imports.assert_called_once_with(yaml_dict) self._cos_db_plugin.create_event.assert_called_once_with( self.context, evt_type=constants.RES_EVT_CREATE, res_id=mock.ANY, - res_state=constants.RES_EVT_VNFD_ONBOARDED, + res_state=constants.RES_EVT_ONBOARDED, res_type=constants.RES_TYPE_VNFD, tstamp=mock.ANY) def test_create_vnfd_no_service_types(self): diff --git a/tacker/vnfm/infra_drivers/openstack/translate_template.py b/tacker/vnfm/infra_drivers/openstack/translate_template.py index 6e307dc0d..63b8408f1 100644 --- a/tacker/vnfm/infra_drivers/openstack/translate_template.py +++ b/tacker/vnfm/infra_drivers/openstack/translate_template.py @@ -289,6 +289,9 @@ class TOSCAToHOT(object): raise vnfm.ParamYAMLNotWellFormed(error_msg_details=str(e)) toscautils.updateimports(vnfd_dict) + if 'substitution_mappings' in str(vnfd_dict): + toscautils.check_for_substitution_mappings(vnfd_dict, + parsed_params) try: tosca = tosca_template.ToscaTemplate(parsed_params=parsed_params, diff --git a/tacker/vnfm/tosca/utils.py b/tacker/vnfm/tosca/utils.py index b257b0e0e..0db064ace 100644 --- a/tacker/vnfm/tosca/utils.py +++ b/tacker/vnfm/tosca/utils.py @@ -99,6 +99,38 @@ def updateimports(template): LOG.debug(_("%s"), path) +@log.log +def check_for_substitution_mappings(template, params): + sm_dict = params.get('substitution_mappings') + if not sm_dict: + raise vnfm.InvalidParamsForSM() + del params['substitution_mappings'] + requirements = sm_dict.get('requirements') + if not requirements: + pass + node_tpl = template['topology_template']['node_templates'] + req_dict_tpl = template['topology_template']['substitution_mappings'][ + 'requirements'] + for req_name, req_val in iteritems(req_dict_tpl): + if req_name not in requirements: + raise vnfm.SMRequirementMissing(requirement=req_name) + if not isinstance(req_val, list): + raise vnfm.InvalidSubstitutionMapping(requirement=req_name) + try: + node_name = req_val[0] + node_req = req_val[1] + + node_tpl[node_name]['requirements'].append({ + node_req: { + 'node': requirements[req_name] + } + }) + node_tpl[requirements[req_name]] = \ + sm_dict[requirements[req_name]] + except Exception: + raise vnfm.InvalidSubstitutionMapping(requirement=req_name) + + @log.log def get_vdu_monitoring(template): monitoring_dict = {}