Support VNF create from inline template

Accept transient VNFD template as part of VNF creation without
need for VNFD onboarding into Tacker VNFD catalog.

Change-Id: I3c8bbe139dec27adbfc943d3ac9f909db8097f89
Implements: blueprint vnf-inline-template
Depends-On: I719237dd04dd7fe13fb7e7964402d7074679b2d6
This commit is contained in:
janki 2017-01-03 16:43:49 +05:30
parent 3e238f1bc4
commit a3ea91d124
17 changed files with 295 additions and 34 deletions

View File

@ -709,7 +709,19 @@ vnfd_id:
description: |
The UUID of the VNFD.
in: body
required: true
required: false
type: string
vnfd_template:
description: |
Template to create VNF.
in: body
required: false
type: object
vnfd_template_source:
description: |
Source of VNFD.
in: body
required: false
type: string
vnfds:
description: |

View File

@ -15,7 +15,8 @@
"vnfd": "description: Demo example\nmetadata: {template_name: sample-tosca-vnfd}\ntopology_template:\n node_templates:\n CP1:\n properties: {anti_spoofing_protection: false, management: true, order: 0}\n requirements:\n - virtualLink: {node: VL1}\n - virtualBinding: {node: VDU1}\n type: tosca.nodes.nfv.CP.Tacker\n VDU1:\n capabilities:\n nfv_compute:\n properties: {disk_size: 1 GB, mem_size: 512 MB, num_cpus: 1}\n properties: {image: cirros-0.3.4-x86_64-uec}\n type: tosca.nodes.nfv.VDU.Tacker\n VL1:\n properties: {network_name: net_mgmt, vendor: Tacker}\n type: tosca.nodes.nfv.VL\ntosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0\n"
},
"id": "0fb827e7-32b0-4e5b-b300-e1b1dce8a831",
"name": "vnfd-sample"
"name": "vnfd-sample",
"template_source": "onboarded or inline"
}
]
}

View File

@ -14,6 +14,7 @@
"vnfd": "description: Demo example\nmetadata: {template_name: sample-tosca-vnfd}\ntopology_template:\n node_templates:\n CP1:\n properties: {anti_spoofing_protection: false, management: true, order: 0}\n requirements:\n - virtualLink: {node: VL1}\n - virtualBinding: {node: VDU1}\n type: tosca.nodes.nfv.CP.Tacker\n VDU1:\n capabilities:\n nfv_compute:\n properties: {disk_size: 1 GB, mem_size: 512 MB, num_cpus: 1}\n properties: {image: cirros-0.3.4-x86_64-uec}\n type: tosca.nodes.nfv.VDU.Tacker\n VL1:\n properties: {network_name: net_mgmt, vendor: Tacker}\n type: tosca.nodes.nfv.VL\ntosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0\n"
},
"id": "0fb827e7-32b0-4e5b-b300-e1b1dce8a831",
"name": "vnfd-sample"
"name": "vnfd-sample",
"template_source": "onboarded or inline"
}
}

View File

@ -0,0 +1,66 @@
{
"vnf": {
"tenant_id": "6673e4d4e13340acb0b847f9ecde613b",
"vim_id": "f6bd6f24-7a0e-4111-8994-e108c5ee2ff2",
"name": "OpenWRT",
"description": "OpenWRT VNF",
"attributes": {
"config": {
"vdus": {
"vdu1": {
"config": {
"firewall": "package firewall\n"
}
}
}
},
"param_values": {
"vdus": {
"vdu1": {
"param": {
"vdu-name": "openwrt_vdu1"
}
}
}
}
},
"placement_attr": {
"region_name": "RegionOne"
},
"vnfd_template": {
"tosca_definitions_version": "tosca_simple_profile_for_nfv_1_0_0",
"description": "Demo example",
"metadata": {
"template_name": "sample-tosca-vnfd"},
"topology_template": {
"node_templates": {
"VDU1": {
"type": "tosca.nodes.nfv.VDU.Tacker",
"capabilities": {
"nfv_compute": {
"properties": {
"num_cpus": 1,
"mem_size": "512 MB",
"disk_size": "1 GB"}}},
"properties": {"image": "cirros-0.3.4-x86_64-uec"}},
"CP1": {
"type": "tosca.nodes.nfv.CP.Tacker",
"properties": {
"order": 0,
"management": true,
"anti_spoofing_protection": false},
"requirements": [
{"virtualLink": {
"node": "VL1"}},
{"virtualBinding": {
"node": "VDU1"}}]},
"VL1": {
"type": "tosca.nodes.nfv.VL",
"properties": {
"vendor": "Tacker",
"network_name": "net_mgmt"}}
}
}
}
}
}

