diff --git a/releasenotes/notes/implement-vnffg-support-for-ns-3acd9759e87c9d0c.yaml b/releasenotes/notes/implement-vnffg-support-for-ns-3acd9759e87c9d0c.yaml new file mode 100644 index 000000000..f013aec34 --- /dev/null +++ b/releasenotes/notes/implement-vnffg-support-for-ns-3acd9759e87c9d0c.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + This patch will add VNFFG support for NS. In NSD, users can describe VNFDs + and nested VNFFGD inside. When NS is created, VNFs and VNFFG are also + created. diff --git a/samples/tosca-templates/vnffg-nsd/ns_param.yaml b/samples/tosca-templates/vnffg-nsd/ns_param.yaml new file mode 100644 index 000000000..1c9661be5 --- /dev/null +++ b/samples/tosca-templates/vnffg-nsd/ns_param.yaml @@ -0,0 +1,5 @@ +nsd: + vl1_name: net_mgmt + vl2_name: net0 + net_src_port_id: c0a40f9c-d229-40b7-871f-55131cf17783 + ip_dest_prefix: 10.10.0.11/24 diff --git a/samples/tosca-templates/vnffg-nsd/tosca-multiple-vnffg-nsd.yaml b/samples/tosca-templates/vnffg-nsd/tosca-multiple-vnffg-nsd.yaml new file mode 100644 index 000000000..45facb483 --- /dev/null +++ b/samples/tosca-templates/vnffg-nsd/tosca-multiple-vnffg-nsd.yaml @@ -0,0 +1,109 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 + +description: Import VNFDs(already on-boarded) with input parameters +imports: + - sample-vnfd1 + - sample-vnfd2 + +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 + net_src_port_id: + type: string + description: neutron port id of source port + ip_dest_prefix: + type: string + description: IP prefix of destination port + + node_templates: + VNF1: + type: tosca.nodes.nfv.VNF1 + requirements: + - virtualLink1: VL1 + + 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 + + Forwarding_path1: + type: tosca.nodes.nfv.FP.TackerV2 + description: creates path inside ns (src_port->CP12->CP22->dst_port) + properties: + id: 51 + policy: + type: ACL + criteria: + - name: block_tcp + classifier: + network_src_port_id: {get_input: net_src_port_id} + destination_port_range: 80-1024 + ip_proto: 6 + ip_dst_prefix: {get_input: ip_dest_prefix} + path: + - forwarder: sample-vnfd1 + capability: CP12 + - forwarder: sample-vnfd2 + capability: CP22 + + Forwarding_path2: + type: tosca.nodes.nfv.FP.TackerV2 + description: creates path inside ns (src_port->CP12->dst_port) + properties: + id: 52 + policy: + type: ACL + criteria: + - name: block_tcp + classifier: + network_src_port_id: {get_input: net_src_port_id} + destination_port_range: 8080-8080 + ip_proto: 6 + ip_dst_prefix: {get_input: ip_dest_prefix} + path: + - forwarder: sample-vnfd1 + capability: CP12 + + groups: + + VNFFG1: + type: tosca.groups.nfv.VNFFG + description: HTTP to Corporate Net + properties: + vendor: tacker + version: 1.0 + number_of_endpoints: 2 + dependent_virtual_link: [VL1, VL2] + connection_point: [CP12, CP22] + constituent_vnfs: [sample-vnfd1, sample-vnfd2] + members: [Forwarding_path1] + + VNFFG2: + type: tosca.groups.nfv.VNFFG + description: HTTP to Corporate Net + properties: + vendor: tacker + version: 1.0 + number_of_endpoints: 1 + dependent_virtual_link: [VL1] + connection_point: [CP12] + constituent_vnfs: [sample-vnfd1] + members: [Forwarding_path2] + diff --git a/samples/tosca-templates/vnffg-nsd/tosca-single-vnffg-nsd.yaml b/samples/tosca-templates/vnffg-nsd/tosca-single-vnffg-nsd.yaml new file mode 100644 index 000000000..30e5ec907 --- /dev/null +++ b/samples/tosca-templates/vnffg-nsd/tosca-single-vnffg-nsd.yaml @@ -0,0 +1,78 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 + +description: Import VNFDs(already on-boarded) with input parameters +imports: + - sample-vnfd1 + - sample-vnfd2 + +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 + net_src_port_id: + type: string + description: neutron port id of source port + ip_dest_prefix: + type: string + description: IP prefix of destination port + + node_templates: + VNF1: + type: tosca.nodes.nfv.VNF1 + requirements: + - virtualLink1: VL1 + + 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 + + Forwarding_path1: + type: tosca.nodes.nfv.FP.TackerV2 + description: creates path inside ns (src_port->CP12->CP22->dst_port) + properties: + id: 51 + policy: + type: ACL + criteria: + - name: block_tcp + classifier: + network_src_port_id: {get_input: net_src_port_id} + destination_port_range: 80-1024 + ip_proto: 6 + ip_dst_prefix: {get_input: ip_dest_prefix} + path: + - forwarder: sample-vnfd1 + capability: CP12 + - forwarder: sample-vnfd2 + capability: CP22 + + groups: + + VNFFG1: + type: tosca.groups.nfv.VNFFG + description: HTTP to Corporate Net + properties: + vendor: tacker + version: 1.0 + number_of_endpoints: 2 + dependent_virtual_link: [VL1, VL2] + connection_point: [CP12, CP22] + constituent_vnfs: [sample-vnfd1, sample-vnfd2] + members: [Forwarding_path1] diff --git a/samples/tosca-templates/vnffg-nsd/tosca-vnfd1-sample.yaml b/samples/tosca-templates/vnffg-nsd/tosca-vnfd1-sample.yaml new file mode 100644 index 000000000..3d8befecf --- /dev/null +++ b/samples/tosca-templates/vnffg-nsd/tosca-vnfd1-sample.yaml @@ -0,0 +1,67 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 + +description: VirtualLinks of CP11 and CP22 will be provided by NS descriptor +node_types: + tosca.nodes.nfv.VNF1: + requirements: + - virtualLink1: + type: tosca.nodes.nfv.VL + required: true + capabilities: + forwader1: + type: tosca.capabilities.nfv.Forwarder + +topology_template: + substitution_mappings: + node_type: tosca.nodes.nfv.VNF1 + requirements: + virtualLink1: [CP11, virtualLink] + capabilities: + forwarder1: [CP11, forwarder] + + node_templates: + VDU1: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.5-x86_64-disk + flavor: m1.tiny + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + user_data_format: RAW + user_data: | + #!/bin/sh + echo 1 > /proc/sys/net/ipv4/ip_forward + + CP11: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + anti_spoofing_protection: false + requirements: + - virtualBinding: + node: VDU1 + + CP12: + type: tosca.nodes.nfv.CP.Tacker + properties: + anti_spoofing_protection: false + requirements: + - virtualLink: + node: VL2 + - virtualBinding: + node: VDU1 + + VL1: + type: tosca.nodes.nfv.VL + properties: + network_name: net_mgmt + vendor: Tacker + + VL2: + type: tosca.nodes.nfv.VL + properties: + network_name: net0 + vendor: Tacker diff --git a/samples/tosca-templates/vnffg-nsd/tosca-vnfd2-sample.yaml b/samples/tosca-templates/vnffg-nsd/tosca-vnfd2-sample.yaml new file mode 100644 index 000000000..3e62e4717 --- /dev/null +++ b/samples/tosca-templates/vnffg-nsd/tosca-vnfd2-sample.yaml @@ -0,0 +1,61 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 + +description: Demo example + +node_types: + tosca.nodes.nfv.VNF2: + capabilities: + forwarder1: + type: tosca.capabilities.nfv.Forwarder +topology_template: + substitution_mappings: + node_type: tosca.nodes.nfv.VNF2 + capabilities: + forwarder1: [CP21, forwarder] + node_templates: + VDU1: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.5-x86_64-disk + flavor: m1.tiny + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + user_data_format: RAW + user_data: | + #!/bin/sh + echo 1 > /proc/sys/net/ipv4/ip_forward + + CP21: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + anti_spoofing_protection: false + requirements: + - virtualLink: + node: VL1 + - virtualBinding: + node: VDU1 + + CP22: + type: tosca.nodes.nfv.CP.Tacker + requirements: + - virtualLink: + node: VL2 + - virtualBinding: + node: VDU1 + + VL1: + type: tosca.nodes.nfv.VL + properties: + network_name: net_mgmt + vendor: Tacker + + VL2: + type: tosca.nodes.nfv.VL + properties: + network_name: net0 + vendor: Tacker + diff --git a/samples/tosca-templates/vnffgd/tosca-vnffgd-sample.yaml b/samples/tosca-templates/vnffgd/tosca-vnffgd-sample.yaml index 890c55d14..6bd4246fc 100644 --- a/samples/tosca-templates/vnffgd/tosca-vnffgd-sample.yaml +++ b/samples/tosca-templates/vnffgd/tosca-vnffgd-sample.yaml @@ -16,10 +16,10 @@ topology_template: criteria: - name: block_tcp classifier: - network_src_port_id: 640dfd77-c92b-45a3-b8fc-22712de480e1 + network_src_port_id: 14ad4f29-629f-4b97-8bc8-86e96cb49974 destination_port_range: 80-1024 ip_proto: 6 - ip_dst_prefix: 192.168.1.2/24 + ip_dst_prefix: 10.10.0.5/24 path: - forwarder: VNFD1 capability: CP12 diff --git a/tacker/db/migration/alembic_migrations/versions/4747cc26b9c6_add_support_vnffg_to_ns_database.py b/tacker/db/migration/alembic_migrations/versions/4747cc26b9c6_add_support_vnffg_to_ns_database.py new file mode 100644 index 000000000..910c1b390 --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/4747cc26b9c6_add_support_vnffg_to_ns_database.py @@ -0,0 +1,41 @@ +# Copyright 2018 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. +# + +"""add support vnffg to ns database + +Revision ID: 4747cc26b9c6 +Revises: 5d490546290c +Create Date: 2018-06-27 03:18:12.227673 + +""" + +# revision identifiers, used by Alembic. +revision = '4747cc26b9c6' +down_revision = '5d490546290c' + +from alembic import op +import sqlalchemy as sa + +from tacker.db import types + + +def upgrade(active_plugins=None, options=None): + op.add_column('ns', sa.Column( + 'vnffg_ids', sa.TEXT(length=65535), nullable=True)) + op.add_column('vnffgs', sa.Column( + 'ns_id', types.Uuid(length=36), nullable=True)) + op.create_foreign_key('vnffg_foreign_key', + 'vnffgs', 'ns', + ['ns_id'], ['id']) diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index ce1ff77ea..1d116db35 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -5d490546290c +4747cc26b9c6 \ No newline at end of file diff --git a/tacker/db/nfvo/ns_db.py b/tacker/db/nfvo/ns_db.py index b34f12528..2e4a09bf3 100644 --- a/tacker/db/nfvo/ns_db.py +++ b/tacker/db/nfvo/ns_db.py @@ -98,6 +98,9 @@ class NS(model_base.BASE, models_v1.HasId, models_v1.HasTenant, # Dict of VNF details that network service launches vnf_ids = sa.Column(sa.TEXT(65535), nullable=True) + # VNFFG ids + vnffg_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) @@ -161,8 +164,8 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): LOG.debug('ns_db %s', ns_db) res = {} key_list = ('id', 'tenant_id', 'nsd_id', 'name', 'description', - 'vnf_ids', 'status', 'mgmt_urls', 'error_reason', - 'vim_id', 'created_at', 'updated_at') + 'vnf_ids', 'vnffg_ids', 'status', 'mgmt_urls', + 'error_reason', 'vim_id', 'created_at', 'updated_at') res.update((key, ns_db[key]) for key in key_list) return self._fields(res, fields) @@ -209,18 +212,14 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): tstamp=nsd_dict[constants.RES_EVT_CREATED_FLD]) return nsd_dict - def delete_nsd(self, - context, - nsd_id, - soft_delete=True): + 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 nfvo.NSDInUse(nsd_id=nsd_id) - nsd_db = self._get_resource(context, NSD, - 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( @@ -254,7 +253,7 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): nsd_id = ns['nsd_id'] vim_id = ns['vim_id'] name = ns.get('name') - ns_id = uuidutils.generate_uuid() + ns_id = ns['ns_id'] description = None if 'description' in ns: description = ns.get('description') @@ -269,6 +268,7 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): name=name, description=description, vnf_ids=None, + vnffg_ids=None, status=constants.PENDING_CREATE, mgmt_urls=None, nsd_id=nsd_id, @@ -291,11 +291,12 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): return self._make_ns_dict(ns_db) def create_ns_post(self, context, ns_id, mistral_obj, - vnfd_dict, error_reason): + vnfd_dict, vnffgd_templates, error_reason): LOG.debug('ns ID %s', ns_id) output = ast.literal_eval(mistral_obj.output) mgmt_urls = dict() vnf_ids = dict() + vnffg_ids = dict() if len(output) > 0: for vnfd_name, vnfd_val in iteritems(vnfd_dict): for instance in vnfd_val['instances']: @@ -305,28 +306,37 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): vnf_ids[instance] = output['vnf_id_' + instance] vnf_ids = str(vnf_ids) mgmt_urls = str(mgmt_urls) + if vnffgd_templates: + for vnffg_name in vnffgd_templates: + vnffg_output = 'vnffg_id_%s' % vnffg_name + vnffg_ids[vnffg_name] = output[vnffg_output] + vnffg_ids = str(vnffg_ids) if not vnf_ids: vnf_ids = None if not mgmt_urls: mgmt_urls = None + if not vnffg_ids: + vnffg_ids = None status = constants.ACTIVE if mistral_obj.state == 'SUCCESS' \ else constants.ERROR + with context.session.begin(subtransactions=True): - ns_db = self._get_resource(context, NS, - ns_id) + ns_db = self._get_resource(context, NS, ns_id) ns_db.update({'vnf_ids': vnf_ids}) + ns_db.update({'vnffg_ids': vnffg_ids}) ns_db.update({'mgmt_urls': mgmt_urls}) ns_db.update({'status': status}) ns_db.update({'error_reason': error_reason}) ns_db.update({'updated_at': timeutils.utcnow()}) ns_dict = self._make_ns_dict(ns_db) - self._cos_db_plg.create_event( - context, res_id=ns_dict['id'], - res_type=constants.RES_TYPE_NS, - res_state=constants.RES_EVT_NA_STATE, - evt_type=constants.RES_EVT_UPDATE, - tstamp=ns_dict[constants.RES_EVT_UPDATED_FLD]) + + self._cos_db_plg.create_event( + context, res_id=ns_dict['id'], + res_type=constants.RES_TYPE_NS, + res_state=constants.RES_EVT_NA_STATE, + evt_type=constants.RES_EVT_UPDATE, + tstamp=ns_dict[constants.RES_EVT_UPDATED_FLD]) return ns_dict # reference implementation. needs to be overrided by subclass diff --git a/tacker/db/nfvo/vnffg_db.py b/tacker/db/nfvo/vnffg_db.py index a25ddf51b..556d1ae17 100644 --- a/tacker/db/nfvo/vnffg_db.py +++ b/tacker/db/nfvo/vnffg_db.py @@ -24,6 +24,7 @@ from sqlalchemy.orm import exc as orm_exc from tacker.db import db_base from tacker.db import model_base from tacker.db import models_v1 +from tacker.db.nfvo.ns_db import NS from tacker.db import types from tacker.extensions import nfvo from tacker.extensions.nfvo_plugins import vnffg @@ -97,6 +98,9 @@ class Vnffg(model_base.BASE, models_v1.HasTenant, models_v1.HasId): attributes = sa.Column(types.Json) + # Associated Network Service + ns_id = sa.Column(types.Uuid, sa.ForeignKey('ns.id'), nullable=True) + class VnffgNfp(model_base.BASE, models_v1.HasTenant, models_v1.HasId): """Network Forwarding Path Data Model""" @@ -332,7 +336,7 @@ class VnffgPluginDbMixin(vnffg.VNFFGPluginBase, db_base.CommonDbMixin): param_value=param_vattrs_dict) for param_key in param_vattrs_dict.keys(): if param_matched.get(param_key) is None: - raise nfvo.VnffgParamValueNotUsed(param_key=param_key) + LOG.warning("Param input %s not used.", param_key) def _parametrize_topology_template(self, vnffg, template_db): if vnffg.get('attributes') and \ @@ -353,6 +357,7 @@ class VnffgPluginDbMixin(vnffg.VNFFGPluginBase, db_base.CommonDbMixin): name = vnffg.get('name') vnffg_id = vnffg.get('id') or uuidutils.generate_uuid() template_id = vnffg['vnffgd_id'] + ns_id = vnffg.get('ns_id', None) symmetrical = vnffg['symmetrical'] with context.session.begin(subtransactions=True): @@ -377,6 +382,7 @@ class VnffgPluginDbMixin(vnffg.VNFFGPluginBase, db_base.CommonDbMixin): description=template_db.description, vnf_mapping=vnf_mapping, vnffgd_id=template_id, + ns_id=ns_id, attributes=template_db.get('template'), status=constants.PENDING_CREATE) context.session.add(vnffg_db) @@ -835,7 +841,7 @@ class VnffgPluginDbMixin(vnffg.VNFFGPluginBase, db_base.CommonDbMixin): res = { 'forwarding_paths': vnffg_db.forwarding_paths[0]['id'] } - key_list = ('id', 'tenant_id', 'name', 'description', + key_list = ('id', 'tenant_id', 'name', 'description', 'ns_id', 'vnf_mapping', 'status', 'vnffgd_id', 'attributes') res.update((key, vnffg_db[key]) for key in key_list) return self._fields(res, fields) @@ -1240,6 +1246,14 @@ class VnffgPluginDbMixin(vnffg.VNFFGPluginBase, db_base.CommonDbMixin): def _delete_vnffg_pre(self, context, vnffg_id): vnffg = self.get_vnffg(context, vnffg_id) + ns_id = vnffg.get('ns_id') + if ns_id: + ns_db = self._get_resource(context, NS, ns_id) + # If network service is not in pending_delete status, + # raise error when delete vnffg. + if ns_db['status'] != constants.PENDING_DELETE: + raise nfvo.VnffgInUseNS(vnffg_id=vnffg_id, + ns_id=vnffg.get('ns_id')) nfp = self.get_nfp(context, vnffg['forwarding_paths']) chain = self.get_sfc(context, nfp['chain_id']) classifiers = [self.get_classifier(context, classifier_id) diff --git a/tacker/extensions/nfvo.py b/tacker/extensions/nfvo.py index a62d22363..b30858406 100644 --- a/tacker/extensions/nfvo.py +++ b/tacker/extensions/nfvo.py @@ -153,10 +153,6 @@ class VnffgTemplateParamParsingException(exceptions.TackerException): "missing input param %(get_input)s.") -class VnffgParamValueNotUsed(exceptions.TackerException): - message = _("Param input %(param_key)s not used.") - - class VnffgPropertyNotFoundException(exceptions.NotFound): message = _('VNFFG Property %(vnffg_property)s could not be found') @@ -183,6 +179,11 @@ class VnffgDeleteFailed(exceptions.TackerException): message = _('Deleting VNFFG %(vnffg_id)s failed') +class VnffgInUseNS(exceptions.TackerException): + message = _('VNFFG %(vnffg_id)s belongs to active network service ' + '%(ns_id)s') + + class NfpAttributeNotFoundException(exceptions.NotFound): message = _('NFP attribute %(attribute)s could not be found') @@ -485,6 +486,12 @@ RESOURCE_ATTRIBUTE_MAP = { 'is_visible': True, 'default': None, }, + 'ns_id': { + 'allow_post': True, + 'allow_put': False, + 'is_visible': True, + 'default': None, + }, }, 'nfps': { @@ -745,6 +752,13 @@ RESOURCE_ATTRIBUTE_MAP = { 'is_visible': True, 'default': '', }, + 'vnffg_ids': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': '', + }, 'nsd_id': { 'allow_post': True, 'allow_put': False, diff --git a/tacker/nfvo/drivers/workflow/workflow_generator.py b/tacker/nfvo/drivers/workflow/workflow_generator.py index 8a1d89a74..8fa2c8e0e 100644 --- a/tacker/nfvo/drivers/workflow/workflow_generator.py +++ b/tacker/nfvo/drivers/workflow/workflow_generator.py @@ -11,13 +11,13 @@ # under the License. import ast -from oslo_utils import uuidutils from tacker.mistral import workflow_generator OUTPUT = { - 'create_vnf': ['vnf_id', 'vim_id', 'mgmt_url', 'status'] + 'create_vnf': ['vnf_id', 'vim_id', 'mgmt_url', 'status'], + 'create_vnffg': ['vnffg_id'], } @@ -31,9 +31,9 @@ class WorkflowGenerator(workflow_generator.WorkflowGeneratorBase): for node in nodes: task = self.wf_name + '_' + node task_dict[task] = { - 'action': 'tacker.create_vnf body=<% $.vnf.{0} ' + 'action': 'tacker.create_vnf body=<% $.ns.{0} ' '%>'.format(node), - 'input': {'body': '<% $.vnf.{0} %>'.format(node)}, + 'input': {'body': '<% $.ns.{0} %>'.format(node)}, 'publish': { 'vnf_id_' + node: '<% task({0}).result.vnf.id ' '%>'.format(task), @@ -79,9 +79,20 @@ class WorkflowGenerator(workflow_generator.WorkflowGeneratorBase): '"ERROR" %>'.format(node)} ] } + vnffgd_templates = ns.get('vnffgd_templates') + if vnffgd_templates: + for vnffg_name in vnffgd_templates: + vnffg_group = vnffgd_templates[vnffg_name][ + 'topology_template']['groups'][vnffg_name] + constituent_vnfs = vnffg_group[ + 'properties']['constituent_vnfs'] + + if vnfd_name in constituent_vnfs: + task_dict[task]['on-success'].append( + 'create_vnffg_%s' % vnffg_name) return task_dict - def _add_delete_vnf_tasks(self, ns): + def _add_delete_vnf_tasks(self, ns, vnffg_ids=None): vnfds = ns['vnfd_details'] task_dict = dict() for vnfd_name, vnfd_info in (vnfds).items(): @@ -92,6 +103,55 @@ class WorkflowGenerator(workflow_generator.WorkflowGeneratorBase): 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_{0}' '%>'.format(node), } + if vnffg_ids and len(vnffg_ids): + task_dict[task].update({'join': 'all'}) + return task_dict + + def _add_create_vnffg_task(self, vnffgd_templates): + task_dict = dict() + previous_task = None + for vnffg_name in vnffgd_templates: + task = 'create_vnffg_%s' % vnffg_name + vnffg_output = 'vnffg_id_%s' % vnffg_name + task_dict[task] = { + 'join': 'all', + 'action': 'tacker.create_vnffg body=<% $.ns.{0} ' + '%>'.format(vnffg_name), + 'input': {'body': '<% $.ns.{0} %>'.format(vnffg_name)}, + 'publish': { + vnffg_output: '<% task({0}).result.' + 'vnffg.id %>'.format(task)} + } + if previous_task: + task_dict[previous_task].update({'on-success': [task]}) + previous_task = task + return task_dict + + def _add_delete_vnffg_task(self, ns): + task_dict = dict() + vnfds = ns['vnfd_details'] + vnffg_ids = ns['vnffg_details'] + delayed_tasks = list() + + for vnfd_name, vnfd_info in (vnfds).items(): + nodes = vnfd_info['instances'] + for node in nodes: + wait_task = 'delete_vnf_%s' % node + delayed_tasks.append(wait_task) + + previous_task = None + for vnffg_name in vnffg_ids.keys(): + task = 'delete_vnffg_%s' % vnffg_name + if previous_task: + wait_tasks = delayed_tasks + [previous_task] + else: + wait_tasks = delayed_tasks + previous_task = task + task_dict[task] = { + 'action': 'tacker.delete_vnffg vnffg=<% $.{0} ' + '%>'.format(vnffg_name), + 'on-success': wait_tasks + } return task_dict def _build_output_dict(self, ns): @@ -100,9 +160,16 @@ class WorkflowGenerator(workflow_generator.WorkflowGeneratorBase): for vnfd_name, vnfd_info in (vnfds).items(): nodes = vnfd_info['instances'] for node in nodes: - for op_name in OUTPUT[self.wf_name]: + for op_name in OUTPUT['create_vnf']: task_dict[op_name + '_' + node] = \ '<% $.{0}_{1} %>'.format(op_name, node) + vnffgd_templates = ns.get('vnffgd_templates') + if vnffgd_templates: + for vnffg_name in vnffgd_templates: + for op_name in OUTPUT['create_vnffg']: + vnffg_output = '%s_%s' % (op_name, vnffg_name) + task_dict[vnffg_output] = \ + '<% $.{0}_{1} %>'.format(op_name, vnffg_name) return task_dict def get_input_dict(self): @@ -110,24 +177,60 @@ class WorkflowGenerator(workflow_generator.WorkflowGeneratorBase): def build_input(self, ns, params): vnfds = ns['vnfd_details'] - id = uuidutils.generate_uuid() - self.input_dict = {'vnf': {}} + ns_id = ns['ns'].get('ns_id') + ns_name = ns['ns'].get('name') + self.input_dict = {'ns': {}} for vnfd_name, vnfd_info in (vnfds).items(): nodes = vnfd_info['instances'] for node in nodes: - self.input_dict['vnf'][node] = dict() - self.input_dict['vnf'][node]['vnf'] = { + vnf_name = '%s_VNF_%s' % (ns_name, vnfd_info['id']) + self.input_dict['ns'][node] = dict() + self.input_dict['ns'][node]['vnf'] = { 'attributes': {}, 'vim_id': ns['ns'].get('vim_id', ''), 'vnfd_id': vnfd_info['id'], - 'name': 'create_vnf_%s_%s' % (vnfd_info['id'], id) + 'name': vnf_name } if params.get(vnfd_name): - self.input_dict['vnf'][node]['vnf']['attributes'] = { + self.input_dict['ns'][node]['vnf']['attributes'] = { 'param_values': params.get(vnfd_name) } + if ns.get('vnffgd_templates'): + vnffg_input = self.build_vnffg_input(ns, params, ns_id) + self.input_dict['ns'].update(vnffg_input) - def create_vnf(self, **kwargs): + def build_vnffg_input(self, ns, params, ns_id): + vnffgd_templates = ns.get('vnffgd_templates') + vnffg_input = dict() + for vnffg_name in vnffgd_templates: + vnffg_group = vnffgd_templates[vnffg_name][ + 'topology_template']['groups'][vnffg_name] + constituent_vnfs = vnffg_group[ + 'properties']['constituent_vnfs'] + vnf_mapping = self.get_vnf_mapping(ns, constituent_vnfs) + vnffgd_body = dict() + vnffgd_body['vnffg'] = { + 'name': '%s_%s_%s' % (ns['ns'].get('name'), vnffg_name, ns_id), + 'vnffgd_template': vnffgd_templates[vnffg_name], + 'vnf_mapping': vnf_mapping, + 'attributes': { + 'param_values': params.get('nsd')}, + 'ns_id': ns_id + } + vnffg_input[vnffg_name] = vnffgd_body + return vnffg_input + + def get_vnf_mapping(self, ns, constituent_vnfs): + vnfds = ns['vnfd_details'] + vnf_mapping = dict() + for vnfd_name, vnfd_info in (vnfds).items(): + if vnfd_name in constituent_vnfs: + vnf_name = '%s_VNF_%s' % (ns['ns'].get('name'), + vnfd_info['id']) + vnf_mapping[vnfd_name] = vnf_name + return vnf_mapping + + def create_ns(self, **kwargs): ns = kwargs.get('ns') params = kwargs.get('params') # TODO(anyone): Keep this statements in a loop and @@ -142,8 +245,12 @@ class WorkflowGenerator(workflow_generator.WorkflowGeneratorBase): self.definition[self.wf_identifier]['output'] = \ self._build_output_dict(ns) self.build_input(ns, params) + vnffgd_templates = ns.get('vnffgd_templates') + if vnffgd_templates: + self.definition[self.wf_identifier]['tasks'].update( + self._add_create_vnffg_task(vnffgd_templates)) - def delete_vnf(self, ns): + def delete_ns(self, ns): ns_dict = {'vnfd_details': {}} vnf_ids = ast.literal_eval(ns['vnf_ids']) self.definition[self.wf_identifier]['input'] = [] @@ -153,5 +260,14 @@ class WorkflowGenerator(workflow_generator.WorkflowGeneratorBase): self.input_dict[vnf_key] = vnf_ids[vnf] ns_dict['vnfd_details'][vnf] = {'instances': [vnf]} self.definition[self.wf_identifier]['tasks'] = dict() + + vnffg_ids = ast.literal_eval(ns.get('vnffg_ids')) + if len(vnffg_ids): + for vnffg_name in vnffg_ids.keys(): + self.definition[self.wf_identifier]['input'].append(vnffg_name) + self.input_dict[vnffg_name] = vnffg_ids[vnffg_name] + ns_dict['vnffg_details'] = vnffg_ids + self.definition[self.wf_identifier]['tasks'].update( + self._add_delete_vnffg_task(ns_dict)) self.definition[self.wf_identifier]['tasks'].update( - self._add_delete_vnf_tasks(ns_dict)) + self._add_delete_vnf_tasks(ns_dict, vnffg_ids)) diff --git a/tacker/nfvo/nfvo_plugin.py b/tacker/nfvo/nfvo_plugin.py index 18424c172..4182c4289 100644 --- a/tacker/nfvo/nfvo_plugin.py +++ b/tacker/nfvo/nfvo_plugin.py @@ -663,16 +663,41 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, if vnfd_name == vnfd['name']: return vnfd['id'] + @log.log + def _get_vnffgds_from_nsd(self, nsd_dict): + ns_topo = nsd_dict.get('topology_template') + vnffgd_templates = dict() + if ns_topo and ns_topo.get('groups'): + for vnffg_name in ns_topo.get('groups'): + vnffgd_template = dict() + # TODO(phuoc): add checking in case vnffg_name exists + # more than one time. + # Constructing vnffgd from nsd, remove imports section + vnffgd_template['tosca_definitions_version'] = \ + nsd_dict.get('tosca_definitions_version') + vnffgd_template['description'] = nsd_dict.get('description') + vnffgd_template['topology_template'] = dict() + vnffgd_template['topology_template']['groups'] = dict() + vnffgd_template['topology_template']['groups'][vnffg_name] = \ + ns_topo['groups'].get(vnffg_name) + vnffgd_template['topology_template']['node_templates'] = dict() + for fp_name in ns_topo['groups'][vnffg_name]['members']: + vnffgd_template['topology_template']['node_templates'][ + fp_name] = ns_topo['node_templates'].get(fp_name) + vnffgd_templates[vnffg_name] = vnffgd_template + return vnffgd_templates + @log.log def create_ns(self, context, ns): - """Create NS and corresponding VNFs. + """Create NS, corresponding VNFs, VNFFGs. :param ns: ns dict which contains nsd_id and attributes This method has 3 steps: step-1: substitute all get_input params to its corresponding values step-2: Build params dict for substitution mappings case through which VNFs will actually substitute their requirements. - step-3: Create mistral workflow and execute the workflow + step-3: Create mistral workflow to create VNFs, VNFFG and execute the + workflow """ ns_info = ns['ns'] name = ns_info['name'] @@ -699,6 +724,13 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, if not ns['ns']['vim_id']: ns['ns']['vim_id'] = vim_res['vim_id'] + # TODO(phuoc): currently, create_ns function does not have + # create_ns_pre function, that pre-defines information of a network + # service. Creating ns_uuid keeps ns_id for consistency, it should be + # provided as return value of create_ns_pre function in ns db. + # Generate ns_uuid + ns['ns']['ns_id'] = uuidutils.generate_uuid() + # Step-1 param_values = ns['ns']['attributes'].get('param_values', {}) if 'get_input' in str(nsd_dict): @@ -740,6 +772,11 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, param_values[vnfd_name]['substitution_mappings'][ 'requirements'] = req_dict ns['vnfd_details'] = vnfd_dict + + vnffgd_templates = self._get_vnffgds_from_nsd(nsd_dict) + LOG.debug('vnffgd_templates: %s', vnffgd_templates) + ns['vnffgd_templates'] = vnffgd_templates + # Step-3 kwargs = {'ns': ns, 'params': param_values} @@ -747,7 +784,7 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, workflow = self._vim_drivers.invoke( driver_type, 'prepare_and_create_workflow', - resource='vnf', + resource='ns', action='create', auth_dict=self.get_auth_dict(context), kwargs=kwargs) @@ -780,6 +817,8 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, if exec_state == 'SUCCESS' or exec_state == 'ERROR': break mistral_retries = mistral_retries - 1 + # TODO(phuoc): add more information about error reason in case + # of exec_state is 'ERROR' error_reason = None if mistral_retries == 0 and exec_state == 'RUNNING': error_reason = _( @@ -801,8 +840,9 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, 'delete_workflow', workflow_id=workflow['id'], auth_dict=self.get_auth_dict(context)) - super(NfvoPlugin, self).create_ns_post(context, ns_id, exec_obj, - vnfd_dict, error_reason) + super(NfvoPlugin, self).create_ns_post( + context, ns_id, exec_obj, vnfd_dict, + vnffgd_templates, error_reason) self.spawn_n(_create_ns_wait, self, ns_dict['id'], mistral_execution.id) @@ -837,6 +877,7 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, @log.log def delete_ns(self, context, ns_id): ns = super(NfvoPlugin, self).get_ns(context, ns_id) + LOG.debug("Deleting ns: %s", ns) vim_res = self.vim_client.get_vim(context, ns['vim_id']) super(NfvoPlugin, self).delete_ns_pre(context, ns_id) driver_type = vim_res['vim_type'] @@ -846,11 +887,10 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, workflow = self._vim_drivers.invoke( driver_type, 'prepare_and_create_workflow', - resource='vnf', + resource='ns', action='delete', auth_dict=self.get_auth_dict(context), - kwargs={ - 'ns': ns}) + kwargs={'ns': ns}) except nfvo.NoTasksException: LOG.warning("No VNF deletion task(s).") if workflow: @@ -884,6 +924,8 @@ class NfvoPlugin(nfvo_db_plugin.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, if exec_state == 'SUCCESS' or exec_state == 'ERROR': break mistral_retries -= 1 + # TODO(phuoc): add more information about error reason in case + # of exec_state is 'ERROR' error_reason = None if mistral_retries == 0 and exec_state == 'RUNNING': error_reason = _( diff --git a/tacker/tests/unit/nfvo/drivers/workflow/test_workflow_generator.py b/tacker/tests/unit/nfvo/drivers/workflow/test_workflow_generator.py index 5d96be23a..3bb5a08d9 100644 --- a/tacker/tests/unit/nfvo/drivers/workflow/test_workflow_generator.py +++ b/tacker/tests/unit/nfvo/drivers/workflow/test_workflow_generator.py @@ -31,6 +31,71 @@ def get_dummy_ns(): 'placement_attr': {}} +def get_dummy_vnffg_ns(): + return { + u'ns': { + 'description': '', + 'vim_id': u'96025dd5-ca16-49f3-9823-958eb04260c4', + 'vnf_ids': '', u'attributes': {}, + u'nsd_id': u'b8587afb-6099-4f56-abce-572c62e3d61d', + u'name': u'test_create_ns'}, + 'vnfd_details': { + u'vnf1': {'instances': ['VNF1'], + 'id': u'dec09ed4-f355-4ec8-a00b-8548f6575a80'}, + u'vnf2': {'instances': ['VNF2'], + 'id': u'9f8f2af7-6407-4f79-a6fe-302c56172231'}}, + 'placement_attr': {}, + 'vnffgd_templates': { + 'VNFFG1': { + 'tosca_definitions_version': + 'tosca_simple_profile_for_nfv_1_0_0', + 'description': 'VNFFG1 descriptor', + 'topology_template': { + 'node_templates': { + 'Forwarding_path1': { + 'type': 'tosca.nodes.nfv.FP.TackerV2', + 'description': 'creates path inside ns - test', + 'properties': { + 'policy': { + 'type': 'ACL', + 'criteria': [{ + 'classifier': { + 'ip_proto': 6, + 'network_src_port_id': { + 'get_input': 'net_src_port_id' + }, + 'ip_dst_prefix': { + 'get_input': 'ip_dest_prefix' + }, + 'destination_port_range': '80-1024' + }, + 'name': 'block_tcp'}]}, + 'path': [ + {'capability': 'CP12', + 'forwarder': 'vnf1'}, + {'capability': 'CP22', + 'forwarder': 'vnf2'}], + 'id': 51}}}, + 'groups': { + 'VNFFG1': { + 'type': 'tosca.groups.nfv.VNFFG', + 'description': 'HTTP to Corporate Net', + 'members': ['Forwarding_path1'], + 'properties': { + 'version': 1.0, + 'vendor': 'tacker', + 'constituent_vnfs': ['vnf1', 'vnf2'], + 'connection_point': ['CP12', 'CP22'], + 'number_of_endpoints': 2, + 'dependent_virtual_link': ['VL1', 'VL2']} + } + } + } + } + } + } + + def get_dummy_param(): return {u'vnf1': {'substitution_mappings': {u'VL1b8587afb-60': { 'type': 'tosca.nodes.nfv.VL', 'properties': { @@ -45,7 +110,7 @@ def get_dummy_param(): def get_dummy_create_workflow(): - return {'std.create_vnf_dummy': {'input': ['vnf'], + return {'std.create_ns_dummy': {'input': ['ns'], 'tasks': { 'wait_vnf_active_VNF2': { 'action': 'tacker.show_vnf vnf=<% $.vnf_id_VNF2 %>', @@ -61,30 +126,30 @@ def get_dummy_create_workflow(): 'on-success': [{ 'delete_vnf_VNF2': '<% $.status_VNF2=' '"ERROR" %>'}]}, - 'create_vnf_VNF2': { - 'action': 'tacker.create_vnf body=<% $.vnf.VNF2 %>', - 'input': {'body': '<% $.vnf.VNF2 %>'}, + 'create_ns_VNF2': { + 'action': 'tacker.create_vnf body=<% $.ns.VNF2 %>', + 'input': {'body': '<% $.ns.VNF2 %>'}, 'publish': { - 'status_VNF2': '<% task(create_vnf_VNF2).' + 'status_VNF2': '<% task(create_ns_VNF2).' 'result.vnf.status %>', - 'vim_id_VNF2': '<% task(create_vnf_VNF2).' + 'vim_id_VNF2': '<% task(create_ns_VNF2).' 'result.vnf.vim_id %>', - 'mgmt_url_VNF2': '<% task(create_vnf_VNF2).' + 'mgmt_url_VNF2': '<% task(create_ns_VNF2).' 'result.vnf.mgmt_url %>', - 'vnf_id_VNF2': '<% task(create_vnf_VNF2)' + 'vnf_id_VNF2': '<% task(create_ns_VNF2)' '.result.vnf.id %>'}, 'on-success': ['wait_vnf_active_VNF2']}, - 'create_vnf_VNF1': { - 'action': 'tacker.create_vnf body=<% $.vnf.VNF1 %>', - 'input': {'body': '<% $.vnf.VNF1 %>'}, + 'create_ns_VNF1': { + 'action': 'tacker.create_vnf body=<% $.ns.VNF1 %>', + 'input': {'body': '<% $.ns.VNF1 %>'}, 'publish': { - 'status_VNF1': '<% task(create_vnf_VNF1).' + 'status_VNF1': '<% task(create_ns_VNF1).' 'result.vnf.status %>', - 'vnf_id_VNF1': '<% task(create_vnf_VNF1).' + 'vnf_id_VNF1': '<% task(create_ns_VNF1).' 'result.vnf.id %>', - 'mgmt_url_VNF1': '<% task(create_vnf_VNF1).' + 'mgmt_url_VNF1': '<% task(create_ns_VNF1).' 'result.vnf.mgmt_url %>', - 'vim_id_VNF1': '<% task(create_vnf_VNF1).' + 'vim_id_VNF1': '<% task(create_ns_VNF1).' 'result.vnf.vim_id %>'}, 'on-success': ['wait_vnf_active_VNF1']}, 'wait_vnf_active_VNF1': { @@ -100,10 +165,10 @@ def get_dummy_create_workflow(): 'result.vnf.mgmt_url %>'}, 'on-success': [{'delete_vnf_VNF1': '<% $.status_VNF1=' '"ERROR" %>'}]}, - 'delete_vnf_VNF1': {'action': 'tacker.delete_vnf vnf=<% ' - '$.vnf_id_VNF1%>'}, - 'delete_vnf_VNF2': {'action': 'tacker.delete_vnf vnf=<% ' - '$.vnf_id_VNF2%>'}}, + 'delete_vnf_VNF1': { + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>'}, + 'delete_vnf_VNF2': { + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF2%>'}}, 'type': 'direct', 'output': { 'status_VNF1': '<% $.status_VNF1 %>', 'status_VNF2': '<% $.status_VNF2 %>', @@ -116,15 +181,132 @@ def get_dummy_create_workflow(): 'version': '2.0'} +def get_dummy_create_vnffg_ns_workflow(): + return { + 'std.create_ns_dummy': { + 'input': ['ns'], + 'tasks': { + 'wait_vnf_active_VNF2': { + 'action': 'tacker.show_vnf vnf=<% $.vnf_id_VNF2 %>', + 'retry': { + 'count': 10, + 'delay': 10, + 'continue-on': + '<% $.status_VNF2 = "PENDING_CREATE" %>', + 'break-on': + '<% $.status_VNF2 = "ERROR" %>'}, + 'publish': { + 'status_VNF2': + '<% task(wait_vnf_active_VNF2).result.' + 'vnf.status %>', + 'mgmt_url_VNF2': + ' <% task(wait_vnf_active_VNF2).result.' + 'vnf.mgmt_url %>'}, + 'on-success': [ + {'delete_vnf_VNF2': '<% $.status_VNF2="ERROR" %>'}, + 'create_vnffg_VNFFG1']}, + 'create_vnffg_VNFFG1': { + 'action': 'tacker.create_vnffg body=<% $.ns.VNFFG1 %>', + 'input': {'body': '<% $.ns.VNFFG1 %>'}, + 'join': 'all', + 'publish': { + 'vnffg_id_VNFFG1': '<% task(create_vnffg_VNFFG1).' + 'result.vnffg.id %>'}}, + 'wait_vnf_active_VNF1': { + 'action': 'tacker.show_vnf vnf=<% $.vnf_id_VNF1 %>', + 'retry': { + 'count': 10, + 'delay': 10, + 'continue-on': + '<% $.status_VNF1 = "PENDING_CREATE" %>', + 'break-on': + '<% $.status_VNF1 = "ERROR" %>'}, + 'publish': { + 'status_VNF1': + '<% task(wait_vnf_active_VNF1).result.' + 'vnf.status %>', + 'mgmt_url_VNF1': + ' <% task(wait_vnf_active_VNF1).result.' + 'vnf.mgmt_url %>'}, + 'on-success': [ + {'delete_vnf_VNF1': '<% $.status_VNF1="ERROR" %>'}, + 'create_vnffg_VNFFG1']}, + 'create_ns_VNF1': { + 'action': 'tacker.create_vnf body=<% $.ns.VNF1 %>', + 'input': {'body': '<% $.ns.VNF1 %>'}, + 'publish': { + 'status_VNF1': + '<% task(create_ns_VNF1).result.vnf.status %>', + 'vnf_id_VNF1': + '<% task(create_ns_VNF1).result.vnf.id %>', + 'mgmt_url_VNF1': + '<% task(create_ns_VNF1).result.vnf.mgmt_url %>', + 'vim_id_VNF1': + '<% task(create_ns_VNF1).result.vnf.vim_id %>'}, + 'on-success': ['wait_vnf_active_VNF1']}, + 'create_ns_VNF2': { + 'action': 'tacker.create_vnf body=<% $.ns.VNF2 %>', + 'input': {'body': '<% $.ns.VNF2 %>'}, + 'publish': { + 'status_VNF2': + '<% task(create_ns_VNF2).result.vnf.status %>', + 'vim_id_VNF2': + '<% task(create_ns_VNF2).result.vnf.vim_id %>', + 'mgmt_url_VNF2': + '<% task(create_ns_VNF2).result.vnf.mgmt_url %>', + 'vnf_id_VNF2': + '<% task(create_ns_VNF2).result.vnf.id %>'}, + 'on-success': ['wait_vnf_active_VNF2']}, + 'delete_vnf_VNF1': { + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>'}, + 'delete_vnf_VNF2': { + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF2%>'}}, + 'type': 'direct', + 'output': { + 'status_VNF1': '<% $.status_VNF1 %>', + 'status_VNF2': '<% $.status_VNF2 %>', + 'mgmt_url_VNF2': '<% $.mgmt_url_VNF2 %>', + 'mgmt_url_VNF1': '<% $.mgmt_url_VNF1 %>', + 'vnffg_id_VNFFG1': '<% $.vnffg_id_VNFFG1 %>', + 'vim_id_VNF2': '<% $.vim_id_VNF2 %>', + 'vnf_id_VNF1': '<% $.vnf_id_VNF1 %>', + 'vnf_id_VNF2': '<% $.vnf_id_VNF2 %>', + 'vim_id_VNF1': '<% $.vim_id_VNF1 %>'}}, + 'version': '2.0'} + + def dummy_delete_ns_obj(): - return {'vnf_ids': u"{'VNF1': '5de5eca6-3e21-4bbd-a9d7-86458de75f0c'}"} + return {'vnf_ids': u"{'VNF1': '5de5eca6-3e21-4bbd-a9d7-86458de75f0c'}", + 'vnffg_ids': u"{}"} + + +def dummy_delete_vnffg_ns_obj(): + return {'vnf_ids': u"{'VNF1': '5de5eca6-3e21-4bbd-a9d7-86458de75f0c'}", + 'vnffg_ids': u"{'VNFFG1': '99066f25-3124-44f1-bc5d-bc0bf236b012'}"} def get_dummy_delete_workflow(): return {'version': '2.0', - 'std.delete_vnf_dummy': {'input': ['vnf_id_VNF1'], - 'tasks': {'delete_vnf_VNF1': { - 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>'}}, + 'std.delete_ns_dummy': { + 'input': ['vnf_id_VNF1'], + 'tasks': { + 'delete_vnf_VNF1': { + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>'}}, + 'type': 'direct'}} + + +def get_dummy_delete_vnffg_ns_workflow(): + return {'version': '2.0', + 'std.delete_ns_dummy': { + 'input': ['vnf_id_VNF1', 'VNFFG1'], + 'tasks': { + 'delete_vnf_VNF1': { + 'join': 'all', + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_VNF1%>'}, + 'delete_vnffg_VNFFG1': { + 'action': 'tacker.delete_vnffg vnffg=' + '<% $.VNFFG1 %>', + 'on-success': ['delete_vnf_VNF1']}}, 'type': 'direct'}} @@ -151,22 +333,49 @@ class TestWorkflowGenerator(base.TestCase): def test_prepare_workflow_create(self): fPlugin = FakeNFVOPlugin(context, self.mistral_client, - resource='vnf', action='create') + resource='ns', action='create') fPlugin.prepare_workflow(ns=get_dummy_ns(), params=get_dummy_param()) wf_def_values = [fPlugin.wg.definition[k] for k in fPlugin.wg.definition] - self.assertIn(get_dummy_create_workflow()['std.create_vnf_dummy'], + self.assertIn(get_dummy_create_workflow()['std.create_ns_dummy'], wf_def_values) self.assertEqual(get_dummy_create_workflow()['version'], fPlugin.wg.definition['version']) + def test_prepare_vnffg_ns_workflow_create(self): + fPlugin = FakeNFVOPlugin(context, self.mistral_client, + resource='ns', action='create') + fPlugin.prepare_workflow(ns=get_dummy_vnffg_ns(), + params=get_dummy_param()) + wf_def_values = [fPlugin.wg.definition[k] for + k in fPlugin.wg.definition] + self.assertIn( + get_dummy_create_vnffg_ns_workflow()['std.create_ns_dummy'], + wf_def_values) + self.assertEqual( + get_dummy_create_vnffg_ns_workflow()['version'], + fPlugin.wg.definition['version']) + def test_prepare_workflow_delete(self): fPlugin = FakeNFVOPlugin(context, self.mistral_client, - resource='vnf', action='delete') + resource='ns', action='delete') fPlugin.prepare_workflow(ns=dummy_delete_ns_obj()) wf_def_values = [fPlugin.wg.definition[k] for k in fPlugin.wg.definition] - self.assertIn(get_dummy_delete_workflow()['std.delete_vnf_dummy'], + self.assertIn(get_dummy_delete_workflow()['std.delete_ns_dummy'], wf_def_values) self.assertEqual(get_dummy_delete_workflow()['version'], fPlugin.wg.definition['version']) + + def test_prepare_vnffg_ns_workflow_delete(self): + fPlugin = FakeNFVOPlugin(context, self.mistral_client, + resource='ns', action='delete') + fPlugin.prepare_workflow(ns=dummy_delete_vnffg_ns_obj()) + wf_def_values = [fPlugin.wg.definition[k] for + k in fPlugin.wg.definition] + self.assertIn( + get_dummy_delete_vnffg_ns_workflow()['std.delete_ns_dummy'], + wf_def_values) + self.assertEqual( + get_dummy_delete_vnffg_ns_workflow()['version'], + fPlugin.wg.definition['version']) diff --git a/tacker/tests/unit/nfvo/test_nfvo_plugin.py b/tacker/tests/unit/nfvo/test_nfvo_plugin.py index 81911f759..eb19f096d 100644 --- a/tacker/tests/unit/nfvo/test_nfvo_plugin.py +++ b/tacker/tests/unit/nfvo/test_nfvo_plugin.py @@ -621,6 +621,7 @@ class TestNfvoPlugin(db_base.SqlTestCase): 'name': vnffg.get('name'), 'description': 'fake_template_description', 'vnffgd_id': vnffg.get('vnffgd_id'), + 'ns_id': None, 'attributes': template_db.get('template'), 'status': constants.PENDING_CREATE, 'vnf_mapping': vnf_mapping} @@ -771,16 +772,6 @@ class TestNfvoPlugin(db_base.SqlTestCase): self.nfvo_plugin.create_vnffg, self.context, vnffg_obj) - def test_create_vnffg_param_value_not_use(self): - with patch.object(TackerManager, 'get_service_plugins') as \ - mock_plugins: - mock_plugins.return_value = {'VNFM': FakeVNFMPlugin()} - self._insert_dummy_vnffg_param_template() - vnffg_obj = utils.get_dummy_vnffg_multi_param_obj() - self.assertRaises(nfvo.VnffgParamValueNotUsed, - self.nfvo_plugin.create_vnffg, - self.context, vnffg_obj) - def test_create_vnffg_vnf_mapping(self): with patch.object(TackerManager, 'get_service_plugins') as \ mock_plugins: