commit 059d71036c49b817449e42ed18b7d1eb9c6e6479 Author: Aldinson Esto Date: Tue Aug 18 13:42:45 2020 +0900 Support of version 2.4.1 on Create VNF request VNFM returns a "201 Created" response containing "vnfPkgIdā€¯ attribute in the payload body. Implements: blueprint support-etsi-nfv-specs Spec: https://specs.openstack.org/openstack/tacker-specs/specs/victoria/enhancement_enhance-vnf-lcm-api-support.html Change-Id: I7e227edeb40474fce0a77979f3f540638d90d9f4 (cherry picked from commit ff97127e99851eb4bf1fb088ecc08fa3dce8cfd4) diff --git a/api-ref/source/v1/parameters_vnflcm.yaml b/api-ref/source/v1/parameters_vnflcm.yaml index 22d23f0..94b9bd1 100644 --- a/api-ref/source/v1/parameters_vnflcm.yaml +++ b/api-ref/source/v1/parameters_vnflcm.yaml @@ -546,6 +546,15 @@ vnf_instance_vim_connection_info: in: body required: false type: array +vnf_instance_vnf_pkg_id: + description: | + Identifier of information held by the NFVO about the specific VNF package + on which the VNF is based. + This identifier is allocated by the NFVO + and can be modified with the PATCH method. + in: body + required: true + type: string vnf_instance_vnf_product_name: description: | Name to identify the VNF Product. The value is copied from the VNFD. diff --git a/api-ref/source/v1/vnflcm.inc b/api-ref/source/v1/vnflcm.inc index 73d7acb..43c55c2 100644 --- a/api-ref/source/v1/vnflcm.inc +++ b/api-ref/source/v1/vnflcm.inc @@ -62,6 +62,7 @@ Response Parameters - vnfProductName: vnf_instance_vnf_product_name - vnfSoftwareVersion: vnf_instance_vnf_software_version - vnfdVersion: vnf_instance_vnfd_version + - vnfPkgId: vnf_instance_vnf_pkg_id - instantiationState: vnf_instance_instantiation_state - _links: vnf_instance_links diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index 7c8bbcd..d6958b4 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -185,6 +185,7 @@ class VnfLcmController(wsgi.Controller): vnf_product_name=vnfd.vnf_product_name, vnf_software_version=vnfd.vnf_software_version, vnfd_version=vnfd.vnfd_version, + vnf_pkg_id=vnfd.package_uuid, tenant_id=request.context.project_id) vnf_instance.create() diff --git a/tacker/db/db_sqlalchemy/models.py b/tacker/db/db_sqlalchemy/models.py index 68b53d5..9706a36 100644 --- a/tacker/db/db_sqlalchemy/models.py +++ b/tacker/db/db_sqlalchemy/models.py @@ -200,6 +200,7 @@ class VnfInstance(model_base.BASE, models.SoftDeleteMixin, instantiation_state = sa.Column(sa.String(255), nullable=False) task_state = sa.Column(sa.String(255), nullable=True) vim_connection_info = sa.Column(sa.JSON(), nullable=True) + vnf_pkg_id = sa.Column(types.Uuid, nullable=False) tenant_id = sa.Column('tenant_id', sa.String(length=64), nullable=False) diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index 35190f8..f288ff1 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -e06fbdc90a32 +f9bc96967462 diff --git a/tacker/db/migration/alembic_migrations/versions/f9bc96967462_add_vnf_pkg_id_to_vnf_instances.py b/tacker/db/migration/alembic_migrations/versions/f9bc96967462_add_vnf_pkg_id_to_vnf_instances.py new file mode 100644 index 0000000..b937adc --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/f9bc96967462_add_vnf_pkg_id_to_vnf_instances.py @@ -0,0 +1,40 @@ +# Copyright 2020 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. +# + +# flake8: noqa: E402 + +"""add_vnf_pkg_id_to_vnf_instances + +Revision ID: f9bc96967462 +Revises: e06fbdc90a32 +Create Date: 2020-09-11 22:41:25.799693 + +""" + +# revision identifiers, used by Alembic. +revision = 'f9bc96967462' +down_revision = 'e06fbdc90a32' + +from alembic import op +import sqlalchemy as sa + + +from tacker.db import migration +from tacker.db import types + + +def upgrade(active_plugins=None, options=None): + op.add_column('vnf_instances', sa.Column('vnf_pkg_id', + types.Uuid(length=36), nullable=False)) diff --git a/tacker/objects/vnf_instance.py b/tacker/objects/vnf_instance.py index d47aa11..f56d04b 100644 --- a/tacker/objects/vnf_instance.py +++ b/tacker/objects/vnf_instance.py @@ -138,7 +138,8 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, 'VimConnectionInfo', nullable=True, default=[]), 'tenant_id': fields.StringField(nullable=False), 'instantiated_vnf_info': fields.ObjectField('InstantiatedVnfInfo', - nullable=True, default=None) + nullable=True, default=None), + 'vnf_pkg_id': fields.StringField(nullable=False) } def __init__(self, context=None, **kwargs): @@ -243,6 +244,7 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, 'vnf_provider': self.vnf_provider, 'vnf_product_name': self.vnf_product_name, 'vnf_software_version': self.vnf_software_version, + 'vnf_pkg_id': self.vnf_pkg_id, 'vnfd_version': self.vnfd_version} if (self.instantiation_state == fields.VnfInstanceState.INSTANTIATED diff --git a/tacker/tests/unit/objects/fakes.py b/tacker/tests/unit/objects/fakes.py index 41e244b..865f575 100644 --- a/tacker/tests/unit/objects/fakes.py +++ b/tacker/tests/unit/objects/fakes.py @@ -111,7 +111,8 @@ def get_vnf_instance_data(vnfd_id): "vnf_provider": "test vnf provider", "vnfd_id": vnfd_id, "vnfd_version": "1.0", - "tenant_id": uuidsentinel.tenant_id + "tenant_id": uuidsentinel.tenant_id, + "vnf_pkg_id": uuidsentinel.vnf_pkg_id } @@ -126,7 +127,8 @@ def get_vnf_instance_data_with_id(vnfd_id): "vnf_provider": "test vnf provider", "vnfd_id": vnfd_id, "vnfd_version": "1.0", - "tenant_id": uuidsentinel.tenant_id + "tenant_id": uuidsentinel.tenant_id, + "vnf_pkg_id": uuidsentinel.vnf_pkg_id } @@ -148,6 +150,7 @@ def fake_vnf_instance_model_dict(**updates): 'vim_connection_info': [], 'tenant_id': '33f8dbdae36142eebf214c1869eb4e4c', 'id': constants.UUID, + 'vnf_pkg_id': uuidsentinel.vnf_pkg_id } if updates: @@ -342,7 +345,8 @@ def vnf_instance_model_object(vnf_instance): 'vnfd_version': vnf_instance.vnfd_version, 'vim_connection_info': vnf_instance.vim_connection_info, 'tenant_id': vnf_instance.tenant_id, - 'created_at': vnf_instance.created_at + 'created_at': vnf_instance.created_at, + 'vnf_pkg_id': vnf_instance.vnf_pkg_id } vnf_instance_db_obj = models.VnfInstance() diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index 098e276..da1dbe5 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -79,6 +79,7 @@ def _model_non_instantiated_vnf_instance(**updates): 'vnf_software_version': '1.0', 'tenant_id': uuidsentinel.tenant_id, 'vnfd_id': uuidsentinel.vnfd_id, + 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, 'vnfd_version': '1.0'} if updates: @@ -157,6 +158,7 @@ def _fake_vnf_instance_not_instantiated_response( 'vnfdId': uuidsentinel.vnfd_id, 'vnfdVersion': '1.0', 'vnfSoftwareVersion': '1.0', + 'vnfPkgId': uuidsentinel.vnf_pkg_id, 'id': uuidsentinel.vnf_instance_id } diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index cad7ffb..7612091 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -21,13 +21,14 @@ from webob import exc from tacker.api.vnflcm.v1 import controller from tacker.common import exceptions -from tacker.conductor.conductorrpc.vnf_lcm_rpc import VNFLcmRPCAPI +import tacker.conductor.conductorrpc.vnf_lcm_rpc as vnf_lcm_rpc from tacker.extensions import nfvo from tacker import objects from tacker.objects import fields from tacker.tests import constants from tacker.tests.unit import base from tacker.tests.unit import fake_request +import tacker.tests.unit.nfvo.test_nfvo_plugin as nfvo_plugin from tacker.tests.unit.vnflcm import fakes from tacker.tests import uuidsentinel from tacker.vnfm import vim_client @@ -38,20 +39,40 @@ class TestController(base.TestCase): def setUp(self): super(TestController, self).setUp() + self.patcher = mock.patch( + 'tacker.manager.TackerManager.get_service_plugins', + return_value={'VNFM': nfvo_plugin.FakeVNFMPlugin()}) + self.mock_manager = self.patcher.start() self.controller = controller.VnfLcmController() + def tearDown(self): + self.mock_manager.stop() + super(TestController, self).tearDown() + @property def app(self): return fakes.wsgi_app_v1() + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_package.VnfPackage, 'get_by_id') + @mock.patch.object(objects.vnf_package.VnfPackage, 'save') @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') - @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') def test_create_without_name_and_description( - self, mock_get_by_id, mock_vnf_instance_create): - mock_get_by_id.return_value = fakes.return_vnf_package_vnfd() + self, mock_get_by_id_package_vnfd, + mock_vnf_instance_create, mock_package_save, + mock_get_by_id_package, mock_get_vim): + + mock_get_by_id_package_vnfd.return_value = \ + fakes.return_vnf_package_vnfd() + mock_get_by_id_package.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + + updates = {'vnfd_id': uuidsentinel.vnfd_id, + 'vnf_instance_description': None, + 'vnf_instance_name': None, + 'vnf_pkg_id': uuidsentinel.vnf_pkg_id} - updates = {'vnf_instance_description': None, - 'vnf_instance_name': None} mock_vnf_instance_create.return_value =\ fakes.return_vnf_instance_model(**updates) @@ -59,6 +80,7 @@ class TestController(base.TestCase): body = {'vnfdId': uuidsentinel.vnfd_id} req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' + req.headers['Version'] = '2.6.1' req.method = 'POST' # Call create API @@ -67,20 +89,34 @@ class TestController(base.TestCase): self.assertEqual(http_client.CREATED, resp.status_code) updates = {'vnfInstanceDescription': None, 'vnfInstanceName': None} - expected_vnf = fakes.fake_vnf_instance_response(**updates) + expected_vnf = fakes.fake_vnf_instance_response( + instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED, + **updates) location_header = ('http://localhost/vnflcm/v1/vnf_instances/%s' % resp.json['id']) self.assertEqual(expected_vnf, resp.json) self.assertEqual(location_header, resp.headers['location']) + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_package.VnfPackage, 'get_by_id') + @mock.patch.object(objects.vnf_package.VnfPackage, 'save') @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') - @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') def test_create_with_name_and_description( - self, mock_get_by_id, mock_vnf_instance_create): - mock_get_by_id.return_value = fakes.return_vnf_package_vnfd() - updates = {'vnf_instance_description': 'SampleVnf Description', - 'vnf_instance_name': 'SampleVnf'} + self, mock_get_by_id_package_vnfd, + mock_vnf_instance_create, mock_package_save, + mock_get_by_id_package, mock_get_vim): + mock_get_by_id_package_vnfd.return_value = \ + fakes.return_vnf_package_vnfd() + mock_get_by_id_package.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + + updates = {'vnfd_id': uuidsentinel.vnfd_id, + 'vnf_instance_description': 'SampleVnf Description', + 'vnf_instance_name': 'SampleVnf', + 'vnf_pkg_id': uuidsentinel.vnf_pkg_id} + mock_vnf_instance_create.return_value =\ fakes.return_vnf_instance_model(**updates) @@ -90,6 +126,7 @@ class TestController(base.TestCase): req = fake_request.HTTPRequest.blank('/vnf_instances') req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' + req.headers['Version'] = '2.6.1' req.method = 'POST' # Call Create API @@ -98,13 +135,54 @@ class TestController(base.TestCase): self.assertEqual(http_client.CREATED, resp.status_code) updates = {"vnfInstanceName": "SampleVnf", "vnfInstanceDescription": "SampleVnf Description"} - expected_vnf = fakes.fake_vnf_instance_response(**updates) + expected_vnf = fakes.fake_vnf_instance_response( + instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED, + **updates) location_header = ('http://localhost/vnflcm/v1/vnf_instances/%s' % resp.json['id']) self.assertEqual(expected_vnf, resp.json) self.assertEqual(location_header, resp.headers['location']) + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.vnf_package.VnfPackage, 'get_by_id') + @mock.patch.object(objects.vnf_package.VnfPackage, 'save') + @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') + def test_create_without_name_and_description_with_v241( + self, mock_get_by_id_package_vnfd, + mock_vnf_instance_create, mock_package_save, + mock_get_by_id_package, mock_get_vim): + mock_get_by_id_package_vnfd.return_value = \ + fakes.return_vnf_package_vnfd() + mock_get_by_id_package.return_value = \ + fakes.return_vnf_package_with_deployment_flavour() + + updates = {'vnfd_id': uuidsentinel.vnfd_id, + 'vnf_instance_description': None, + 'vnf_instance_name': None, + 'vnf_pkg_id': uuidsentinel.vnf_pkg_id} + + mock_vnf_instance_create.return_value =\ + fakes.return_vnf_instance_model(**updates) + + req = fake_request.HTTPRequest.blank('/vnf_instances') + body = {'vnfdId': uuidsentinel.vnfd_id} + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.headers['Version'] = '' + req.method = 'POST' + + # Call create API + resp = req.get_response(self.app) + + self.assertEqual(http_client.CREATED, resp.status_code) + + updates = {'vnfInstanceDescription': None, 'vnfInstanceName': None} + expected_vnf = fakes.fake_vnf_instance_response(**updates) + + self.assertEqual(expected_vnf, resp.json) + @ddt.data( {'attribute': 'vnfdId', 'value': True, 'expected_type': 'uuid'}, @@ -130,6 +208,7 @@ class TestController(base.TestCase): body.update({attribute: value}) req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' + req.headers['Version'] = '2.6.1' req.method = 'POST' exception = self.assertRaises( exceptions.ValidationError, self.controller.create, @@ -154,6 +233,7 @@ class TestController(base.TestCase): req = fake_request.HTTPRequest.blank('/vnf_instances') req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' + req.headers['Version'] = '2.6.1' req.method = 'POST' self.assertRaises(exc.HTTPBadRequest, self.controller.create, req, body=body) @@ -202,7 +282,7 @@ class TestController(base.TestCase): @mock.patch.object(objects.VnfInstance, "save") @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfPackage, "get_by_id") - @mock.patch.object(VNFLcmRPCAPI, "instantiate") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") def test_instantiate_with_deployment_flavour( self, mock_instantiate, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, mock_save, @@ -259,7 +339,7 @@ class TestController(base.TestCase): @mock.patch.object(objects.VnfInstance, "save") @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfPackage, "get_by_id") - @mock.patch.object(VNFLcmRPCAPI, "instantiate") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") def test_instantiate_with_instantiation_level( self, mock_instantiate, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, mock_save, @@ -290,7 +370,7 @@ class TestController(base.TestCase): @mock.patch.object(objects.VnfInstance, "save") @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfPackage, "get_by_id") - @mock.patch.object(VNFLcmRPCAPI, "instantiate") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") def test_instantiate_with_no_inst_level_in_flavour( self, mock_instantiate, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, mock_save, @@ -323,7 +403,7 @@ class TestController(base.TestCase): @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfPackage, "get_by_id") - @mock.patch.object(VNFLcmRPCAPI, "instantiate") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") def test_instantiate_with_non_existing_instantiation_level( self, mock_instantiate, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, @@ -355,7 +435,7 @@ class TestController(base.TestCase): @mock.patch.object(objects.VnfInstance, "save") @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfPackage, "get_by_id") - @mock.patch.object(VNFLcmRPCAPI, "instantiate") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") def test_instantiate_with_vim_connection( self, mock_instantiate, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, mock_save, @@ -696,7 +776,7 @@ class TestController(base.TestCase): @mock.patch.object(objects.VnfInstance, "get_by_id") @mock.patch.object(objects.VnfInstance, "save") - @mock.patch.object(VNFLcmRPCAPI, "terminate") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "terminate") @ddt.data({'terminationType': 'FORCEFUL'}, {'terminationType': 'GRACEFUL'}, {'terminationType': 'GRACEFUL', @@ -836,7 +916,7 @@ class TestController(base.TestCase): @mock.patch.object(objects.VnfInstance, "get_by_id") @mock.patch.object(objects.VnfInstance, "save") - @mock.patch.object(VNFLcmRPCAPI, "heal") + @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "heal") @ddt.data({'cause': 'healing'}, {}) def test_heal(self, body, mock_rpc_heal, mock_save, mock_vnf_by_id):