View File

@ -102,6 +102,7 @@ Response Parameters
- attributes: vnfd_attributes
- id: vnfd_id
- name: name
- template_source: vnfd_template_source
Response Example
@ -151,6 +152,7 @@ Response Parameters
- attributes: vnfd_attributes
- id: vnfd_id
- name: name
- template_source: vnfd_template_source
Response Example
----------------

View File

@ -44,6 +44,7 @@ Request Parameters
- config: vnf_config_opt
- param_values: vnf_param_values_opt
- placement_attr: vnf_placement_attr_opt
- vnfd_template: vnfd_template
Request Example
---------------

View File

@ -0,0 +1,4 @@
---
features:
- Support VNF create with direct VNFD template input via CLI/API without
onboarding VNFD.

View File

@ -563,6 +563,11 @@ class Controller(object):
if 'validate' not in attr_vals:
continue
for rule in attr_vals['validate']:
# skip validating vnfd_id when vnfd_template is specified to
# create vnf
if resource == 'vnf' and 'vnfd_template' in body['vnf'] and \
attr == "vnfd_id" and is_create:
continue
res = attributes.validators[rule](res_dict[attr],
attr_vals['validate'][rule])
if res:

View File

@ -21,8 +21,10 @@
import functools
import logging as std_logging
import os
import random
import signal
import socket
import string
import sys
from eventlet.green import subprocess
@ -306,3 +308,11 @@ def deprecate_warning(what, as_of, in_favor_of=None, remove_in=1):
versionutils.deprecation_warning(as_of=as_of, what=what,
in_favor_of=in_favor_of,
remove_in=remove_in)
def generate_resource_name(resource, prefix='tmpl'):
return prefix + '-' \
+ ''.join(random.SystemRandom().choice(
string.ascii_lowercase + string.digits)
for _ in range(16)) \
+ '-' + resource

View File

@ -0,0 +1,33 @@
# Copyright 2016 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""add template_source column
Revision ID: 000632983ada
Revises: 0ae5b1ce3024
Create Date: 2016-12-22 20:30:03.931290
"""
# revision identifiers, used by Alembic.
revision = '000632983ada'
down_revision = '0ad3bbce1c19'
from alembic import op
import sqlalchemy as sa
def upgrade(active_plugins=None, options=None):
op.add_column('vnfd', sa.Column('template_source', sa.String(length=255)))

View File

@ -1 +1 @@
0ad3bbce1c19
000632983ada

View File

@ -67,6 +67,9 @@ class VNFD(model_base.BASE, models_v1.HasId, models_v1.HasTenant,
attributes = orm.relationship('VNFDAttribute',
backref='vnfd')
# vnfd template source - inline or onboarded
template_source = sa.Column(sa.String(255))
class ServiceType(model_base.BASE, models_v1.HasId, models_v1.HasTenant):
"""Represents service type which hosting vnf provides.
@ -184,7 +187,8 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin):
vnfd.service_types)
}
key_list = ('id', 'tenant_id', 'name', 'description',
'mgmt_driver', 'created_at', 'updated_at')
'mgmt_driver', 'created_at', 'updated_at',
'template_source')
res.update((key, vnfd[key]) for key in key_list)
return self._fields(res, fields)
@ -219,6 +223,7 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin):
tenant_id = self._get_tenant_id_for_create(context, vnfd)
service_types = vnfd.get('service_types')
mgmt_driver = vnfd.get('mgmt_driver')
template_source = vnfd.get("template_source")
if (not attributes.is_attr_set(service_types)):
LOG.debug(_('service types unspecified'))
@ -231,7 +236,8 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin):
tenant_id=tenant_id,
name=vnfd.get('name'),
description=vnfd.get('description'),
mgmt_driver=mgmt_driver)
mgmt_driver=mgmt_driver,
template_source=template_source)
context.session.add(vnfd_db)
for (key, value) in vnfd.get('attributes', {}).items():
attribute_db = VNFDAttribute(
@ -288,9 +294,7 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin):
vnfs_db = context.session.query(VNF).filter_by(
vnfd_id=vnfd_id).first()
if vnfs_db is not None and vnfs_db.deleted_at is None:
raise vnfm.VNFDInUse(
vnfd_id=vnfd_id)
raise vnfm.VNFDInUse(vnfd_id=vnfd_id)
vnfd_db = self._get_resource(context, VNFD,
vnfd_id)
if soft_delete:
@ -313,6 +317,9 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin):
return self._make_vnfd_dict(vnfd_db)
def get_vnfds(self, context, filters, fields=None):
if 'template_source' in filters and \
filters['template_source'][0] == 'all':
filters.pop('template_source')
return self._get_collection(context, VNFD,
self._make_vnfd_dict,
filters=filters, fields=fields)
@ -521,7 +528,8 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin):
tstamp=timeutils.utcnow(), details="VNF delete initiated")
return deleted_vnf_db
def _delete_vnf_post(self, context, vnf_id, error, soft_delete=True):
def _delete_vnf_post(self, context, vnf_dict, error, soft_delete=True):
vnf_id = vnf_dict['id']
with context.session.begin(subtransactions=True):
query = (
self._model_query(context, VNF).
@ -552,6 +560,10 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin):
filter(VNFAttribute.vnf_id == vnf_id).delete())
query.delete()
# Delete corresponding vnfd
if vnf_dict['vnfd']['template_source'] == "inline":
self.delete_vnfd(context, vnf_dict["vnfd_id"])
# reference implementation. needs to be overrided by subclass
def create_vnf(self, context, vnf):
vnf_dict = self._create_vnf_pre(context, vnf)
@ -577,12 +589,12 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin):
# reference implementation. needs to be overrided by subclass
def delete_vnf(self, context, vnf_id, soft_delete=True):
self._delete_vnf_pre(context, vnf_id)
vnf_dict = self._delete_vnf_pre(context, vnf_id)
# start actual deletion of hosting vnf.
# Waiting for completion of deletion should be done backgroundly
# by another thread if it takes a while.
self._delete_vnf_post(context,
vnf_id,
vnf_dict,
False,
soft_delete=soft_delete)

