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<bharath_ves@hotmail.com>
Partially-implements: blueprint nsd-support
changes/37/414937/31
dharmendra 6 years ago committed by Bharath Thiruveedula
parent d311cfb77a
commit 15c4d7ec5d

@ -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 <nsd file> <nsd name>**
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 <nsd-id> <ns-name>**
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

@ -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
=================

@ -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

@ -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)

@ -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': {

@ -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': {

@ -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')

@ -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")

@ -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"""

@ -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)

@ -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

@ -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))

@ -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']

@ -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"

@ -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

@ -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

@ -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

@ -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: