From 15c4d7ec5d51cd159579f084f7b3773dac6725f1 Mon Sep 17 00:00:00 2001 From: dharmendra Date: Mon, 26 Dec 2016 16:56:48 +0530 Subject: [PATCH] Implement NSD Support part 2 This patchset adds mistral workflow, tests, sample templates and user guide for NSD Change-Id: If53081bc76a5436287b307538a1255c65fc71cb2 Co-Authored-By: Bharath Thiruveedula Partially-implements: blueprint nsd-support --- doc/source/devref/nsd_usage_guide.rst | 244 ++++++++++++++++++ doc/source/index.rst | 1 + tacker/common/clients.py | 1 + tacker/db/nfvo/ns_db.py | 163 +++++++++++- tacker/extensions/common_services.py | 8 + tacker/extensions/nfvo.py | 4 + .../nfvo_plugins/network_service.py | 9 + tacker/extensions/vnfm.py | 8 - tacker/nfvo/drivers/vim/openstack_driver.py | 74 ++++++ tacker/nfvo/drivers/workflow/__init__.py | 0 tacker/nfvo/drivers/workflow/mistral.py | 53 ++++ tacker/nfvo/drivers/workflow/workflow.py | 27 ++ .../drivers/workflow/workflow_generator.py | 190 ++++++++++++++ tacker/nfvo/nfvo_plugin.py | 239 +++++++++++++++++ tacker/plugins/common/constants.py | 1 + tacker/tests/constants.py | 2 + tacker/tests/etc/samples/test-ns-nsd.yaml | 37 +++ tacker/tests/etc/samples/test-ns-vnfd1.yaml | 98 +++++++ tacker/tests/etc/samples/test-ns-vnfd2.yaml | 68 +++++ tacker/tests/etc/samples/test-nsd-vnfd1.yaml | 98 +++++++ tacker/tests/etc/samples/test-nsd-vnfd2.yaml | 68 +++++ tacker/tests/etc/samples/test-nsd.yaml | 37 +++ tacker/tests/functional/nfvo/test_nfvo.py | 150 +++++++++++ tacker/tests/unit/db/utils.py | 12 + .../unit/vm/nfvo/drivers/workflow/__init__.py | 0 .../vm/nfvo/drivers/workflow/test_mistral.py | 159 ++++++++++++ tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py | 178 ++++++++++++- tacker/tests/unit/vm/test_toscautils.py | 15 ++ .../vnfm/infra_drivers/openstack/openstack.py | 2 +- .../openstack/translate_template.py | 7 +- tacker/vnfm/tosca/utils.py | 19 +- 31 files changed, 1936 insertions(+), 36 deletions(-) create mode 100644 doc/source/devref/nsd_usage_guide.rst create mode 100644 tacker/nfvo/drivers/workflow/__init__.py create mode 100644 tacker/nfvo/drivers/workflow/mistral.py create mode 100644 tacker/nfvo/drivers/workflow/workflow.py create mode 100644 tacker/nfvo/drivers/workflow/workflow_generator.py create mode 100644 tacker/tests/etc/samples/test-ns-nsd.yaml create mode 100644 tacker/tests/etc/samples/test-ns-vnfd1.yaml create mode 100644 tacker/tests/etc/samples/test-ns-vnfd2.yaml create mode 100644 tacker/tests/etc/samples/test-nsd-vnfd1.yaml create mode 100644 tacker/tests/etc/samples/test-nsd-vnfd2.yaml create mode 100644 tacker/tests/etc/samples/test-nsd.yaml create mode 100644 tacker/tests/functional/nfvo/test_nfvo.py create mode 100644 tacker/tests/unit/vm/nfvo/drivers/workflow/__init__.py create mode 100644 tacker/tests/unit/vm/nfvo/drivers/workflow/test_mistral.py diff --git a/doc/source/devref/nsd_usage_guide.rst b/doc/source/devref/nsd_usage_guide.rst new file mode 100644 index 000000000..f902d42a1 --- /dev/null +++ b/doc/source/devref/nsd_usage_guide.rst @@ -0,0 +1,244 @@ +.. + 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. + +.. _ref-nsd: + +========================================================== +Orchestrating VNFs using Network Services Descriptor (NSD) +========================================================== + +To enable dynamic composition of network services, NFV introduces Network +Service Descriptors (NSDs) that specify the network service to be created. +This usage guide describes lifecycle of Network service descriptors and +services. + +NSD in Ocata can be used for creating multiple (related) VNFs in one shot +using a single TOSCA template. This is a first (big) step into NSD, few +follow-on enhancements like: +1) Creating VLs / neutron networks using NSD (to support inter-VNF private VL) +2) VNFFGD support in NSD. + +Creating the NSD +~~~~~~~~~~~~~~~~ + +Once OpenStack along with Tacker has been successfully installed, +deploy a sample VNFD templates using vnf1.yaml and vnf2.yaml as mentioned in +reference section. + +:: + tacker vnfd-create --vnfd-file vnfd1.yaml VNFD1 + + tacker vnfd-create --vnfd-file vnfd2.yaml VNFD2 + +The following code represents sample NSD which instantiates the above VNFs + +:: + + tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 + imports: + - VNFD1 + - VNFD2 + topology_template: + 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: net0 + vendor: tacker + VL2: + type: tosca.nodes.nfv.VL + properties: + network_name: net_mgmt + vendor: tacker + +In above NSD template VL1 and VL2 are substituting the virtuallinks of VNF1. +To onboard the above NSD: + +**tacker nsd-create --nsd-file ** + +Creating the NS +~~~~~~~~~~~~~~~~ + +To create a NS, you must have onboarded corresponding NSD and +VNFDS(which NS is substituting) + +Tacker provides the following CLI to create NS: + + **tacker ns-create --nsd-id ** + +Reference +~~~~~~~~~ + +VNF1 sample template for nsd named vnfd1.yaml: + +:: + + tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 + description: Demo example + node_types: + tosca.nodes.nfv.VNF1: + requirements: + - virtualLink1: + type: tosca.nodes.nfv.VL + required: true + - virtualLink2: + type: tosca.nodes.nfv.VL + required: true + capabilities: + forwader1: + type: tosca.capabilities.nfv.Forwarder + forwader2: + type: tosca.capabilities.nfv.Forwarder + + topology_template: + substitution_mappings: + node_type: tosca.nodes.nfv.VNF1 + requirements: + virtualLink1: [CP11, virtualLink] + virtualLink2: [CP14, virtualLink] + capabilities: + forwarder1: [CP11, forwarder] + forwarder2: [CP14, forwarder] + node_templates: + VDU1: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.4-x86_64-uec + flavor: m1.tiny + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + CP11: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + anti_spoofing_protection: false + requirements: + - virtualBinding: + node: VDU1 + + VDU2: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.4-x86_64-uec + flavor: m1.medium + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + CP13: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + anti_spoofing_protection: false + requirements: + - virtualLink: + node: VL1 + - virtualBinding: + node: VDU2 + CP14: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + anti_spoofing_protection: false + requirements: + - virtualBinding: + node: VDU2 + 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 + +VNF2 sample template for nsd named vnfd2.yaml: + +:: + + 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.4-x86_64-uec + flavor: m1.tiny + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + CP21: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + anti_spoofing_protection: false + requirements: + - virtualLink: + node: VL1 + - virtualBinding: + node: VDU1 + VDU2: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.4-x86_64-uec + flavor: m1.medium + availability_zone: nova + mgmt_driver: noop + CP22: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + anti_spoofing_protection: false + requirements: + - virtualLink: + node: VL2 + - virtualBinding: + node: VDU2 + 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/doc/source/index.rst b/doc/source/index.rst index 321a257e1..71eb4827f 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -60,6 +60,7 @@ Feature Documentation devref/event_logging.rst devref/vnffgd_template_description.rst devref/vnffg_usage_guide.rst + devref/nsd_usage_guide.rst API Documentation ================= diff --git a/tacker/common/clients.py b/tacker/common/clients.py index 991617bea..3a56064bc 100644 --- a/tacker/common/clients.py +++ b/tacker/common/clients.py @@ -20,6 +20,7 @@ class OpenstackClients(object): super(OpenstackClients, self).__init__() self.keystone_plugin = keystone.Keystone() self.heat_client = None + self.mistral_client = None self.keystone_client = None self.region_name = region_name self.auth_attr = auth_attr diff --git a/tacker/db/nfvo/ns_db.py b/tacker/db/nfvo/ns_db.py index 20feffe74..045590bcb 100644 --- a/tacker/db/nfvo/ns_db.py +++ b/tacker/db/nfvo/ns_db.py @@ -10,19 +10,23 @@ # License for the specific language governing permissions and limitations # under the License. +import ast import uuid from oslo_log import log as logging from oslo_utils import timeutils +from six import iteritems import sqlalchemy as sa from sqlalchemy import orm +from sqlalchemy.orm import exc as orm_exc 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 import nfvo from tacker.extensions.nfvo_plugins import network_service from tacker.plugins.common import constants @@ -94,6 +98,29 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): super(NSPluginDb, self).__init__() self._cos_db_plg = common_services_db.CommonServicesPluginDb() + def _get_resource(self, context, model, id): + try: + return self._get_by_id(context, model, id) + except orm_exc.NoResultFound: + if issubclass(model, NSD): + raise network_service.NSDNotFound(nsd_id=id) + if issubclass(model, NS): + raise network_service.NSNotFound(ns_id=id) + else: + raise + + def _get_ns_db(self, context, ns_id, current_statuses, new_status): + try: + ns_db = ( + self._model_query(context, NS). + filter(NS.id == ns_id). + filter(NS.status.in_(current_statuses)). + with_lockmode('update').one()) + except orm_exc.NoResultFound: + raise network_service.NSNotFound(ns_id=ns_id) + ns_db.update({'status': new_status}) + return ns_db + def _make_attributes_dict(self, attributes_db): return dict((attr.key, attr.value) for attr in attributes_db) @@ -106,6 +133,18 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): res.update((key, nsd[key]) for key in key_list) return self._fields(res, fields) + def _make_dev_attrs_dict(self, dev_attrs_db): + return dict((arg.key, arg.value) for arg in dev_attrs_db) + + def _make_ns_dict(self, ns_db, fields=None): + 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') + res.update((key, ns_db[key]) for key in key_list) + return self._fields(res, fields) + def create_nsd(self, context, nsd): vnfds = nsd['vnfds'] nsd = nsd['nsd'] @@ -150,8 +189,7 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): 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) + raise nfvo.NSDInUse(nsd_id=nsd_id) nsd_db = self._get_resource(context, NSD, nsd_id) @@ -179,14 +217,125 @@ class NSPluginDb(network_service.NSPluginBase, db_base.CommonDbMixin): # reference implementation. needs to be overrided by subclass def create_ns(self, context, ns): - return {'nsd': {}} + LOG.debug(_('ns %s'), ns) + ns = ns['ns'] + tenant_id = self._get_tenant_id_for_create(context, ns) + nsd_id = ns['nsd_id'] + vim_id = ns['vim_id'] + name = ns.get('name') + ns_id = str(uuid.uuid4()) + with context.session.begin(subtransactions=True): + nsd_db = self._get_resource(context, NSD, + nsd_id) + ns_db = NS(id=ns_id, + tenant_id=tenant_id, + name=name, + description=nsd_db.description, + vnf_ids=None, + status=constants.PENDING_CREATE, + mgmt_urls=None, + nsd_id=nsd_id, + vim_id=vim_id, + error_reason=None) + context.session.add(ns_db) + evt_details = "NS UUID assigned." + self._cos_db_plg.create_event( + context, res_id=ns_id, + res_type=constants.RES_TYPE_NS, + res_state=constants.PENDING_CREATE, + evt_type=constants.RES_EVT_CREATE, + tstamp=ns_db[constants.RES_EVT_CREATED_FLD], + details=evt_details) + return self._make_ns_dict(ns_db) + + def create_ns_post(self, context, ns_id, mistral_obj, + vnfd_dict, error_reason): + LOG.debug(_('ns ID %s'), ns_id) + output = ast.literal_eval(mistral_obj.output) + mgmt_urls = dict() + vnf_ids = dict() + if len(output) > 0: + for vnfd_name, vnfd_val in iteritems(vnfd_dict): + for instance in vnfd_val['instances']: + mgmt_urls[instance] = ast.literal_eval( + output['mgmt_url_' + instance].strip()) + vnf_ids[instance] = output['vnf_id_' + instance] + vnf_ids = str(vnf_ids) + mgmt_urls = str(mgmt_urls) + + if not vnf_ids: + vnf_ids = None + if not mgmt_urls: + mgmt_urls = 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.update({'vnf_ids': vnf_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]) + return ns_dict # reference implementation. needs to be overrided by subclass - def delete_ns(self, context, ns_id, soft_delete=True): - pass + def delete_ns(self, context, ns_id): + with context.session.begin(subtransactions=True): + ns_db = self._get_ns_db( + context, ns_id, _ACTIVE_UPDATE_ERROR_DEAD, + constants.PENDING_DELETE) + deleted_ns_db = self._make_ns_dict(ns_db) + self._cos_db_plg.create_event( + context, res_id=ns_id, + res_type=constants.RES_TYPE_NS, + res_state=deleted_ns_db['status'], + evt_type=constants.RES_EVT_DELETE, + tstamp=timeutils.utcnow(), details="NS delete initiated") + return deleted_ns_db + + def delete_ns_post(self, context, ns_id, mistral_obj, + error_reason, soft_delete=True): + with context.session.begin(subtransactions=True): + query = ( + self._model_query(context, NS). + filter(NS.id == ns_id). + filter(NS.status == constants.PENDING_DELETE)) + if mistral_obj.state == 'ERROR': + query.update({'status': constants.ERROR}) + self._cos_db_plg.create_event( + context, res_id=ns_id, + res_type=constants.RES_TYPE_NS, + res_state=constants.ERROR, + evt_type=constants.RES_EVT_DELETE, + tstamp=timeutils.utcnow(), + details="NS Delete ERROR") + else: + if soft_delete: + deleted_time_stamp = timeutils.utcnow() + query.update({'deleted_at': deleted_time_stamp}) + self._cos_db_plg.create_event( + context, res_id=ns_id, + res_type=constants.RES_TYPE_NS, + res_state=constants.PENDING_DELETE, + evt_type=constants.RES_EVT_DELETE, + tstamp=deleted_time_stamp, + details="ns Delete Complete") + else: + query.delete() def get_ns(self, context, ns_id, fields=None): - pass + ns_db = self._get_resource(context, NS, ns_id) + return self._make_ns_dict(ns_db) def get_nss(self, context, filters=None, fields=None): - pass + return self._get_collection(context, NS, + self._make_ns_dict, + filters=filters, fields=fields) diff --git a/tacker/extensions/common_services.py b/tacker/extensions/common_services.py index 46ee247cc..aba59c214 100644 --- a/tacker/extensions/common_services.py +++ b/tacker/extensions/common_services.py @@ -38,6 +38,14 @@ class InvalidModelException(exceptions.TackerException): message = _("Specified model is invalid, only Event model supported") +class InputValuesMissing(exceptions.InvalidInput): + message = _("Parameter input values missing for the key '%(key)s'") + + +class ParamYAMLInputMissing(exceptions.InvalidInput): + message = _("Parameter YAML input missing") + + RESOURCE_ATTRIBUTE_MAP = { 'events': { diff --git a/tacker/extensions/nfvo.py b/tacker/extensions/nfvo.py index a16d03488..f25a70f77 100644 --- a/tacker/extensions/nfvo.py +++ b/tacker/extensions/nfvo.py @@ -212,6 +212,10 @@ class ClassifierNotFoundException(exceptions.NotFound): class NSDInUse(exceptions.InUse): message = _('NSD %(nsd_id)s is still in use') + +class NSInUse(exceptions.InUse): + message = _('NS %(ns_id)s is still in use') + RESOURCE_ATTRIBUTE_MAP = { 'vims': { diff --git a/tacker/extensions/nfvo_plugins/network_service.py b/tacker/extensions/nfvo_plugins/network_service.py index 0101389c5..5b4abf72f 100644 --- a/tacker/extensions/nfvo_plugins/network_service.py +++ b/tacker/extensions/nfvo_plugins/network_service.py @@ -13,6 +13,7 @@ import abc import six +from tacker.common import exceptions from tacker.services import service_base @@ -50,3 +51,11 @@ class NSPluginBase(service_base.NFVPluginBase): @abc.abstractmethod def delete_ns(self, context, ns_id): pass + + +class NSDNotFound(exceptions.NotFound): + message = _('NSD %(nsd_id)s could not be found') + + +class NSNotFound(exceptions.NotFound): + message = _('NS %(ns_id)s could not be found') diff --git a/tacker/extensions/vnfm.py b/tacker/extensions/vnfm.py index ac7a56282..29d61f4c1 100644 --- a/tacker/extensions/vnfm.py +++ b/tacker/extensions/vnfm.py @@ -95,14 +95,6 @@ class HeatTranslatorFailed(exceptions.InvalidInput): message = _("heat-translator failed: - %(error_msg_details)s") -class InputValuesMissing(exceptions.InvalidInput): - message = _("Parameter input values missing for the key '%(key)s'") - - -class ParamYAMLInputMissing(exceptions.InvalidInput): - message = _("Parameter YAML input missing") - - class HeatClientException(exceptions.TackerException): message = _("%(msg)s") diff --git a/tacker/nfvo/drivers/vim/openstack_driver.py b/tacker/nfvo/drivers/vim/openstack_driver.py index e86a045a5..3debc74b3 100644 --- a/tacker/nfvo/drivers/vim/openstack_driver.py +++ b/tacker/nfvo/drivers/vim/openstack_driver.py @@ -16,12 +16,14 @@ import os import six +import yaml from keystoneauth1 import exceptions from keystoneauth1 import identity from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 from keystoneauth1 import session +from mistralclient.api import client as mistral_client from neutronclient.common import exceptions as nc_exceptions from neutronclient.v2_0 import client as neutron_client from oslo_config import cfg @@ -33,6 +35,7 @@ from tacker.common import log from tacker.extensions import nfvo from tacker.nfvo.drivers.vim import abstract_vim_driver from tacker.nfvo.drivers.vnffg import abstract_vnffg_driver +from tacker.nfvo.drivers.workflow import workflow_generator from tacker.vnfm import keystone @@ -453,6 +456,77 @@ class OpenStack_Driver(abstract_vim_driver.VimAbstractDriver, neutronclient_ = NeutronClient(auth_attr) neutronclient_.flow_classifier_delete(fc_id) + def prepare_and_create_workflow(self, resource, action, vim_auth, + kwargs, auth_token=None): + if not auth_token: + LOG.warning(_("auth token required to create mistral workflows")) + raise EnvironmentError('auth token required for' + ' mistral workflow driver') + mistral_client = MistralClient( + self.keystone.initialize_client('2', **vim_auth), + auth_token).get_client() + wg = workflow_generator.WorkflowGenerator(resource, action) + wg.task(**kwargs) + definition_yaml = yaml.dump(wg.definition) + workflow = mistral_client.workflows.create(definition_yaml) + return {'id': workflow[0].id, 'input': wg.get_input_dict()} + + def execute_workflow(self, workflow, vim_auth, auth_token=None): + if not auth_token: + LOG.warning(_("auth token required to create mistral workflows")) + raise EnvironmentError('auth token required for' + ' mistral workflow driver') + mistral_client = MistralClient( + self.keystone.initialize_client('2', **vim_auth), + auth_token).get_client() + return mistral_client.executions.create( + workflow_identifier=workflow['id'], + workflow_input=workflow['input'], + wf_params={}) + + def get_execution(self, execution_id, vim_auth, auth_token=None): + if not auth_token: + LOG.warning(_("auth token required to create mistral workflows")) + raise EnvironmentError('auth token required for' + ' mistral workflow driver') + mistral_client = MistralClient( + self.keystone.initialize_client('2', **vim_auth), + auth_token).get_client() + return mistral_client.executions.get(execution_id) + + def delete_execution(self, execution_id, vim_auth, auth_token=None): + if not auth_token: + LOG.warning(_("auth token required to create mistral workflows")) + raise EnvironmentError('auth token required for' + ' mistral workflow driver') + mistral_client = MistralClient( + self.keystone.initialize_client('2', **vim_auth), + auth_token).get_client() + return mistral_client.executions.delete(execution_id) + + def delete_workflow(self, workflow_id, vim_auth, auth_token=None): + if not auth_token: + LOG.warning(_("auth token required to create mistral workflows")) + raise EnvironmentError('auth token required for' + ' mistral workflow driver') + mistral_client = MistralClient( + self.keystone.initialize_client('2', **vim_auth), + auth_token).get_client() + return mistral_client.workflows.delete(workflow_id) + + +class MistralClient(object): + """Mistral Client class for NSD""" + + def __init__(self, keystone, auth_token): + endpoint = keystone.session.get_endpoint( + service_type='workflowv2', region_name=None) + self.client = mistral_client.client(auth_token=auth_token, + mistral_url=endpoint) + + def get_client(self): + return self.client + class NeutronClient(object): """Neutron Client class for networking-sfc driver""" diff --git a/tacker/nfvo/drivers/workflow/__init__.py b/tacker/nfvo/drivers/workflow/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/nfvo/drivers/workflow/mistral.py b/tacker/nfvo/drivers/workflow/mistral.py new file mode 100644 index 000000000..0bf45c301 --- /dev/null +++ b/tacker/nfvo/drivers/workflow/mistral.py @@ -0,0 +1,53 @@ +# 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 yaml + +from oslo_log import log as logging + +from tacker.nfvo.drivers.workflow import workflow_generator + +LOG = logging.getLogger(__name__) +FREQUENCY = 10 +SLEEP = 5 + + +class MistralClient(object): + + def __init__(self, context, client, resource, action): + self.context = context + self.client = client + self.wg = workflow_generator.WorkflowGenerator(resource, action) + + def prepare_workflow(self, **kwargs): + self.wg.task(**kwargs) + + def create_workflow(self): + definition_yaml = yaml.dump(self.wg.definition) + wf = self.client.workflows.create(definition_yaml) + wf_id = wf[0].id + return wf_id + + def delete_workflow(self, wf_id): + self.client.workflows.delete(wf_id) + + def execute_workflow(self, wf_id): + wf_ex = self.client.executions.create( + workflow_identifier=wf_id, + workflow_input=self.wg.input_dict, + wf_params={}) + return wf_ex + + def get_execution_state(self, ex_id): + return self.client.executions.get(ex_id).state + + def delete_execution(self, ex_id): + self.client.executions.delete(ex_id) diff --git a/tacker/nfvo/drivers/workflow/workflow.py b/tacker/nfvo/drivers/workflow/workflow.py new file mode 100644 index 000000000..1985f4d3a --- /dev/null +++ b/tacker/nfvo/drivers/workflow/workflow.py @@ -0,0 +1,27 @@ +# 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. + + +class Workflow(object): + def __init__(self, wf_name, wf_type, version='2.0'): + self._wf_name = wf_name + self._wf_type = wf_type + self._version = '2.0' + + def get_name(self): + return self.wf_name + + def get_type(self): + return self.wf_type + + def get_version(self): + self._version diff --git a/tacker/nfvo/drivers/workflow/workflow_generator.py b/tacker/nfvo/drivers/workflow/workflow_generator.py new file mode 100644 index 000000000..c4bf6cd8b --- /dev/null +++ b/tacker/nfvo/drivers/workflow/workflow_generator.py @@ -0,0 +1,190 @@ +# 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 ast +import uuid + +from six import iteritems + + +OUTPUT = { + 'create_vnf': ['vnf_id', 'vim_id', 'mgmt_url', 'status'] +} + + +class WorkflowGenerator(object): + def __init__(self, resource, action): + self.resource = resource + self.action = action + self.wf_name = self.action + '_' + self.resource + self.wf_identifier = 'std.' + self.wf_name + str(uuid.uuid4()) + self.task = getattr(self, self.wf_name) + self.input_dict = dict() + self._build_basic_workflow() + + def _build_basic_workflow(self): + self.definition = { + 'version': '2.0', + self.wf_identifier: { + 'type': 'direct', + 'input': [self.resource] + } + } + + def _get_vim_id(self): + pass + + def _get_vnfd_id(self): + pass + + def _get_vnf_name(self): + pass + + def _get_attr(self): + pass + + def _get_description(self): + pass + + def _add_create_vnf_tasks(self, ns): + vnfds = ns['vnfd_details'] + task_dict = dict() + for vnfd_name, vnfd_info in iteritems(vnfds): + nodes = vnfd_info['instances'] + for node in nodes: + task = self.wf_name + '_' + node + task_dict[task] = { + 'action': 'tacker.create_vnf body=<% $.vnf.{0} ' + '%>'.format(node), + 'input': {'body': '<% $.vnf.{0} %>'.format(node)}, + 'publish': { + 'vnf_id_' + node: '<% task({0}).result.vnf.id ' + '%>'.format(task), + 'vim_id_' + node: '<% task({0}).result.vnf.vim_id' + ' %>'.format(task), + 'mgmt_url_' + node: '<% task({0}).result.vnf.mgmt_url' + ' %>'.format(task), + 'status_' + node: '<% task({0}).result.vnf.status' + ' %>'.format(task), + }, + 'on-success': ['wait_vnf_active_%s' % node] + } + return task_dict + + def _add_wait_vnf_tasks(self, ns): + vnfds = ns['vnfd_details'] + task_dict = dict() + for vnfd_name, vnfd_info in iteritems(vnfds): + nodes = vnfd_info['instances'] + for node in nodes: + task = 'wait_vnf_active_%s' % node + task_dict[task] = { + 'action': 'tacker.show_vnf vnf=<% $.vnf_id_{0} ' + '%>'.format(node), + 'retry': { + 'count': 10, + 'delay': 10, + 'break-on': '<% $.status_{0} = "ACTIVE" ' + '%>'.format(node), + 'break-on': '<% $.status_{0} = "ERROR"' + ' %>'.format(node), + 'continue-on': '<% $.status_{0} = "PENDING_CREATE" ' + '%>'.format(node), + }, + 'publish': { + 'mgmt_url_' + node: ' <% task({0}).result.vnf.' + 'mgmt_url %>'.format(task), + 'status_' + node: '<% task({0}).result.vnf.status' + ' %>'.format(task), + }, + 'on-success': [ + {'delete_vnf_' + node: '<% $.status_{0}=' + '"ERROR" %>'.format(node)} + ] + } + return task_dict + + def _add_delete_vnf_tasks(self, ns): + vnfds = ns['vnfd_details'] + task_dict = dict() + for vnfd_name, vnfd_info in iteritems(vnfds): + nodes = vnfd_info['instances'] + for node in nodes: + task = 'delete_vnf_%s' % node + task_dict[task] = { + 'action': 'tacker.delete_vnf vnf=<% $.vnf_id_{0}' + '%>'.format(node), + } + return task_dict + + def _build_output_dict(self, ns): + vnfds = ns['vnfd_details'] + task_dict = dict() + for vnfd_name, vnfd_info in iteritems(vnfds): + nodes = vnfd_info['instances'] + for node in nodes: + for op_name in OUTPUT[self.wf_name]: + task_dict[op_name + '_' + node] = \ + '<% $.{0}_{1} %>'.format(op_name, node) + return task_dict + + def get_input_dict(self): + return self.input_dict + + def build_input(self, ns, params): + vnfds = ns['vnfd_details'] + id = str(uuid.uuid4()) + self.input_dict = {'vnf': {}} + for vnfd_name, vnfd_info in iteritems(vnfds): + nodes = vnfd_info['instances'] + for node in nodes: + self.input_dict['vnf'][node] = dict() + self.input_dict['vnf'][node]['vnf'] = { + 'attributes': {}, + 'vim_id': ns['ns'].get('vim_id', ''), + 'vnfd_id': vnfd_info['id'], + 'name': 'create_vnf_%s_%s' % (vnfd_info['id'], + id) + } + if params.get(vnfd_name): + self.input_dict['vnf'][node]['vnf']['attributes'] = { + 'param_values': params.get(vnfd_name) + } + + def create_vnf(self, **kwargs): + ns = kwargs.get('ns') + params = kwargs.get('params') + # TODO(anyone): Keep this statements in a loop and + # remove in all the methods. + self.definition[self.wf_identifier]['tasks'] = dict() + self.definition[self.wf_identifier]['tasks'].update( + self._add_create_vnf_tasks(ns)) + self.definition[self.wf_identifier]['tasks'].update( + self._add_wait_vnf_tasks(ns)) + self.definition[self.wf_identifier]['tasks'].update( + self._add_delete_vnf_tasks(ns)) + self.definition[self.wf_identifier]['output'] = \ + self._build_output_dict(ns) + self.build_input(ns, params) + + def delete_vnf(self, ns): + ns_dict = {'vnfd_details': {}} + vnf_ids = ast.literal_eval(ns['vnf_ids']) + self.definition[self.wf_identifier]['input'] = [] + for vnf in vnf_ids.keys(): + vnf_key = 'vnf_id_' + vnf + self.definition[self.wf_identifier]['input'].append(vnf_key) + self.input_dict[vnf_key] = vnf_ids[vnf] + ns_dict['vnfd_details'][vnf] = {'instances': [vnf]} + self.definition[self.wf_identifier]['tasks'] = dict() + self.definition[self.wf_identifier]['tasks'].update( + self._add_delete_vnf_tasks(ns_dict)) diff --git a/tacker/nfvo/nfvo_plugin.py b/tacker/nfvo/nfvo_plugin.py index 4f316907d..d381e8f25 100644 --- a/tacker/nfvo/nfvo_plugin.py +++ b/tacker/nfvo/nfvo_plugin.py @@ -21,10 +21,12 @@ import uuid import yaml from cryptography import fernet +import eventlet from oslo_config import cfg from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import strutils +from six import iteritems from tempfile import mkstemp from toscaparser.tosca_template import ToscaTemplate @@ -37,15 +39,19 @@ 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 common_services as cs from tacker.extensions import nfvo from tacker import manager from tacker.plugins.common import constants from tacker.vnfm.tosca import utils as toscautils +from tacker.vnfm import vim_client from toscaparser import tosca_template LOG = logging.getLogger(__name__) CONF = cfg.CONF +MISTRAL_RETRIES = 30 +MISTRAL_RETRY_WAIT = 6 def config_opts(): @@ -76,10 +82,12 @@ class NfvoPlugin(nfvo_db.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, def __init__(self): super(NfvoPlugin, self).__init__() + self._pool = eventlet.GreenPool() self._vim_drivers = driver_manager.DriverManager( 'tacker.nfvo.vim.drivers', cfg.CONF.nfvo_vim.vim_drivers) self._created_vims = dict() + self.vim_client = vim_client.VimClient() context = t_context.get_admin_context() vims = self.get_vims(context) for vim in vims: @@ -93,6 +101,9 @@ class NfvoPlugin(nfvo_db.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, for created_vim in self._created_vims.values(): self.monitor_vim(created_vim) + def spawn_n(self, function, *args, **kwargs): + self._pool.spawn_n(function, *args, **kwargs) + @log.log def create_vim(self, context, vim): LOG.debug(_('Create vim called with parameters %s'), @@ -480,3 +491,231 @@ class NfvoPlugin(nfvo_db.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin, 'template_name', '') LOG.debug(_('nsd %s'), nsd) + + def _get_vnfd_id(self, vnfd_name, onboarded_vnfds): + for vnfd in onboarded_vnfds: + if vnfd_name == vnfd['name']: + return vnfd['id'] + + @log.log + def create_ns(self, context, ns): + """Create NS and corresponding VNFs. + + :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 + """ + nsd = self.get_nsd(context, ns['ns']['nsd_id']) + nsd_dict = yaml.load(nsd['attributes']['nsd']) + vnfm_plugin = manager.TackerManager.get_service_plugins()['VNFM'] + onboarded_vnfds = vnfm_plugin.get_vnfds(context, []) + region_name = ns.setdefault('placement_attr', {}).get( + 'region_name', None) + vim_res = self.vim_client.get_vim(context, ns['ns']['vim_id'], + region_name) + driver_type = vim_res['vim_type'] + if not ns['ns']['vim_id']: + ns['ns']['vim_id'] = vim_res['vim_id'] + if self._get_by_name(context, ns_db.NS, ns['ns']['name']): + raise exceptions.DuplicateResourceName(resource='NS', + name=ns['ns']['name']) + + # Step-1 + param_values = ns['ns']['attributes'].get('param_values', {}) + if 'get_input' in str(nsd_dict): + self._process_parameterized_input(ns['ns']['attributes'], + nsd_dict) + # Step-2 + vnfds = nsd['vnfds'] + # vnfd_dict is used while generating workflow + vnfd_dict = dict() + for node_name, node_val in \ + iteritems(nsd_dict['topology_template']['node_templates']): + if node_val.get('type') not in vnfds.keys(): + continue + vnfd_name = vnfds[node_val.get('type')] + if not vnfd_dict.get(vnfd_name): + vnfd_dict[vnfd_name] = { + 'id': self._get_vnfd_id(vnfd_name, onboarded_vnfds), + 'instances': [node_name] + } + else: + vnfd_dict[vnfd_name]['instances'].append(node_name) + if not node_val.get('requirements'): + continue + if not param_values.get(vnfd_name): + param_values[vnfd_name] = {} + param_values[vnfd_name]['substitution_mappings'] = dict() + req_dict = dict() + requirements = node_val.get('requirements') + for requirement in requirements: + req_name = list(requirement.keys())[0] + req_val = list(requirement.values())[0] + res_name = req_val + ns['ns']['nsd_id'][:11] + req_dict[req_name] = res_name + if req_val in nsd_dict['topology_template']['node_templates']: + param_values[vnfd_name]['substitution_mappings'][ + res_name] = nsd_dict['topology_template'][ + 'node_templates'][req_val] + + param_values[vnfd_name]['substitution_mappings'][ + 'requirements'] = req_dict + ns['vnfd_details'] = vnfd_dict + # Step-3 + kwargs = {'ns': ns, 'params': param_values} + workflow = self._vim_drivers.invoke(driver_type, + 'prepare_and_create_workflow', + resource='vnf', + action='create', + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token, + kwargs=kwargs) + try: + mistral_execution = self._vim_drivers.invoke( + driver_type, + 'execute_workflow', + workflow=workflow, + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token) + except Exception as ex: + raise ex + ns_dict = super(NfvoPlugin, self).create_ns(context, ns) + + def _create_ns_wait(self_obj, ns_id, execution_id): + exec_state = "RUNNING" + mistral_retries = MISTRAL_RETRIES + while exec_state == "RUNNING" and mistral_retries > 0: + time.sleep(MISTRAL_RETRY_WAIT) + exec_state = self._vim_drivers.invoke( + driver_type, + 'get_execution', + execution_id=execution_id, + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token).state + LOG.debug(_('status: %s'), exec_state) + if exec_state == 'SUCCESS' or exec_state == 'ERROR': + break + mistral_retries = mistral_retries - 1 + error_reason = None + if mistral_retries == 0 and exec_state == 'RUNNING': + error_reason = _("NS creation is not completed within" + " {wait} seconds as creation of mistral" + " exection {mistral} is not completed").format( + wait=MISTRAL_RETRIES * MISTRAL_RETRY_WAIT, + mistral=execution_id) + exec_obj = self._vim_drivers.invoke(driver_type, + 'get_execution', + execution_id=execution_id, + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token) + self._vim_drivers.invoke(driver_type, + 'delete_execution', + execution_id=execution_id, + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token) + self._vim_drivers.invoke(driver_type, + 'delete_workflow', + workflow_id=workflow['id'], + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token) + super(NfvoPlugin, self).create_ns_post(context, ns_id, exec_obj, + vnfd_dict, error_reason) + + self.spawn_n(_create_ns_wait, self, ns_dict['id'], + mistral_execution.id) + return ns_dict + + @log.log + def _update_params(self, original, paramvalues): + for key, value in iteritems(original): + if not isinstance(value, dict) or 'get_input' not in str(value): + pass + elif isinstance(value, dict): + if '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 cs.InputValuesMissing(key=key) + else: + self._update_params(value, paramvalues) + + @log.log + def _process_parameterized_input(self, attrs, nsd_dict): + param_vattrs_dict = attrs.pop('param_values', None) + if param_vattrs_dict: + for node in \ + nsd_dict['topology_template']['node_templates'].values(): + if 'get_input' in str(node): + self._update_params(node, param_vattrs_dict['nsd']) + else: + raise cs.ParamYAMLInputMissing() + + @log.log + def delete_ns(self, context, ns_id): + ns = super(NfvoPlugin, self).get_ns(context, ns_id) + vim_res = self.vim_client.get_vim(context, ns['vim_id']) + driver_type = vim_res['vim_type'] + workflow = self._vim_drivers.invoke(driver_type, + 'prepare_and_create_workflow', + resource='vnf', + action='delete', + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token, + kwargs={'ns': ns}) + try: + mistral_execution = self._vim_drivers.invoke( + driver_type, + 'execute_workflow', + workflow=workflow, + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token) + except Exception as ex: + raise ex + super(NfvoPlugin, self).delete_ns(context, ns_id) + + def _delete_ns_wait(ns_id, execution_id): + exec_state = "RUNNING" + mistral_retries = MISTRAL_RETRIES + while exec_state == "RUNNING" and mistral_retries > 0: + time.sleep(MISTRAL_RETRY_WAIT) + exec_state = self._vim_drivers.invoke( + driver_type, + 'get_execution', + execution_id=execution_id, + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token).state + LOG.debug(_('status: %s'), exec_state) + if exec_state == 'SUCCESS' or exec_state == 'ERROR': + break + mistral_retries -= 1 + error_reason = None + if mistral_retries == 0 and exec_state == 'RUNNING': + error_reason = _("NS deletion is not completed within" + " {wait} seconds as deletion of mistral" + " exection {mistral} is not completed").format( + wait=MISTRAL_RETRIES * MISTRAL_RETRY_WAIT, + mistral=execution_id) + exec_obj = self._vim_drivers.invoke(driver_type, + 'get_execution', + execution_id=execution_id, + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token) + self._vim_drivers.invoke(driver_type, + 'delete_execution', + execution_id=execution_id, + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token) + self._vim_drivers.invoke(driver_type, + 'delete_workflow', + workflow_id=workflow['id'], + vim_auth=vim_res['vim_auth'], + auth_token=context.auth_token) + super(NfvoPlugin, self).delete_ns_post(context, ns_id, exec_obj, + error_reason) + + self.spawn_n(_delete_ns_wait, ns['id'], mistral_execution.id) + return ns['id'] diff --git a/tacker/plugins/common/constants.py b/tacker/plugins/common/constants.py index 6af1471a2..b4008c773 100644 --- a/tacker/plugins/common/constants.py +++ b/tacker/plugins/common/constants.py @@ -57,6 +57,7 @@ DEFAULT_ALARM_ACTIONS = ['respawn', 'log', 'log_and_kill', 'notify'] RES_TYPE_VNFD = "vnfd" RES_TYPE_NSD = "nsd" +RES_TYPE_NS = "ns" RES_TYPE_VNF = "vnf" RES_TYPE_VIM = "vim" diff --git a/tacker/tests/constants.py b/tacker/tests/constants.py index 0030f5e88..8836b9207 100644 --- a/tacker/tests/constants.py +++ b/tacker/tests/constants.py @@ -18,3 +18,5 @@ VNF_CIRROS_DEAD_TIMEOUT = 250 ACTIVE_SLEEP_TIME = 3 DEAD_SLEEP_TIME = 1 SCALE_WINDOW_SLEEP_TIME = 120 +NS_CREATE_TIMEOUT = 400 +NS_DELETE_TIMEOUT = 300 diff --git a/tacker/tests/etc/samples/test-ns-nsd.yaml b/tacker/tests/etc/samples/test-ns-nsd.yaml new file mode 100644 index 000000000..425b36d0d --- /dev/null +++ b/tacker/tests/etc/samples/test-ns-nsd.yaml @@ -0,0 +1,37 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 +imports: + - test-ns-vnfd1 + - test-ns-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 + 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/etc/samples/test-ns-vnfd1.yaml b/tacker/tests/etc/samples/test-ns-vnfd1.yaml new file mode 100644 index 000000000..f18e06262 --- /dev/null +++ b/tacker/tests/etc/samples/test-ns-vnfd1.yaml @@ -0,0 +1,98 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 + +description: Demo example +node_types: + tosca.nodes.nfv.VNF1: + requirements: + - virtualLink1: + type: tosca.nodes.nfv.VL + required: true + - virtualLink2: + type: tosca.nodes.nfv.VL + required: true + capabilities: + forwader1: + type: tosca.capabilities.nfv.Forwarder + forwader2: + type: tosca.capabilities.nfv.Forwarder + +topology_template: + substitution_mappings: + node_type: tosca.nodes.nfv.VNF1 + requirements: + virtualLink1: [CP11, virtualLink] + virtualLink2: [CP14, virtualLink] + capabilities: + forwarder1: [CP11, forwarder] + forwarder2: [CP14, forwarder] + + node_templates: + VDU1: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.4-x86_64-uec + flavor: m1.tiny + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + + 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 + + VDU2: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.4-x86_64-uec + flavor: m1.medium + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + + CP13: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + requirements: + - virtualLink: + node: VL1 + - virtualBinding: + node: VDU2 + + CP14: + type: tosca.nodes.nfv.CP.Tacker + requirements: + - virtualBinding: + node: VDU2 + + 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/tacker/tests/etc/samples/test-ns-vnfd2.yaml b/tacker/tests/etc/samples/test-ns-vnfd2.yaml new file mode 100644 index 000000000..99e22839d --- /dev/null +++ b/tacker/tests/etc/samples/test-ns-vnfd2.yaml @@ -0,0 +1,68 @@ +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.4-x86_64-uec + flavor: m1.tiny + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + + CP21: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + anti_spoofing_protection: false + requirements: + - virtualLink: + node: VL1 + - virtualBinding: + node: VDU1 + + VDU2: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.4-x86_64-uec + flavor: m1.medium + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + + CP22: + type: tosca.nodes.nfv.CP.Tacker + requirements: + - virtualLink: + node: VL2 + - virtualBinding: + node: VDU2 + + 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/tacker/tests/etc/samples/test-nsd-vnfd1.yaml b/tacker/tests/etc/samples/test-nsd-vnfd1.yaml new file mode 100644 index 000000000..f18e06262 --- /dev/null +++ b/tacker/tests/etc/samples/test-nsd-vnfd1.yaml @@ -0,0 +1,98 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 + +description: Demo example +node_types: + tosca.nodes.nfv.VNF1: + requirements: + - virtualLink1: + type: tosca.nodes.nfv.VL + required: true + - virtualLink2: + type: tosca.nodes.nfv.VL + required: true + capabilities: + forwader1: + type: tosca.capabilities.nfv.Forwarder + forwader2: + type: tosca.capabilities.nfv.Forwarder + +topology_template: + substitution_mappings: + node_type: tosca.nodes.nfv.VNF1 + requirements: + virtualLink1: [CP11, virtualLink] + virtualLink2: [CP14, virtualLink] + capabilities: + forwarder1: [CP11, forwarder] + forwarder2: [CP14, forwarder] + + node_templates: + VDU1: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.4-x86_64-uec + flavor: m1.tiny + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + + 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 + + VDU2: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.4-x86_64-uec + flavor: m1.medium + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + + CP13: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + requirements: + - virtualLink: + node: VL1 + - virtualBinding: + node: VDU2 + + CP14: + type: tosca.nodes.nfv.CP.Tacker + requirements: + - virtualBinding: + node: VDU2 + + 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/tacker/tests/etc/samples/test-nsd-vnfd2.yaml b/tacker/tests/etc/samples/test-nsd-vnfd2.yaml new file mode 100644 index 000000000..99e22839d --- /dev/null +++ b/tacker/tests/etc/samples/test-nsd-vnfd2.yaml @@ -0,0 +1,68 @@ +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.4-x86_64-uec + flavor: m1.tiny + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + + CP21: + type: tosca.nodes.nfv.CP.Tacker + properties: + management: true + anti_spoofing_protection: false + requirements: + - virtualLink: + node: VL1 + - virtualBinding: + node: VDU1 + + VDU2: + type: tosca.nodes.nfv.VDU.Tacker + properties: + image: cirros-0.3.4-x86_64-uec + flavor: m1.medium + availability_zone: nova + mgmt_driver: noop + config: | + param0: key1 + param1: key2 + + CP22: + type: tosca.nodes.nfv.CP.Tacker + requirements: + - virtualLink: + node: VL2 + - virtualBinding: + node: VDU2 + + 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/tacker/tests/etc/samples/test-nsd.yaml b/tacker/tests/etc/samples/test-nsd.yaml new file mode 100644 index 000000000..82a93b9eb --- /dev/null +++ b/tacker/tests/etc/samples/test-nsd.yaml @@ -0,0 +1,37 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 +imports: + - test-nsd-vnfd1 + - test-nsd-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 + 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/functional/nfvo/test_nfvo.py b/tacker/tests/functional/nfvo/test_nfvo.py new file mode 100644 index 000000000..5380d8ed0 --- /dev/null +++ b/tacker/tests/functional/nfvo/test_nfvo.py @@ -0,0 +1,150 @@ +# Copyright 2016 Brocade Communications System, Inc. +# +# 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 yaml + +from oslo_config import cfg +from tackerclient.common import exceptions + +from tacker.plugins.common import constants as evt_constants +from tacker.tests import constants +from tacker.tests.functional import base +from tacker.tests.utils import read_file + +import time +CONF = cfg.CONF + + +class NsdTestCreate(base.BaseTackerTest): + def _test_create_tosca_vnfd(self, tosca_vnfd_file, vnfd_name): + input_yaml = read_file(tosca_vnfd_file) + tosca_dict = yaml.safe_load(input_yaml) + tosca_arg = {'vnfd': {'name': vnfd_name, + 'attributes': {'vnfd': tosca_dict}}} + vnfd_instance = self.client.create_vnfd(body=tosca_arg) + self.assertEqual(vnfd_instance['vnfd']['name'], vnfd_name) + self.assertIsNotNone(vnfd_instance) + + vnfds = self.client.list_vnfds().get('vnfds') + self.assertIsNotNone(vnfds, "List of vnfds are Empty after Creation") + return vnfd_instance['vnfd']['id'] + + def _test_create_nsd(self, tosca_nsd_file, nsd_name): + input_yaml = read_file(tosca_nsd_file) + tosca_dict = yaml.safe_load(input_yaml) + tosca_arg = {'nsd': {'name': nsd_name, + 'attributes': {'nsd': tosca_dict}}} + nsd_instance = self.client.create_nsd(body=tosca_arg) + self.assertIsNotNone(nsd_instance) + return nsd_instance['nsd']['id'] + + def _test_delete_nsd(self, nsd_id): + try: + self.client.delete_nsd(nsd_id) + except Exception: + assert False, "nsd Delete failed" + + def _test_delete_vnfd(self, vnfd_id, timeout=constants.NS_DELETE_TIMEOUT): + start_time = int(time.time()) + while True: + try: + self.client.delete_vnfd(vnfd_id) + except exceptions.Conflict: + time.sleep(2) + except Exception: + assert False, "vnfd Delete failed" + else: + break + if (int(time.time()) - start_time) > timeout: + assert False, "vnfd still in use" + self.verify_vnfd_events(vnfd_id, evt_constants.RES_EVT_DELETE, + evt_constants.RES_EVT_NA_STATE) + + def _wait_until_ns_status(self, ns_id, target_status, timeout, + sleep_interval): + start_time = int(time.time()) + while True: + ns_result = self.client.show_ns(ns_id) + status = ns_result['ns']['status'] + if (status == target_status) or ( + (int(time.time()) - start_time) > timeout): + break + time.sleep(sleep_interval) + + self.assertEqual(status, target_status, + "ns %(ns_id)s with status %(status)s is" + " expected to be %(target)s" % + {"ns_id": ns_id, "status": status, + "target": target_status}) + + def _wait_until_ns_delete(self, ns_id, timeout): + start_time = int(time.time()) + while True: + try: + ns_result = self.client.show_ns(ns_id) + time.sleep(2) + except Exception: + return + status = ns_result['ns']['status'] + if (status != 'PENDING_DELETE') or (( + int(time.time()) - start_time) > timeout): + raise Exception("Failed with status: %s" % status) + + def _test_create_delete_ns(self, nsd_file, ns_name): + vnfd1_id = self._test_create_tosca_vnfd( + 'test-ns-vnfd1.yaml', + 'test-ns-vnfd1') + vnfd2_id = self._test_create_tosca_vnfd( + 'test-ns-vnfd2.yaml', + 'test-ns-vnfd2') + nsd_id = self._test_create_nsd( + nsd_file, + 'test-ns-nsd') + ns_arg = {'ns': {'nsd_id': nsd_id, 'name': ns_name, + 'attributes': {"param_values": {"nsd": + {"vl2_name": "net0", + "vl1_name": "net_mgmt"}}}}} + ns_instance = self.client.create_ns(body=ns_arg) + ns_id = ns_instance['ns']['id'] + self._wait_until_ns_status(ns_id, 'ACTIVE', + constants.NS_CREATE_TIMEOUT, + constants.ACTIVE_SLEEP_TIME) + ns_show_out = self.client.show_ns(ns_id)['ns'] + self.assertIsNotNone(ns_show_out['mgmt_urls']) + try: + self.client.delete_ns(ns_id) + except Exception: + assert False, "ns Delete failed" + self._wait_until_ns_delete(ns_id, constants.NS_DELETE_TIMEOUT) + self._test_delete_nsd(nsd_id) + self._test_delete_vnfd(vnfd1_id) + self._test_delete_vnfd(vnfd2_id) + + def test_create_delete_nsd(self): + vnfd1_id = self._test_create_tosca_vnfd( + 'test-nsd-vnfd1.yaml', + 'test-nsd-vnfd1') + vnfd2_id = self._test_create_tosca_vnfd( + 'test-nsd-vnfd2.yaml', + 'test-nsd-vnfd2') + nsd_id = self._test_create_nsd( + 'test-nsd.yaml', + 'test-nsd') + self._test_delete_nsd(nsd_id) + self._test_delete_vnfd(vnfd1_id) + self._test_delete_vnfd(vnfd2_id) + + def test_create_delete_network_service(self): + self._test_create_delete_ns('test-ns-nsd.yaml', + 'test-ns') diff --git a/tacker/tests/unit/db/utils.py b/tacker/tests/unit/db/utils.py index 33c1116e1..f8b15377a 100644 --- a/tacker/tests/unit/db/utils.py +++ b/tacker/tests/unit/db/utils.py @@ -186,3 +186,15 @@ def get_dummy_nsd_obj(): 'tenant_id': u'8819a1542a5948b68f94d4be0fd50496', 'template': {}, 'attributes': {u'nsd': nsd_tosca_template}}} + + +def get_dummy_ns_obj(): + return {'ns': {'description': 'dummy_ns_description', + 'id': u'ba6bf017-f6f7-45f1-a280-57b073bf78ea', + 'nsd_id': u'eb094833-995e-49f0-a047-dfb56aaf7c4e', + 'vim_id': u'6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', + 'name': 'dummy_ns', + 'attributes': { + 'param_values': {'nsd': {'vl1_name': 'net_mgmt', + 'vl2_name': 'net0'}}}}} diff --git a/tacker/tests/unit/vm/nfvo/drivers/workflow/__init__.py b/tacker/tests/unit/vm/nfvo/drivers/workflow/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/tests/unit/vm/nfvo/drivers/workflow/test_mistral.py b/tacker/tests/unit/vm/nfvo/drivers/workflow/test_mistral.py new file mode 100644 index 000000000..fffaa4dca --- /dev/null +++ b/tacker/tests/unit/vm/nfvo/drivers/workflow/test_mistral.py @@ -0,0 +1,159 @@ +# 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. + +from tacker import context +from tacker.nfvo.drivers.workflow import mistral +from tacker.tests.unit import base + + +def get_dummy_ns(): + return {u'ns': {'description': '', + 'tenant_id': u'a81900a92bda40588c52699e1873a92f', + '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': {}} + + +def get_dummy_param(): + return {u'vnf1': {'substitution_mappings': {u'VL1b8587afb-60': { + 'type': 'tosca.nodes.nfv.VL', 'properties': { + 'network_name': u'net_mgmt', + 'vendor': 'tacker'}}, 'requirements': { + 'virtualLink2': u'VL2b8587afb-60', + 'virtualLink1': u'VL1b8587afb-60'}, u'VL2b8587afb-60': { + 'type': 'tosca.nodes.nfv.VL', + 'properties': {'network_name': u'net0', + 'vendor': 'tacker'}}}}, + u'nsd': {u'vl2_name': u'net0', u'vl1_name': u'net_mgmt'}} + + +def get_dummy_create_workflow(): + return {'std.create_vnf_dummy': {'input': ['vnf'], + '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_vnf_VNF2': { + 'action': 'tacker.create_vnf body=<% $.vnf.VNF2 %>', + 'input': {'body': '<% $.vnf.VNF2 %>'}, + 'publish': { + 'status_VNF2': '<% task(create_vnf_VNF2).' + 'result.vnf.status %>', + 'vim_id_VNF2': '<% task(create_vnf_VNF2).' + 'result.vnf.vim_id %>', + 'mgmt_url_VNF2': '<% task(create_vnf_VNF2).' + 'result.vnf.mgmt_url %>', + 'vnf_id_VNF2': '<% task(create_vnf_VNF2)' + '.result.vnf.id %>'}, + 'on-success': ['wait_vnf_active_VNF2']}, + 'create_vnf_VNF1': { + 'action': 'tacker.create_vnf body=<% $.vnf.VNF1 %>', + 'input': {'body': '<% $.vnf.VNF1 %>'}, + 'publish': { + 'status_VNF1': '<% task(create_vnf_VNF1).' + 'result.vnf.status %>', + 'vnf_id_VNF1': '<% task(create_vnf_VNF1).' + 'result.vnf.id %>', + 'mgmt_url_VNF1': '<% task(create_vnf_VNF1).' + 'result.vnf.mgmt_url %>', + 'vim_id_VNF1': '<% task(create_vnf_VNF1).' + 'result.vnf.vim_id %>'}, + 'on-success': ['wait_vnf_active_VNF1']}, + '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" %>'}]}, + '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 %>', + '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'}"} + + +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%>'}}, + 'type': 'direct'}} + + +class FakeMistral(object): + def __init__(self): + pass + + +class TestMistralClient(base.TestCase): + def setUp(self): + super(TestMistralClient, self).setUp() + self.mistral_client = FakeMistral() + + def test_prepare_workflow_create(self): + mc = mistral.MistralClient(context, self.mistral_client, + resource='vnf', action='create') + mc.prepare_workflow(ns=get_dummy_ns(), params=get_dummy_param()) + wf_def_values = [mc.wg.definition[k] for k in mc.wg.definition] + self.assertIn(get_dummy_create_workflow()['std.create_vnf_dummy'], + wf_def_values) + self.assertEqual(get_dummy_create_workflow()['version'], + mc.wg.definition['version']) + + def test_prepare_workflow_delete(self): + mc = mistral.MistralClient(context, self.mistral_client, + resource='vnf', action='delete') + mc.prepare_workflow(ns=dummy_delete_ns_obj()) + wf_def_values = [mc.wg.definition[k] for k in mc.wg.definition] + self.assertIn(get_dummy_delete_workflow()['std.delete_vnf_dummy'], + wf_def_values) + self.assertEqual(get_dummy_delete_workflow()['version'], + mc.wg.definition['version']) diff --git a/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py b/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py index a8a855c76..7f99a0ed4 100644 --- a/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py +++ b/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py @@ -13,7 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. +import codecs +import datetime import mock +import os import uuid @@ -23,6 +26,7 @@ from tacker.common import exceptions from tacker import context from tacker.db.common_services import common_services_db 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.manager import TackerManager @@ -30,6 +34,7 @@ from tacker.nfvo import nfvo_plugin from tacker.plugins.common import constants from tacker.tests.unit.db import base as db_base from tacker.tests.unit.db import utils +from tacker.vnfm import vim_client SECRET_PASSWORD = '***' @@ -41,11 +46,66 @@ def dummy_get_vim(*args, **kwargs): return vim_obj +def _get_template(name): + filename = os.path.abspath(os.path.join(os.path.dirname(__file__), + '../../../etc/samples/' + str(name))) + f = codecs.open(filename, encoding='utf-8', errors='strict') + return f.read() + + class FakeDriverManager(mock.Mock): def invoke(self, *args, **kwargs): if any(x in ['create', 'create_chain', 'create_flow_classifier'] for x in args): return str(uuid.uuid4()) + elif 'execute_workflow' in args: + mock_execution = mock.Mock() + mock_execution.id.return_value = \ + "ba6bf017-f6f7-45f1-a280-57b073bf78ea" + return mock_execution + + +def get_fake_nsd(): + create_time = datetime.datetime(2017, 1, 19, 9, 2, 11) + return {'description': u'', + 'tenant_id': u'a81900a92bda40588c52699e1873a92f', + 'created_at': create_time, 'updated_at': None, + 'vnfds': {u'tosca.nodes.nfv.VNF1': u'vnf1', + u'tosca.nodes.nfv.VNF2': u'vnf2'}, + 'attributes': {u'nsd': u'imports: [tinku1, tinku2]\ntopology_' + 'template:\n inputs:\n vl1_name: {default: net_mgmt, ' + 'description: name of VL1 virtuallink, type: string}\n ' + 'vl2_name: {default: net0, description: name of VL2 virtuallink, ' + 'type: string}\n node_templates:\n VL1:\n properties:\n' + ' network_name: {get_input: vl1_name}\n vendor: ' + 'tacker\n type: tosca.nodes.nfv.VL\n VL2:\n ' + 'properties:\n network_name: {get_input: vl2_name}\n ' + ' vendor: tacker\n type: tosca.nodes.nfv.VL\n VNF1:\n' + ' requirements:\n - {virtualLink1: VL1}\n - {' + 'virtualLink2: VL2}\n type: tosca.nodes.nfv.VNF1\n VNF2: ' + '{type: tosca.nodes.nfv.VNF2}\ntosca_definitions_version: tosca_' + 'simple_profile_for_nfv_1_0_0\n'}, + 'id': u'eb094833-995e-49f0-a047-dfb56aaf7c4e', 'name': u'nsd'} + + +def get_by_name(): + return False + + +def dummy_get_vim_auth(*args, **kwargs): + return {'vim_auth': {u'username': u'admin', 'password': 'devstack', + u'project_name': u'nfv', u'user_id': u'', + u'user_domain_name': u'Default', + u'auth_url': u'http://10.0.4.207/identity/v3', + u'project_id': u'', + u'project_domain_name': u'Default'}, + 'vim_id': u'96025dd5-ca16-49f3-9823-958eb04260c4', + 'vim_type': u'openstack', 'vim_name': u'VIM0'} + + +class FakeClient(mock.Mock): + def __init__(self, auth): + pass class FakeVNFMPlugin(mock.Mock): @@ -55,6 +115,7 @@ class FakeVNFMPlugin(mock.Mock): self.vnf1_vnfd_id = 'eb094833-995e-49f0-a047-dfb56aaf7c4e' self.vnf1_vnf_id = '91e32c20-6d1f-47a4-9ba7-08f5e5effe07' self.vnf3_vnfd_id = 'e4015e9f-1ef2-49fb-adb6-070791ad3c45' + self.vnf2_vnfd_id = 'e4015e9f-1ef2-49fb-adb6-070791ad3c45' self.vnf3_vnf_id = '7168062e-9fa1-4203-8cb7-f5c99ff3ee1b' self.vnf3_update_vnf_id = '10f66bc5-b2f1-45b7-a7cd-6dd6ad0017f5' @@ -63,6 +124,18 @@ class FakeVNFMPlugin(mock.Mock): self.cp32_id = '3d1bd2a2-bf0e-44d1-87af-a2c6b2cad3ed' self.cp32_update_id = '064c0d99-5a61-4711-9597-2a44dc5da14b' + def get_vnfd(self, *args, **kwargs): + if 'VNF1' in args: + return {'id': self.vnf1_vnfd_id, + 'name': 'VNF1', + 'attributes': {'vnfd': _get_template( + 'test-nsd-vnfd1.yaml')}} + elif 'VNF2' in args: + return {'id': self.vnf3_vnfd_id, + 'name': 'VNF2', + 'attributes': {'vnfd': _get_template( + 'test-nsd-vnfd2.yaml')}} + def get_vnfds(self, *args, **kwargs): if {'name': ['VNF1']} in args: return [{'id': self.vnf1_vnfd_id}] @@ -433,12 +506,99 @@ class TestNfvoPlugin(db_base.SqlTestCase): 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) + def _insert_dummy_ns_template(self): + session = self.context.session + attributes = { + u'nsd': 'imports: [VNF1, VNF2]\ntopology_template:\n inputs:\n ' + ' vl1_name: {default: net_mgmt, description: name of VL1' + ' virtuallink, type: string}\n vl2_name: {default: ' + 'net0, description: name of VL2 virtuallink, type: string' + '}\n node_templates:\n VL1:\n properties:\n ' + ' network_name: {get_input: vl1_name}\n vendor: ' + 'tacker\n type: tosca.nodes.nfv.VL\n VL2:\n ' + 'properties:\n network_name: {get_input: vl2_name}' + '\n vendor: tacker\n type: tosca.nodes.nfv.VL' + '\n VNF1:\n requirements:\n - {virtualLink1: ' + 'VL1}\n - {virtualLink2: VL2}\n type: tosca.node' + 's.nfv.VNF1\n VNF2: {type: tosca.nodes.nfv.VNF2}\ntosca' + '_definitions_version: tosca_simple_profile_for_nfv_1_0_0' + '\n'} + nsd_template = ns_db.NSD( + id='eb094833-995e-49f0-a047-dfb56aaf7c4e', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + name='fake_template', + vnfds={'tosca.nodes.nfv.VNF1': 'vnf1', + 'tosca.nodes.nfv.VNF2': 'vnf2'}, + description='fake_nsd_template_description') + session.add(nsd_template) + for (key, value) in attributes.items(): + attribute_db = ns_db.NSDAttribute( + id=str(uuid.uuid4()), + nsd_id='eb094833-995e-49f0-a047-dfb56aaf7c4e', + key=key, + value=value) + session.add(attribute_db) + session.flush() + return nsd_template + + def _insert_dummy_ns(self): + session = self.context.session + ns = ns_db.NS( + id='ba6bf017-f6f7-45f1-a280-57b073bf78ea', + name='fake_ns', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + status='ACTIVE', + nsd_id='eb094833-995e-49f0-a047-dfb56aaf7c4e', + vim_id='6261579e-d6f3-49ad-8bc3-a9cb974778ff', + description='fake_ns_description') + session.add(ns) + session.flush() + return ns + + 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) + self.assertEqual(result['name'], 'dummy_NSD') + + @mock.patch.object(vim_client.VimClient, 'get_vim') + @mock.patch.object(nfvo_plugin.NfvoPlugin, '_get_by_name') + def test_create_ns(self, mock_get_by_name, mock_get_vim): + self._insert_dummy_ns_template() + self._insert_dummy_vim() + 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() + mock_get_by_name.return_value = get_by_name() + + ns_obj = utils.get_dummy_ns_obj() + result = self.nfvo_plugin.create_ns(self.context, ns_obj) + self.assertIsNotNone(result) + self.assertIn('id', result) + self.assertEqual(ns_obj['ns']['nsd_id'], result['nsd_id']) + self.assertEqual(ns_obj['ns']['name'], result['name']) + self.assertIn('status', result) + self.assertIn('tenant_id', result) + + @mock.patch.object(vim_client.VimClient, 'get_vim') + @mock.patch.object(nfvo_plugin.NfvoPlugin, '_get_by_name') + def test_delete_ns(self, mock_get_by_name, mock_get_vim): + self._insert_dummy_vim() + self._insert_dummy_ns_template() + self._insert_dummy_ns() + 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() + mock_get_by_name.return_value = get_by_name() + result = self.nfvo_plugin.delete_ns(self.context, + 'ba6bf017-f6f7-45f1-a280-57b073bf78ea') + self.assertIsNotNone(result) diff --git a/tacker/tests/unit/vm/test_toscautils.py b/tacker/tests/unit/vm/test_toscautils.py index 5f923639b..242dd8bc9 100644 --- a/tacker/tests/unit/vm/test_toscautils.py +++ b/tacker/tests/unit/vm/test_toscautils.py @@ -229,3 +229,18 @@ class TestToscaUtils(testtools.TestCase): toscautils.convert_unsupported_res_prop(dummy_heat_dict, unsupported_res_prop_dict) self.assertEqual(expected_heat_dict, dummy_heat_dict) + + def test_check_for_substitution_mappings(self): + tosca_sb_map = _get_template('../../../../../etc/samples/test-nsd-' + 'vnfd1.yaml') + param = {'substitution_mappings': { + 'VL2': {'type': 'tosca.nodes.nfv.VL', 'properties': { + 'network_name': 'net0', 'vendor': 'tacker'}}, + 'VL1': {'type': 'tosca.nodes.nfv.VL', 'properties': { + 'network_name': 'net_mgmt', 'vendor': 'tacker'}}, + 'requirements': {'virtualLink2': 'VL2', + 'virtualLink1': 'VL1'}}} + template = yaml.load(tosca_sb_map) + toscautils.updateimports(template) + toscautils.check_for_substitution_mappings(template, param) + self.assertNotIn('substitution_mappings', param) diff --git a/tacker/vnfm/infra_drivers/openstack/openstack.py b/tacker/vnfm/infra_drivers/openstack/openstack.py index e479fb522..a53d747a0 100644 --- a/tacker/vnfm/infra_drivers/openstack/openstack.py +++ b/tacker/vnfm/infra_drivers/openstack/openstack.py @@ -280,7 +280,7 @@ class OpenStack(abstract_driver.DeviceAbstractDriver, stack_status=status) LOG.warning(error_reason) raise vnfm.VNFCreateWaitFailed(vnf_id=vnf_id, - eason=error_reason) + reason=error_reason) @classmethod def _find_mgmt_ips_from_groups(cls, heat_client, instance_id, group_names): diff --git a/tacker/vnfm/infra_drivers/openstack/translate_template.py b/tacker/vnfm/infra_drivers/openstack/translate_template.py index 63b8408f1..5e075a11d 100644 --- a/tacker/vnfm/infra_drivers/openstack/translate_template.py +++ b/tacker/vnfm/infra_drivers/openstack/translate_template.py @@ -21,6 +21,7 @@ from translator.hot import tosca_translator import yaml from tacker.common import log +from tacker.extensions import common_services as cs from tacker.extensions import vnfm from tacker.vnfm.tosca import utils as toscautils @@ -165,13 +166,13 @@ class TOSCAToHOT(object): self._update_params(value, paramvalues[key], False) else: LOG.debug('Key missing Value: %s', key) - raise vnfm.InputValuesMissing(key=key) + raise cs.InputValuesMissing(key=key) 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(key=key) + raise cs.InputValuesMissing(key=key) else: self._update_params(value, paramvalues, True) @@ -189,7 +190,7 @@ class TOSCAToHOT(object): else: self._update_params(vnfd_dict, param_vattrs_dict) else: - raise vnfm.ParamYAMLInputMissing() + raise cs.ParamYAMLInputMissing() @log.log def _process_vdu_network_interfaces(self, vdu_id, vdu_dict, properties, diff --git a/tacker/vnfm/tosca/utils.py b/tacker/vnfm/tosca/utils.py index 0db064ace..24461f47d 100644 --- a/tacker/vnfm/tosca/utils.py +++ b/tacker/vnfm/tosca/utils.py @@ -101,16 +101,19 @@ def updateimports(template): @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'] + sm_dict = params.get('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'] + req_dict_tpl = template['topology_template']['substitution_mappings'].get( + 'requirements') + # Check if substitution_mappings and requirements are empty in params but + # not in template. If True raise exception + if (not sm_dict or not requirements) and req_dict_tpl: + raise vnfm.InvalidParamsForSM() + # Check if requirements are present for SM in template, if True then return + elif (not sm_dict or not requirements) and not req_dict_tpl: + return + del params['substitution_mappings'] for req_name, req_val in iteritems(req_dict_tpl): if req_name not in requirements: raise vnfm.SMRequirementMissing(requirement=req_name)