View File

@ -236,6 +236,12 @@ RESOURCE_ATTRIBUTE_MAP = {
'allow_put': False,
'is_visible': True,
},
'template_source': {
'allow_post': False,
'allow_put': False,
'is_visible': True,
'default': 'onboarded'
},
},
'vnfs': {
@ -258,6 +264,7 @@ RESOURCE_ATTRIBUTE_MAP = {
'allow_put': False,
'validate': {'type:uuid': None},
'is_visible': True,
'default': None
},
'vim_id': {
'allow_post': True,
@ -325,6 +332,13 @@ RESOURCE_ATTRIBUTE_MAP = {
'allow_put': False,
'is_visible': True,
},
'vnfd_template': {
'allow_post': True,
'allow_put': False,
'validate': {'type:dict_or_none': None},
'is_visible': True,
'default': None,
},
},
}

View File

@ -28,7 +28,8 @@ VNF_CIRROS_CREATE_TIMEOUT = 120
class VnfTestToscaCreate(base.BaseTackerTest):
def _test_create_vnf(self, vnfd_file, vnf_name):
def _test_create_vnf(self, vnfd_file, vnf_name,
template_source="onboarded"):
data = dict()
values_str = read_file(vnfd_file)
data['tosca'] = values_str
@ -36,16 +37,23 @@ class VnfTestToscaCreate(base.BaseTackerTest):
tosca_arg = {'vnfd': {'name': vnf_name,
'attributes': {'vnfd': toscal}}}
# Create vnfd with tosca template
vnfd_instance = self.client.create_vnfd(body=tosca_arg)
self.assertIsNotNone(vnfd_instance)
if template_source == "onboarded":
# Create vnfd with tosca template
vnfd_instance = self.client.create_vnfd(body=tosca_arg)
self.assertIsNotNone(vnfd_instance)
# Create vnf with vnfd_id
vnfd_id = vnfd_instance['vnfd']['id']
vnf_arg = {'vnf': {'vnfd_id': vnfd_id, 'name': vnf_name}}
vnf_instance = self.client.create_vnf(body=vnf_arg)
# Create vnf with vnfd_id
vnfd_id = vnfd_instance['vnfd']['id']
vnf_arg = {'vnf': {'vnfd_id': vnfd_id, 'name': vnf_name}}
vnf_instance = self.client.create_vnf(body=vnf_arg)
self.validate_vnf_instance(vnfd_instance, vnf_instance)
self.validate_vnf_instance(vnfd_instance, vnf_instance)
if template_source == 'inline':
# create vnf directly from template
template = yaml.load(values_str)
vnf_arg = {'vnf': {'vnfd_template': template, 'name': vnf_name}}
vnf_instance = self.client.create_vnf(body=vnf_arg)
vnfd_id = vnf_instance['vnf']['vnfd_id']
vnf_id = vnf_instance['vnf']['id']
self.wait_until_vnf_active(
@ -100,10 +108,10 @@ class VnfTestToscaCreate(base.BaseTackerTest):
self.addCleanup(self.wait_until_vnf_delete, vnf_id,
constants.VNF_CIRROS_DELETE_TIMEOUT)
def test_create_delete_vnf_tosca(self):
vnfd_id, vnf_id = self._test_create_vnf(
'sample-tosca-vnfd.yaml',
'test_tosca_vnf_with_cirros')
def _test_create_delete_vnf_tosca(self, vnfd_file, vnf_name,
template_source):
vnfd_id, vnf_id = self._test_create_vnf(vnfd_file, vnf_name,
template_source)
servers = self.novaclient().servers.list()
vdus = []
for server in servers:
@ -116,7 +124,18 @@ class VnfTestToscaCreate(base.BaseTackerTest):
vdu_ports.append(port['name'])
self.assertIn('test-cp', vdu_ports)
self._test_delete_vnf(vnf_id)
self._test_cleanup_vnfd(vnfd_id, vnf_id)
if template_source == "onboarded":
self._test_cleanup_vnfd(vnfd_id, vnf_id)
def test_create_delete_vnf_tosca_from_vnfd(self):
self._test_create_delete_vnf_tosca('sample-tosca-vnfd.yaml',
'test_tosca_vnf_with_cirros',
'onboarded')
def test_create_delete_vnf_from_template(self):
self._test_create_delete_vnf_tosca('sample-tosca-vnfd.yaml',
'test_tosca_vnf_with_cirros_inline',
'inline')
def test_create_delete_vnf_static_ip(self):
vnfd_id, vnf_id = self._test_create_vnf(

View File

@ -46,9 +46,31 @@ def get_dummy_vnfd_obj():
'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437',
u'attributes': {u'vnfd': yaml.safe_load(
tosca_vnfd_openwrt)},
'description': 'dummy_vnfd_description'},
'description': 'dummy_vnfd_description',
'template_source': 'onboarded',
u'auth': {u'tenantName': u'admin', u'passwordCredentials': {
u'username': u'admin', u'password': u'devstack'}}}
u'username': u'admin', u'password': u'devstack'}}}}
def get_dummy_vnfd_obj_inline():
return {u'vnfd': {u'service_types': [{u'service_type': u'vnfd'}],
'name': 'tmpl-koeak4tqgoqo8cr4-dummy_inline_vnf',
'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437',
u'attributes': {u'vnfd': yaml.safe_load(
tosca_vnfd_openwrt)},
'template_source': 'inline',
u'auth': {u'tenantName': u'admin', u'passwordCredentials': {
u'username': u'admin', u'password': u'devstack'}}}}
def get_dummy_inline_vnf_obj():
return {'vnf': {'description': 'dummy_inline_vnf_description',
'vnfd_template': yaml.safe_load(tosca_vnfd_openwrt),
'vim_id': u'6261579e-d6f3-49ad-8bc3-a9cb974778ff',
'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437',
'name': 'dummy_inline_vnf',
'attributes': {},
'vnfd_id': None}}
def get_dummy_vnf_obj():
@ -57,7 +79,8 @@ def get_dummy_vnf_obj():
'vim_id': u'6261579e-d6f3-49ad-8bc3-a9cb974778ff',
'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437',
'name': 'dummy_vnf',
'attributes': {}}}
'attributes': {},
'vnfd_template': None}}
def get_dummy_vnf_config_obj():

