commit 7e49a141393d2e33842b7a61a47af382be0d7c25 Author: Aldinson Esto Date: Thu Aug 27 11:15:36 2020 +0900 Support attribute filtering for List VNF Supported Attribute Filtering, filter defined in SOL013. 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: I98ce706a5081bbfa4947d782e2e28cd7667c5fdd (cherry picked from commit 60dd90d02bd564c5bcb2f978f5843e2d46b270c3) diff --git a/api-ref/source/v1/vnflcm.inc b/api-ref/source/v1/vnflcm.inc index 67a2096..01e1767 100644 --- a/api-ref/source/v1/vnflcm.inc +++ b/api-ref/source/v1/vnflcm.inc @@ -410,6 +410,8 @@ List VNF Instance .. rest_method:: GET /vnflcm/v1/vnf_instances The GET method queries information about multiple VNF instances. +In Victoria release, added attribute-based filtering expression (for VnfInstance) +that follows clause 5.2 of ETSI GS NFV SOL13. Response Codes -------------- diff --git a/tacker/api/views/vnf_lcm.py b/tacker/api/views/vnf_lcm.py index 868126f..dea88d5 100644 --- a/tacker/api/views/vnf_lcm.py +++ b/tacker/api/views/vnf_lcm.py @@ -13,11 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +from tacker.api import views as base from tacker.common import utils from tacker.objects import fields +from tacker.objects import vnf_instance as _vnf_instance -class ViewBuilder(object): +class ViewBuilder(base.BaseViewBuilder): + + FLATTEN_ATTRIBUTES = _vnf_instance.VnfInstance.FLATTEN_ATTRIBUTES def _get_links(self, vnf_instance): links = { @@ -54,7 +58,8 @@ class ViewBuilder(object): return {"_links": links} - def _get_vnf_instance_info(self, vnf_instance): + def _get_vnf_instance_info(self, + vnf_instance, api_version=None): vnf_instance_dict = vnf_instance.to_dict() if 'vnf_metadata' in vnf_instance_dict: metadata_val = vnf_instance_dict.pop('vnf_metadata') @@ -63,6 +68,9 @@ class ViewBuilder(object): vnf_instance_dict = utils.convert_snakecase_to_camelcase( vnf_instance_dict) + if api_version == "2.6.1": + del vnf_instance_dict["vnfPkgId"] + links = self._get_links(vnf_instance) vnf_instance_dict.update(links) @@ -74,6 +82,6 @@ class ViewBuilder(object): def show(self, vnf_instance): return self._get_vnf_instance_info(vnf_instance) - def index(self, vnf_instances): - return [self._get_vnf_instance_info(vnf_instance) + def index(self, vnf_instances, api_version=None): + return [self._get_vnf_instance_info(vnf_instance, api_version) for vnf_instance in vnf_instances] diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index a622e5f..3ff96d2 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -207,8 +207,16 @@ class VnfLcmController(wsgi.Controller): @wsgi.expected_errors((http_client.FORBIDDEN)) def index(self, request): context = request.environ['tacker.context'] - vnf_instances = objects.VnfInstanceList.get_all(context) - return self._view_builder.index(vnf_instances) + context.can(vnf_lcm_policies.VNFLCM % 'index') + + filters = request.GET.get('filter') + filters = self._view_builder.validate_filter(filters) + + vnf_instances = objects.VnfInstanceList.get_by_filters( + request.context, filters=filters) + + api_version = request.headers['Version'] + return self._view_builder.index(vnf_instances, api_version) @check_vnf_state(action="delete", instantiation_state=[fields.VnfInstanceState.NOT_INSTANTIATED], diff --git a/tacker/objects/vnf_instance.py b/tacker/objects/vnf_instance.py index e4c1371..16a2f17 100644 --- a/tacker/objects/vnf_instance.py +++ b/tacker/objects/vnf_instance.py @@ -18,15 +18,18 @@ from oslo_utils import timeutils from oslo_utils import uuidutils from oslo_versionedobjects import base as ovoo_base from sqlalchemy.orm import joinedload +from sqlalchemy_filters import apply_filters from tacker._i18n import _ from tacker.common import exceptions +from tacker.common import utils from tacker.db import api as db_api from tacker.db.db_sqlalchemy import api from tacker.db.db_sqlalchemy import models from tacker import objects from tacker.objects import base from tacker.objects import fields +from tacker.objects import vnf_instantiated_info LOG = logging.getLogger(__name__) @@ -100,6 +103,23 @@ def _vnf_instance_list(context, columns_to_join=None): return query.all() +@db_api.context_manager.reader +def _vnf_instance_list_by_filter(context, columns_to_join=None, + filters=None): + query = api.model_query(context, models.VnfInstance, + read_deleted="no", + project_only=True) + + if columns_to_join: + for column in columns_to_join: + query = query.options(joinedload(column)) + + if filters: + query = apply_filters(query, filters) + + return query.all() + + def _make_vnf_instance_list(context, vnf_instance_list, db_vnf_instance_list, expected_attrs): vnf_instance_cls = VnfInstance @@ -143,6 +163,34 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, 'vnf_metadata': fields.DictOfStringsField(nullable=True, default={}) } + ALL_ATTRIBUTES = { + 'id': ('id', "string", 'VnfInstance'), + 'vnfInstanceName': ('vnf_instance_name', 'string', 'VnfInstance'), + 'vnfInstanceDescription': ( + 'vnf_instance_description', 'string', 'VnfInstance'), + 'instantiationState': ('instantiation_state', 'string', 'VnfInstance'), + 'taskState': ('task_state', 'string', 'VnfInstance'), + 'vnfdId': ('vnfd_id', 'string', 'VnfInstance'), + 'vnfProvider': ('vnf_provider', 'string', 'VnfInstance'), + 'vnfProductName': ('vnf_product_name', 'string', 'VnfInstance'), + 'vnfSoftwareVersion': ( + 'vnf_software_version', 'string', 'VnfInstance'), + 'vnfdVersion': ('vnfd_version', 'string', 'VnfInstance'), + 'tenantId': ('tenant_id', 'string', 'VnfInstance'), + 'vnfPkgId': ('vnf_pkg_id', 'string', 'VnfInstance'), + 'vimConnectionInfo/*': ('vim_connection_info', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfInstance"}), + 'metadata/*': ('vnf_metadata', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfInstance"}), + } + + ALL_ATTRIBUTES.update( + vnf_instantiated_info.InstantiatedVnfInfo.ALL_ATTRIBUTES) + + FLATTEN_ATTRIBUTES = utils.flatten_dict(ALL_ATTRIBUTES.copy()) + def __init__(self, context=None, **kwargs): super(VnfInstance, self).__init__(context, **kwargs) self.obj_set_defaults() @@ -286,3 +334,14 @@ class VnfInstanceList(ovoo_base.ObjectListBase, base.TackerObject): columns_to_join=expected_attrs) return _make_vnf_instance_list(context, cls(), db_vnf_instances, expected_attrs) + + @base.remotable_classmethod + def get_by_filters(cls, context, filters=None, + expected_attrs=None): + expected_attrs = ["instantiated_vnf_info"] + db_vnf_instances = _vnf_instance_list_by_filter( + context, columns_to_join=expected_attrs, + filters=filters) + + return _make_vnf_instance_list(context, cls(), db_vnf_instances, + expected_attrs) diff --git a/tacker/objects/vnf_instantiated_info.py b/tacker/objects/vnf_instantiated_info.py index 2807f79..38bfd53 100644 --- a/tacker/objects/vnf_instantiated_info.py +++ b/tacker/objects/vnf_instantiated_info.py @@ -16,6 +16,7 @@ from oslo_log import log as logging from tacker.common import exceptions +from tacker.common import utils from tacker.db import api as db_api from tacker.db.db_sqlalchemy import api from tacker.db.db_sqlalchemy import models @@ -78,9 +79,48 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat, default=None), 'additional_params': fields.DictOfStringsField(nullable=True, default={}) + } + ALL_ATTRIBUTES = { + 'instantiatedInfo': { + 'flavourId': ('id', 'string', 'VnfInstantiatedInfo'), + 'vnfInstanceId': + ('vnf_instance_id', 'string', 'VnfInstantiatedInfo'), + 'vnfState': ('vnf_state', 'string', 'VnfInstantiatedInfo'), + 'instanceId': ('instance_id', 'string', 'VnfInstantiatedInfo'), + 'instantiationLevelId': + ('instantiation_level_id', 'string', 'VnfInstantiatedInfo'), + 'extCpInfo/*': ('ext_cp_info', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfInstantiatedInfo"}), + 'extVirtualLinkInfo/*': ('ext_virtual_link_info', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfInstantiatedInfo"}), + 'extManagedVirtualLinkInfo/*': ( + 'ext_managed_virtual_link_info', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfInstantiatedInfo"}), + 'vnfcResourceInfo/*': ( + 'vnfc_resource_info', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfInstantiatedInfo"}), + 'vnfVirtualLinkResourceInfo/*': ( + 'vnf_virtual_link_resource_info', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfInstantiatedInfo"}), + 'virtualStorageResourceInfo/*': ( + 'virtual_storage_resource_info', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfInstantiatedInfo"}), + 'additionalParams/*': ( + 'additional_params', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfInstantiatedInfo"}), + } } + FLATTEN_ATTRIBUTES = utils.flatten_dict(ALL_ATTRIBUTES.copy()) + @staticmethod def _from_db_object(context, inst_vnf_info, db_inst_vnf_info): diff --git a/tacker/tests/unit/objects/test_vnf_instance.py b/tacker/tests/unit/objects/test_vnf_instance.py index 175acd7..8608665 100644 --- a/tacker/tests/unit/objects/test_vnf_instance.py +++ b/tacker/tests/unit/objects/test_vnf_instance.py @@ -130,6 +130,19 @@ class TestVnfInstance(SqlTestCase): self.assertTrue(result.objects, list) self.assertTrue(result.objects) + def test_vnf_instance_list_get_by_filters(self): + vnf_instance_data = fakes.get_vnf_instance_data( + self.vnf_package.vnfd_id) + vnf_instance = objects.VnfInstance(context=self.context, + **vnf_instance_data) + vnf_instance.create() + filters = {'field': 'instantiation_state', 'model': 'VnfInstance', + 'value': 'NOT_INSTANTIATED', + 'op': '=='} + vnf_instance_list = objects.VnfInstanceList.get_by_filters( + self.context, filters=filters) + self.assertEqual(1, len(vnf_instance_list)) + @mock.patch('tacker.objects.vnf_instance._destroy_vnf_instance') def test_destroy(self, mock_vnf_destroy): vnf_instance_data = fakes.get_vnf_instance_data( diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index 42acfab..e7295d2 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -172,7 +172,7 @@ def _fake_vnf_instance_not_instantiated_response( def fake_vnf_instance_response( instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED, - **updates): + api_version=None, **updates): if instantiated_state == fields.VnfInstanceState.NOT_INSTANTIATED: data = _fake_vnf_instance_not_instantiated_response(**updates) else: @@ -191,6 +191,9 @@ def fake_vnf_instance_response( data['instantiatedVnfInfo'] = _instantiated_vnf_info() + if api_version == '2.6.1': + del data['vnfPkgId'] + return data diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 132d79f..4efaf9e 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -17,11 +17,12 @@ from unittest import mock import ddt from oslo_serialization import jsonutils from six.moves import http_client +import urllib from webob import exc from tacker.api.vnflcm.v1 import controller from tacker.common import exceptions -import tacker.conductor.conductorrpc.vnf_lcm_rpc as vnf_lcm_rpc +from tacker.conductor.conductorrpc.vnf_lcm_rpc import VNFLcmRPCAPI from tacker.extensions import nfvo from tacker import objects from tacker.objects import fields @@ -327,7 +328,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(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") + @mock.patch.object(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, @@ -384,7 +385,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(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") + @mock.patch.object(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, @@ -415,7 +416,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(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") + @mock.patch.object(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, @@ -448,7 +449,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(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") + @mock.patch.object(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, @@ -480,7 +481,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(vnf_lcm_rpc.VNFLcmRPCAPI, "instantiate") + @mock.patch.object(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, @@ -821,7 +822,7 @@ class TestController(base.TestCase): @mock.patch.object(objects.VnfInstance, "get_by_id") @mock.patch.object(objects.VnfInstance, "save") - @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "terminate") + @mock.patch.object(VNFLcmRPCAPI, "terminate") @ddt.data({'terminationType': 'FORCEFUL'}, {'terminationType': 'GRACEFUL'}, {'terminationType': 'GRACEFUL', @@ -961,7 +962,7 @@ class TestController(base.TestCase): @mock.patch.object(objects.VnfInstance, "get_by_id") @mock.patch.object(objects.VnfInstance, "save") - @mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "heal") + @mock.patch.object(VNFLcmRPCAPI, "heal") @ddt.data({'cause': 'healing'}, {}) def test_heal(self, body, mock_rpc_heal, mock_save, mock_vnf_by_id): @@ -1096,24 +1097,29 @@ class TestController(base.TestCase): self.assertEqual(expected_message, exception.msg) - @mock.patch.object(objects.VnfInstanceList, "get_all") - def test_index(self, mock_vnf_list): + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + @ddt.data(' ', '2.6.1') + def test_index(self, api_version, mock_vnf_list): req = fake_request.HTTPRequest.blank('/vnf_instances') + req.headers['Version'] = api_version vnf_instance_1 = fakes.return_vnf_instance() vnf_instance_2 = fakes.return_vnf_instance( fields.VnfInstanceState.INSTANTIATED) mock_vnf_list.return_value = [vnf_instance_1, vnf_instance_2] resp = self.controller.index(req) - expected_result = [fakes.fake_vnf_instance_response(), + expected_result = [fakes.fake_vnf_instance_response( + api_version=api_version), fakes.fake_vnf_instance_response( - fields.VnfInstanceState.INSTANTIATED)] + fields.VnfInstanceState.INSTANTIATED, + api_version=api_version)] self.assertEqual(expected_result, resp) - @mock.patch.object(objects.VnfInstanceList, "get_all") - def test_index_empty_response(self, mock_vnf_list): + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + @ddt.data(' ', '2.6.1') + def test_index_empty_response(self, api_version, mock_vnf_list): req = fake_request.HTTPRequest.blank('/vnf_instances') - + req.headers['Version'] = api_version mock_vnf_list.return_value = [] resp = self.controller.index(req) self.assertEqual([], resp) @@ -1212,3 +1218,212 @@ class TestController(base.TestCase): "is in this state.") self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, resp.json['conflictingRequest']['message']) + + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + @ddt.data( + {'filter': "(eq,vnfInstanceName,'dummy_name')"}, + {'filter': "(in,vnfInstanceName,'dummy_name')"}, + {'filter': "(cont,vnfInstanceName,'dummy_name')"}, + {'filter': "(neq,vnfInstanceName,'dummy_name')"}, + {'filter': "(nin,vnfInstanceName,'dummy_name')"}, + {'filter': "(ncont,vnfInstanceName,'dummy_name')"}, + {'filter': "(gt,vnfdVersion, 1)"}, + {'filter': "(gte,vnfdVersion, 1)"}, + {'filter': "(lt,vnfdVersion, 1)"}, + {'filter': "(lte,vnfdVersion, 1)"}, + ) + def test_index_filter_operator(self, filter_params, mock_vnf_list): + """Tests all supported operators in filter expression.""" + api_version = '2.6.1' + query = urllib.parse.urlencode(filter_params) + + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_instances?' + query) + req.headers['Version'] = api_version + + vnf_instance_1 = fakes.return_vnf_instance() + vnf_instance_2 = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + + mock_vnf_list.return_value = [vnf_instance_1, vnf_instance_2] + res_dict = self.controller.index(req) + + expected_result = [fakes.fake_vnf_instance_response( + api_version=api_version), + fakes.fake_vnf_instance_response( + fields.VnfInstanceState.INSTANTIATED, + api_version=api_version)] + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + def test_index_filter_combination(self, mock_vnf_list): + """Test multiple filter parameters separated by semicolon.""" + params = { + 'filter': "(eq,vnfInstanceName,'dummy_name');" + "(eq,vnfInstanceDescription,'dummy_desc')"} + + api_version = '2.6.1' + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_instances?' + query) + req.headers['Version'] = '2.6.1' + + vnf_instance_1 = fakes.return_vnf_instance() + vnf_instance_2 = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + + mock_vnf_list.return_value = [vnf_instance_1, vnf_instance_2] + res_dict = self.controller.index(req) + + expected_result = [fakes.fake_vnf_instance_response( + api_version=api_version), + fakes.fake_vnf_instance_response( + fields.VnfInstanceState.INSTANTIATED, + api_version=api_version)] + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + @ddt.data( + {'filter': "(eq,vnfInstanceName,dummy_value)"}, + {'filter': "(eq,vnfInstanceDescription,dummy value)"}, + {'filter': "(eq,instantiationState,'NOT_INSTANTIATED')"}, + {'filter': "(eq,taskState,'ACTIVE')"}, + {'filter': "(eq,vnfdId,'dummy_vnfd_id')"}, + {'filter': "(eq,vnfProvider,'''dummy ''hi'' value''')"}, + {'filter': "(eq,vnfProductName,'dummy_product_name')"}, + {'filter': "(eq,vnfSoftwareVersion,'1.0')"}, + {'filter': "(eq,vnfdVersion,'dummy_vnfd_version')"}, + {'filter': "(eq,tenantId,'dummy_tenant_id')"}, + {'filter': "(eq,vnfPkgId,'dummy_pkg_id')"}, + {'filter': "(eq,vimConnectionInfo/accessInfo/region,'dummy_id')"}, + {'filter': "(eq,instantiatedInfo/flavourId,'dummy_flavour')"}, + {'filter': "(eq,instantiatedInfo/vnfInstanceId,'dummy_vnf_id')"}, + {'filter': "(eq,instantiatedInfo/vnfState,'ACTIVE')"}, + {'filter': "(eq,instantiatedInfo/instanceId,'dummy_vnf_id')"}, + {'filter': + "(eq,instantiatedInfo/instantiationLevelId,'dummy_level_id')"}, + {'filter': "(eq,instantiatedInfo/extCpInfo/id,'dummy_id')"}, + {'filter': "(eq,instantiatedInfo/extVirtualLinkInfo/name,'dummy')"}, + {'filter': + "(eq,instantiatedInfo/extManagedVirtualLinkInfo/id,'dummy_id')"}, + {'filter': "(eq,instantiatedInfo/vnfcResourceInfo/vduId,'dummy_id')"}, + {'filter': + "(eq,instantiatedInfo/vnfVirtualLinkResourceInfo/" + "vnfVirtualLinkDescId,'dummy_id')"}, + {'filter': + "(eq,instantiatedInfo/virtualStorageResourceInfo/" + "virtualStorageDescId,'dummy_id')"}, + {'filter': "(eq,instantiatedInfo/additionalParams/error,'dummy')"}, + ) + def test_index_filter_attributes(self, filter_params, + mock_vnf_list): + """Test various attributes supported for filter parameter.""" + api_version = '2.6.1' + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_instances?' + query) + req.headers['Version'] = '2.6.1' + + vnf_instance_1 = fakes.return_vnf_instance() + vnf_instance_2 = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + + mock_vnf_list.return_value = [vnf_instance_1, vnf_instance_2] + res_dict = self.controller.index(req) + + expected_result = [fakes.fake_vnf_instance_response( + api_version=api_version), + fakes.fake_vnf_instance_response( + fields.VnfInstanceState.INSTANTIATED, + api_version=api_version)] + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + @ddt.data( + {'filter': "(eq,vnfInstanceName,value"}, + {'filter': "eq,vnfInstanceName,value)"}, + {'filter': "(eq,vnfInstanceName,value);"}, + {'filter': "(eq , vnfInstanceName ,value)"}, + ) + def test_index_filter_invalid_expression(self, filter_params, + mock_vnf_list): + """Test invalid filter expression.""" + api_version = '2.6.1' + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_instances?' + query) + req.headers['Version'] = api_version + + self.assertRaises(exceptions.ValidationError, + self.controller.index, req) + + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + @ddt.data( + {'filter': "(eq,vnfInstanceName,singl'quote)"}, + {'filter': "(eq,vnfInstanceName,three''' quotes)"}, + {'filter': "(eq,vnfInstanceName,round ) bracket)"}, + {'filter': "(eq,vnfInstanceName,'dummy 'hi' value')"}, + {'filter': "(eq,vnfInstanceName,'dummy's value')"}, + {'filter': "(eq,vnfInstanceName,'three ''' quotes')"}, + ) + def test_index_filter_invalid_string_values(self, filter_params, + mock_vnf_list): + """Test invalid string values as per ETSI NFV SOL013 5.2.2.""" + api_version = '2.6.1' + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_instances?' + query) + req.headers['Version'] = api_version + self.assertRaises(exceptions.ValidationError, + self.controller.index, req) + + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + @ddt.data( + {'filter': '(eq,vnfdId,value1,value2)'}, + {'filter': '(fake,vnfdId,dummy_vnfd_id)'}, + {'filter': '(,vnfdId,dummy_vnfd_id)'}, + ) + def test_index_filter_invalid_operator(self, filter_params, + mock_vnf_list): + """Test invalid operator in filter expression.""" + api_version = '2.6.1' + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_instances?' + query) + req.headers['Version'] = api_version + self.assertRaises(exceptions.ValidationError, + self.controller.index, req) + + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + @ddt.data( + {'filter': '(eq,fakeattr,fakevalue)'}, + {'filter': '(eq,,fakevalue)'}, + ) + def test_index_filter_invalid_attribute(self, filter_params, + mock_vnf_list): + """Test invalid attribute in filter expression.""" + api_version = '2.6.1' + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_instances?' + query) + req.headers['Version'] = api_version + self.assertRaises(exceptions.ValidationError, + self.controller.index, req) + + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + @ddt.data( + {'filter': '(eq,data/size,fake_value)'}, + {'filter': '(gt,data/createdAt,fake_value)'}, + {'filter': '(eq,data/minDisk,fake_value)'}, + {'filter': '(eq,data/minRam,fake_value)'}, + ) + def test_index_filter_invalid_value_type(self, filter_params, + mock_vnf_list): + """Test values which doesn't match with attribute data type.""" + api_version = '2.6.1' + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_instances?' + query) + req.headers['Version'] = api_version + self.assertRaises(exceptions.ValidationError, + self.controller.index, req)