From e925ddfa1960d0c9fbd8678af917ea667c0a9b47 Mon Sep 17 00:00:00 2001 From: Aldinson Esto Date: Thu, 27 Aug 2020 14:28:07 +0900 Subject: [PATCH] Support API enhancement for Create VNF Supported/Enhanced the Create VNF API. Implements: blueprint support-vnfm-operations Spec: https://specs.openstack.org/openstack/tacker-specs/specs/victoria/support-sol003-vnfm-operations.html Change-Id: Ie602242474149fec3ee8dbe1b8745c1803ad7336 --- api-ref/source/v1/parameters_vnflcm.yaml | 6 ++ api-ref/source/v1/vnflcm.inc | 5 +- tacker/api/schemas/vnf_lcm.py | 1 + tacker/api/views/vnf_lcm.py | 4 ++ tacker/api/vnflcm/v1/controller.py | 3 +- tacker/db/db_sqlalchemy/models.py | 1 + ...e3e9fe5e2_add_vnf_metadata_to_vnflcm_db.py | 39 ++++++++++ .../alembic_migrations/versions/HEAD | 2 +- tacker/objects/vnf_instance.py | 6 +- tacker/tests/unit/objects/fakes.py | 12 ++-- tacker/tests/unit/vnflcm/fakes.py | 6 +- tacker/tests/unit/vnflcm/test_controller.py | 71 +++++++++++++++---- 12 files changed, 131 insertions(+), 25 deletions(-) create mode 100644 tacker/db/migration/alembic_migrations/versions/745e3e9fe5e2_add_vnf_metadata_to_vnflcm_db.py diff --git a/api-ref/source/v1/parameters_vnflcm.yaml b/api-ref/source/v1/parameters_vnflcm.yaml index 94b9bd190..0d04708e9 100644 --- a/api-ref/source/v1/parameters_vnflcm.yaml +++ b/api-ref/source/v1/parameters_vnflcm.yaml @@ -471,6 +471,12 @@ vnf_instance_create_request_description: in: body required: false type: string +vnf_instance_create_request_metadata: + description: | + This attribute provides values for the "metadata" attribute in "VnfInstance". + in: body + required: false + type: array vnf_instance_create_request_name: description: | Human-readable name of the VNF instance to be created. diff --git a/api-ref/source/v1/vnflcm.inc b/api-ref/source/v1/vnflcm.inc index 43c55c267..0c2c2d474 100644 --- a/api-ref/source/v1/vnflcm.inc +++ b/api-ref/source/v1/vnflcm.inc @@ -42,6 +42,7 @@ Request Parameters - vnfdId: vnf_instance_create_request_vnfd_id - vnfInstanceName: vnf_instance_create_request_name - vnfInstanceDescription: vnf_instance_create_request_description + - metadata: vnf_instance_create_request_metadata Request Example --------------- @@ -520,9 +521,9 @@ Response Parameters - resourceId: resource_handle_resource_id - vimLevelResourceType: resource_handle_vim_level_resource_type - _links: vnf_instance_links - + Response Example ---------------- .. literalinclude:: samples/vnflcm/list-vnf-instance-response.json - :language: javascript \ No newline at end of file + :language: javascript diff --git a/tacker/api/schemas/vnf_lcm.py b/tacker/api/schemas/vnf_lcm.py index 40ab08863..72438e8fc 100644 --- a/tacker/api/schemas/vnf_lcm.py +++ b/tacker/api/schemas/vnf_lcm.py @@ -181,6 +181,7 @@ create = { 'vnfdId': parameter_types.uuid, 'vnfInstanceName': parameter_types.name_allow_zero_min_length, 'vnfInstanceDescription': parameter_types.description, + 'metadata': parameter_types.keyvalue_pairs, }, 'required': ['vnfdId'], 'additionalProperties': False, diff --git a/tacker/api/views/vnf_lcm.py b/tacker/api/views/vnf_lcm.py index 83cbdb692..868126fef 100644 --- a/tacker/api/views/vnf_lcm.py +++ b/tacker/api/views/vnf_lcm.py @@ -56,6 +56,10 @@ class ViewBuilder(object): def _get_vnf_instance_info(self, vnf_instance): vnf_instance_dict = vnf_instance.to_dict() + if 'vnf_metadata' in vnf_instance_dict: + metadata_val = vnf_instance_dict.pop('vnf_metadata') + vnf_instance_dict['metadata'] = metadata_val + vnf_instance_dict = utils.convert_snakecase_to_camelcase( vnf_instance_dict) diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index d6958b4c2..8b907b7b2 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -186,7 +186,8 @@ class VnfLcmController(wsgi.Controller): vnf_software_version=vnfd.vnf_software_version, vnfd_version=vnfd.vnfd_version, vnf_pkg_id=vnfd.package_uuid, - tenant_id=request.context.project_id) + tenant_id=request.context.project_id, + vnf_metadata=req_body.get('metadata')) vnf_instance.create() result = self._view_builder.create(vnf_instance) diff --git a/tacker/db/db_sqlalchemy/models.py b/tacker/db/db_sqlalchemy/models.py index 9706a3631..e2efb75a6 100644 --- a/tacker/db/db_sqlalchemy/models.py +++ b/tacker/db/db_sqlalchemy/models.py @@ -202,6 +202,7 @@ class VnfInstance(model_base.BASE, models.SoftDeleteMixin, 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) + vnf_metadata = sa.Column(sa.JSON(), nullable=True) class VnfInstantiatedInfo(model_base.BASE, models.SoftDeleteMixin, diff --git a/tacker/db/migration/alembic_migrations/versions/745e3e9fe5e2_add_vnf_metadata_to_vnflcm_db.py b/tacker/db/migration/alembic_migrations/versions/745e3e9fe5e2_add_vnf_metadata_to_vnflcm_db.py new file mode 100644 index 000000000..19378ac8f --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/745e3e9fe5e2_add_vnf_metadata_to_vnflcm_db.py @@ -0,0 +1,39 @@ +# 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_metadata_to_vnflcm_db + +Revision ID: 745e3e9fe5e2 +Revises: f9bc96967462 +Create Date: 2020-08-28 20:21:04.604343 + +""" + +# revision identifiers, used by Alembic. +revision = '745e3e9fe5e2' +down_revision = 'f9bc96967462' + +from alembic import op +import sqlalchemy as sa + + +from tacker.db import migration + + +def upgrade(active_plugins=None, options=None): + op.add_column('vnf_instances', + sa.Column('vnf_metadata', sa.JSON(), nullable=True)) diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index f288ff128..873b159a2 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -f9bc96967462 +745e3e9fe5e2 diff --git a/tacker/objects/vnf_instance.py b/tacker/objects/vnf_instance.py index f56d04bea..e4c137196 100644 --- a/tacker/objects/vnf_instance.py +++ b/tacker/objects/vnf_instance.py @@ -139,7 +139,8 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, 'tenant_id': fields.StringField(nullable=False), 'instantiated_vnf_info': fields.ObjectField('InstantiatedVnfInfo', nullable=True, default=None), - 'vnf_pkg_id': fields.StringField(nullable=False) + 'vnf_pkg_id': fields.StringField(nullable=False), + 'vnf_metadata': fields.DictOfStringsField(nullable=True, default={}) } def __init__(self, context=None, **kwargs): @@ -245,7 +246,8 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, '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} + 'vnfd_version': self.vnfd_version, + 'vnf_metadata': self.vnf_metadata} if (self.instantiation_state == fields.VnfInstanceState.INSTANTIATED and self.instantiated_vnf_info): diff --git a/tacker/tests/unit/objects/fakes.py b/tacker/tests/unit/objects/fakes.py index 865f575a6..ef9791a64 100644 --- a/tacker/tests/unit/objects/fakes.py +++ b/tacker/tests/unit/objects/fakes.py @@ -112,7 +112,8 @@ def get_vnf_instance_data(vnfd_id): "vnfd_id": vnfd_id, "vnfd_version": "1.0", "tenant_id": uuidsentinel.tenant_id, - "vnf_pkg_id": uuidsentinel.vnf_pkg_id + "vnf_pkg_id": uuidsentinel.vnf_pkg_id, + "vnf_metadata": {"key": "value"} } @@ -128,7 +129,8 @@ def get_vnf_instance_data_with_id(vnfd_id): "vnfd_id": vnfd_id, "vnfd_version": "1.0", "tenant_id": uuidsentinel.tenant_id, - "vnf_pkg_id": uuidsentinel.vnf_pkg_id + "vnf_pkg_id": uuidsentinel.vnf_pkg_id, + "vnf_metadata": {"key": "value"} } @@ -150,7 +152,8 @@ def fake_vnf_instance_model_dict(**updates): 'vim_connection_info': [], 'tenant_id': '33f8dbdae36142eebf214c1869eb4e4c', 'id': constants.UUID, - 'vnf_pkg_id': uuidsentinel.vnf_pkg_id + 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, + 'vnf_metadata': {'key': 'value'} } if updates: @@ -346,7 +349,8 @@ def vnf_instance_model_object(vnf_instance): 'vim_connection_info': vnf_instance.vim_connection_info, 'tenant_id': vnf_instance.tenant_id, 'created_at': vnf_instance.created_at, - 'vnf_pkg_id': vnf_instance.vnf_pkg_id + 'vnf_pkg_id': vnf_instance.vnf_pkg_id, + 'vnf_metadata': vnf_instance.vnf_metadata } vnf_instance_db_obj = models.VnfInstance() diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index da1dbe5e8..42acfab0f 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -80,7 +80,8 @@ def _model_non_instantiated_vnf_instance(**updates): 'tenant_id': uuidsentinel.tenant_id, 'vnfd_id': uuidsentinel.vnfd_id, 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, - 'vnfd_version': '1.0'} + 'vnfd_version': '1.0', + 'vnf_metadata': {"key": "value"}} if updates: vnf_instance.update(**updates) @@ -159,7 +160,8 @@ def _fake_vnf_instance_not_instantiated_response( 'vnfdVersion': '1.0', 'vnfSoftwareVersion': '1.0', 'vnfPkgId': uuidsentinel.vnf_pkg_id, - 'id': uuidsentinel.vnf_instance_id + 'id': uuidsentinel.vnf_instance_id, + 'metadata': {'key': 'value'} } if updates: diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 76120918c..132d79f8c 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -34,6 +34,26 @@ from tacker.tests import uuidsentinel from tacker.vnfm import vim_client +class FakeVNFMPlugin(mock.Mock): + + def __init__(self): + super(FakeVNFMPlugin, self).__init__() + self.vnf1_vnfd_id = 'eb094833-995e-49f0-a047-dfb56aaf7c4e' + self.vnf1_vnf_id = '91e32c20-6d1f-47a4-9ba7-08f5e5effe07' + self.vnf1_update_vnf_id = '91e32c20-6d1f-47a4-9ba7-08f5e5effaf6' + self.vnf2_vnfd_id = 'e4015e9f-1ef2-49fb-adb6-070791ad3c45' + self.vnf3_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' + + self.cp11_id = 'd18c8bae-898a-4932-bff8-d5eac981a9c9' + self.cp11_update_id = 'a18c8bae-898a-4932-bff8-d5eac981a9b8' + self.cp12_id = 'c8906342-3e30-4b2a-9401-a251a7a9b5dd' + self.cp12_update_id = 'b8906342-3e30-4b2a-9401-a251a7a9b5cc' + self.cp32_id = '3d1bd2a2-bf0e-44d1-87af-a2c6b2cad3ed' + self.cp32_update_id = '064c0d99-5a61-4711-9597-2a44dc5da14b' + + @ddt.ddt class TestController(base.TestCase): @@ -71,13 +91,15 @@ class TestController(base.TestCase): updates = {'vnfd_id': uuidsentinel.vnfd_id, 'vnf_instance_description': None, 'vnf_instance_name': None, - 'vnf_pkg_id': uuidsentinel.vnf_pkg_id} + 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, + 'vnf_metadata': {"key": "value"}} mock_vnf_instance_create.return_value =\ fakes.return_vnf_instance_model(**updates) req = fake_request.HTTPRequest.blank('/vnf_instances') - body = {'vnfdId': uuidsentinel.vnfd_id} + body = {'vnfdId': uuidsentinel.vnfd_id, + 'metadata': {"key": "value"}} req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' req.headers['Version'] = '2.6.1' @@ -115,14 +137,16 @@ class TestController(base.TestCase): updates = {'vnfd_id': uuidsentinel.vnfd_id, 'vnf_instance_description': 'SampleVnf Description', 'vnf_instance_name': 'SampleVnf', - 'vnf_pkg_id': uuidsentinel.vnf_pkg_id} + 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, + 'vnf_metadata': {"key": "value"}} mock_vnf_instance_create.return_value =\ fakes.return_vnf_instance_model(**updates) body = {'vnfdId': uuidsentinel.vnfd_id, "vnfInstanceName": "SampleVnf", - "vnfInstanceDescription": "SampleVnf Description"} + "vnfInstanceDescription": "SampleVnf Description", + 'metadata': {"key": "value"}} req = fake_request.HTTPRequest.blank('/vnf_instances') req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' @@ -161,7 +185,8 @@ class TestController(base.TestCase): updates = {'vnfd_id': uuidsentinel.vnfd_id, 'vnf_instance_description': None, 'vnf_instance_name': None, - 'vnf_pkg_id': uuidsentinel.vnf_pkg_id} + 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, + 'metadata': {'key': 'value'}} mock_vnf_instance_create.return_value =\ fakes.return_vnf_instance_model(**updates) @@ -196,6 +221,12 @@ class TestController(base.TestCase): 'expected_type': 'description'}, {'attribute': 'vnfInstanceDescription', 'value': 123, 'expected_type': 'description'}, + {'attribute': 'metadata', 'value': ['val1', 'val2'], + 'expected_type': 'object'}, + {'attribute': 'metadata', 'value': True, + 'expected_type': 'object'}, + {'attribute': 'metadata', 'value': 123, + 'expected_type': 'object'}, ) @ddt.unpack def test_create_with_invalid_request_body( @@ -203,7 +234,8 @@ class TestController(base.TestCase): """value of attribute in body is of invalid type""" body = {"vnfInstanceName": "SampleVnf", "vnfdId": "29c770a3-02bc-4dfc-b4be-eb173ac00567", - "vnfInstanceDescription": "VNF Description"} + "vnfInstanceDescription": "VNF Description", + "metadata": {"key": "value"}} req = fake_request.HTTPRequest.blank('/vnf_instances') body.update({attribute: value}) req.body = jsonutils.dump_as_bytes(body) @@ -223,13 +255,20 @@ class TestController(base.TestCase): "{attribute}. " "Value: {value}. {value} is " "not of type 'string'". format(value=value, attribute=attribute)) + elif expected_type == 'object': + expected_message = ("Invalid input for field/attribute " + "{attribute}. " "Value: {value}. {value} is " + "not of type 'object'". + format(value=value, attribute=attribute, + expected_type=expected_type)) self.assertEqual(expected_message, exception.msg) - @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') def test_create_non_existing_vnf_package_vnfd(self, mock_vnf_by_id): mock_vnf_by_id.side_effect = exceptions.VnfPackageVnfdNotFound - body = {'vnfdId': uuidsentinel.vnfd_id} + body = {'vnfdId': uuidsentinel.vnfd_id, + 'metadata': {"key": "value"}} req = fake_request.HTTPRequest.blank('/vnf_instances') req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' @@ -239,7 +278,8 @@ class TestController(base.TestCase): body=body) def test_create_without_vnfd_id(self): - body = {"vnfInstanceName": "SampleVnfInstance"} + body = {"vnfInstanceName": "SampleVnfInstance", + 'metadata': {"key": "value"}} req = fake_request.HTTPRequest.blank( '/vnf_instances') req.body = jsonutils.dump_as_bytes(body) @@ -259,16 +299,21 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.METHOD_NOT_ALLOWED, resp.status_code) - @ddt.data({'name': "A" * 256, 'description': "VNF Description"}, - {'name': 'Fake-VNF', 'description': "A" * 1025}) + @ddt.data({'name': "A" * 256, 'description': "VNF Description", + 'meta': {"key": "value"}}, + {'name': 'Fake-VNF', 'description': "A" * 1025, + 'meta': {"key": "value"}}, + {'name': 'Fake-VNF', 'description': "VNF Description", + 'meta': {"key": "v" * 256}}) @ddt.unpack def test_create_max_length_exceeded_for_vnf_name_and_description( - self, name, description): + self, name, description, meta): # vnf instance_name and description with length greater than max # length defined body = {"vnfInstanceName": name, "vnfdId": uuidsentinel.vnfd_id, - "vnfInstanceDescription": description} + "vnfInstanceDescription": description, + "metadata": meta} req = fake_request.HTTPRequest.blank( '/vnf_instances') req.body = jsonutils.dump_as_bytes(body)