View File

@ -123,7 +123,20 @@ class TestVNFMPlugin(db_base.SqlTestCase):
id='eb094833-995e-49f0-a047-dfb56aaf7c4e',
tenant_id='ad7ebc56538745a08ef7c5e97f8bd437',
name='fake_template',
description='fake_template_description')
description='fake_template_description',
template_source='onboarded')
session.add(device_template)
session.flush()
return device_template
def _insert_dummy_device_template_inline(self):
session = self.context.session
device_template = vnfm_db.VNFD(
id='d58bcc4e-d0cf-11e6-bf26-cec0c932ce01',
tenant_id='ad7ebc56538745a08ef7c5e97f8bd437',
name='tmpl-koeak4tqgoqo8cr4-dummy_inline_vnf',
description='inline_fake_template_description',
template_source='inline')
session.add(device_template)
session.flush()
return device_template
@ -219,6 +232,7 @@ class TestVNFMPlugin(db_base.SqlTestCase):
self.assertIn('attributes', result)
self.assertIn('created_at', result)
self.assertIn('updated_at', result)
self.assertIn('template_source', result)
yaml_dict = yaml.safe_load(utils.tosca_vnfd_openwrt)
mock_tosca_template.assert_called_once_with(
a_file=False, yaml_dict_tpl=yaml_dict)
@ -244,7 +258,7 @@ class TestVNFMPlugin(db_base.SqlTestCase):
self.vnfm_plugin.create_vnfd,
self.context, vnfd_obj)
def test_create_vnf(self):
def test_create_vnf_with_vnfd(self):
self._insert_dummy_device_template()
vnf_obj = utils.get_dummy_vnf_obj()
result = self.vnfm_plugin.create_vnf(self.context, vnf_obj)
@ -268,6 +282,35 @@ class TestVNFMPlugin(db_base.SqlTestCase):
res_state=mock.ANY, res_type=constants.RES_TYPE_VNF,
tstamp=mock.ANY, details=mock.ANY)
@mock.patch('tacker.vnfm.plugin.VNFMPlugin.create_vnfd')
def test_create_vnf_from_template(self, mock_create_vnfd):
self._insert_dummy_device_template_inline()
mock_create_vnfd.return_value = {'id':
'd58bcc4e-d0cf-11e6-bf26-cec0c932ce01'}
vnf_obj = utils.get_dummy_inline_vnf_obj()
result = self.vnfm_plugin.create_vnf(self.context, vnf_obj)
self.assertIsNotNone(result)
self.assertIn('id', result)
self.assertIn('instance_id', result)
self.assertIn('status', result)
self.assertIn('attributes', result)
self.assertIn('mgmt_url', result)
self.assertIn('created_at', result)
self.assertIn('updated_at', result)
mock_create_vnfd.assert_called_once_with(mock.ANY, mock.ANY)
self._device_manager.invoke.assert_called_with('test_vim',
'create',
plugin=mock.ANY,
context=mock.ANY,
vnf=mock.ANY,
auth_attr=mock.ANY)
self._pool.spawn_n.assert_called_once_with(mock.ANY)
self._cos_db_plugin.create_event.assert_called_with(
self.context, evt_type=constants.RES_EVT_CREATE,
res_id=mock.ANY,
res_state=mock.ANY, res_type=constants.RES_TYPE_VNF,
tstamp=mock.ANY, details=mock.ANY)
def test_show_vnf_details_vnf_inactive(self):
self._insert_dummy_device_template()
vnf_obj = utils.get_dummy_vnf_obj()

View File

@ -163,6 +163,11 @@ class VNFMPlugin(vnfm_db.VNFMPluginDb, VNFMMgmtMixin):
# framework doesn't know what services are valid for now.
# so doesn't check it here yet.
pass
if 'template_source' in vnfd_data:
template_source = vnfd_data.get('template_source')
else:
template_source = 'onboarded'
vnfd['vnfd']['template_source'] = template_source
self._parse_template_input(vnfd)
return super(VNFMPlugin, self).create_vnfd(
@ -334,6 +339,17 @@ class VNFMPlugin(vnfm_db.VNFMPluginDb, VNFMMgmtMixin):
name = vnf_info['name']
if self._get_by_name(context, vnfm_db.VNF, name):
raise exceptions.DuplicateResourceName(resource='VNF', name=name)
# if vnfd_template specified, create vnfd from template
# create template dictionary structure same as needed in create_vnfd()
if vnf_info.get('vnfd_template'):
vnfd_name = utils.generate_resource_name(name, 'inline')
vnfd = {'vnfd': {'attributes': {'vnfd': vnf_info['vnfd_template']},
'name': vnfd_name,
'template_source': 'inline',
'service_types': [{'service_type': 'vnfd'}]}}
vnf_info['vnfd_id'] = self.create_vnfd(context, vnfd).get('id')
vnf_attributes = vnf_info['attributes']
if vnf_attributes.get('param_values'):
param = vnf_attributes['param_values']
@ -464,8 +480,7 @@ class VNFMPlugin(vnfm_db.VNFMPluginDb, VNFMMgmtMixin):
LOG.exception(_('_delete_vnf_wait'))
self.mgmt_delete_post(context, vnf_dict)
vnf_id = vnf_dict['id']
self._delete_vnf_post(context, vnf_id, e)
self._delete_vnf_post(context, vnf_dict, e)
def delete_vnf(self, context, vnf_id):
vnf_dict = self._delete_vnf_pre(context, vnf_id)
@ -497,7 +512,7 @@ class VNFMPlugin(vnfm_db.VNFMPluginDb, VNFMMgmtMixin):
vnf_dict['status'] = constants.ERROR
vnf_dict['error_reason'] = six.text_type(e)
self.mgmt_delete_post(context, vnf_dict)
self._delete_vnf_post(context, vnf_id, e)
self._delete_vnf_post(context, vnf_dict, e)
self.spawn_n(self._delete_vnf_wait, context, vnf_dict, vim_auth,
driver_name)