From f5f29b34fa7c8be91ddfba4b75b8726eb3bed37a Mon Sep 17 00:00:00 2001 From: Aldinson Esto Date: Wed, 10 Feb 2021 05:07:46 +0900 Subject: [PATCH] Support enhancement for Get Subscription List - Added support for the following attributes: * operationStates * vnfInstanceSubscriptionFilter - The query information is enhanced by improving filtering expressions and operators Implements: blueprint support-fundamental-lcm Spec: https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-fundamental-vnf-lcm-based-on-ETSI-NFV.html Change-Id: I3d2abc25370b0a34793a50edffeccc0bc2a2bfe1 --- api-ref/source/v1/vnflcm.inc | 2 + tacker/api/views/vnf_lcm.py | 8 + tacker/api/views/vnf_subscriptions.py | 80 ++++++ tacker/api/vnflcm/v1/controller.py | 75 ++++- tacker/common/utils.py | 10 + tacker/db/db_sqlalchemy/models.py | 43 ++- tacker/objects/vnf_lcm_subscriptions.py | 261 ++++++++++++++++++ tacker/tests/functional/sol/vnflcm/base.py | 11 + .../test_vnf_instance_with_user_data.py | 66 +++++ tacker/tests/unit/vnflcm/fakes.py | 51 ++++ tacker/tests/unit/vnflcm/test_controller.py | 138 +++++++++ 11 files changed, 727 insertions(+), 18 deletions(-) create mode 100644 tacker/api/views/vnf_subscriptions.py diff --git a/api-ref/source/v1/vnflcm.inc b/api-ref/source/v1/vnflcm.inc index b094822e0..64017edc7 100644 --- a/api-ref/source/v1/vnflcm.inc +++ b/api-ref/source/v1/vnflcm.inc @@ -1297,8 +1297,10 @@ Response Parameters - id: subscription_id_response - filter: filter + - vnfInstanceSubscriptionFilter: vnf_instance_subscription_filter - notificationTypes: filter_notification_types - operationTypes: filter_operation_types + - operationStates: filter_operation_states - callbackUri: callback_uri - _links: vnf_instance_links diff --git a/tacker/api/views/vnf_lcm.py b/tacker/api/views/vnf_lcm.py index 5739651db..c951a205e 100644 --- a/tacker/api/views/vnf_lcm.py +++ b/tacker/api/views/vnf_lcm.py @@ -203,6 +203,10 @@ class ViewBuilder(base.BaseViewBuilder): 'callbackUri': vnf_lcm_subscription.callback_uri, } + # TODO(esto.aln): To remove all processing that are + # related to list subscription from vnf_lcm.py and + # transfer these to vnf_subscriptions.py, but this + # will be handled in a future patch. def _subscription_filter( self, subscription_data, @@ -266,6 +270,10 @@ class ViewBuilder(base.BaseViewBuilder): def subscription_create(self, vnf_lcm_subscription, filter): return self._get_vnf_lcm_subscription(vnf_lcm_subscription, filter) + # TODO(esto.aln): To remove all processing that are + # related to list subscription from vnf_lcm.py and + # transfer these to vnf_subscriptions.py, but this + # will be handled in a future patch. def subscription_list( self, vnf_lcm_subscriptions, diff --git a/tacker/api/views/vnf_subscriptions.py b/tacker/api/views/vnf_subscriptions.py new file mode 100644 index 000000000..b79830607 --- /dev/null +++ b/tacker/api/views/vnf_subscriptions.py @@ -0,0 +1,80 @@ +# 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 json + +from oslo_log import log as logging + +from tacker.api import views as base +import tacker.conf +from tacker.objects import vnf_lcm_subscriptions as vnf_subscription + +CONF = tacker.conf.CONF + +LOG = logging.getLogger(__name__) + + +class ViewBuilder(base.BaseViewBuilder): + + FLATTEN_ATTRIBUTES = vnf_subscription.LccnSubscription. \ + FLATTEN_ATTRIBUTES + + def _get_subscription_links(self, vnf_lcm_subscription): + if isinstance(vnf_lcm_subscription.id, str): + decode_id = vnf_lcm_subscription.id + else: + decode_id = vnf_lcm_subscription.id + return { + "_links": { + "self": { + "href": '%(endpoint)s/vnflcm/v1/subscriptions/%(id)s' % + { + "endpoint": CONF.vnf_lcm.endpoint_url, + "id": decode_id}}}} + + def _basic_subscription_info(self, vnf_lcm_subscription, filter=None): + if filter is None: + if 'filter' in vnf_lcm_subscription: + filter_dict = {} + + if 'filter' in vnf_lcm_subscription.filter: + filter_dict = json.loads( + vnf_lcm_subscription.filter.filter) + return { + 'id': vnf_lcm_subscription.id, + 'filter': filter_dict, + 'callbackUri': vnf_lcm_subscription.callback_uri, + } + return { + 'id': vnf_lcm_subscription.id, + 'callbackUri': vnf_lcm_subscription.callback_uri, + } + else: + return { + 'id': vnf_lcm_subscription.id, + 'filter': filter, + 'callbackUri': vnf_lcm_subscription.callback_uri, + } + + def _get_subscription(self, subscription): + subscription_response = self._basic_subscription_info(subscription) + + links = self._get_subscription_links(subscription) + subscription_response.update(links) + + return subscription_response + + def subscription_list( + self, + vnf_lcm_subscriptions): + return [self._get_subscription(subscription) + for subscription in vnf_lcm_subscriptions] diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index dfd755f40..50672d21d 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -43,6 +43,7 @@ from tacker.api.schemas import vnf_lcm from tacker.api import validation from tacker.api.views import vnf_lcm as vnf_lcm_view from tacker.api.views import vnf_lcm_op_occs as vnf_op_occs_view +from tacker.api.views import vnf_subscriptions as vnf_subscription_view from tacker.api.vnflcm.v1 import sync_resource from tacker.common import exceptions from tacker.common import utils @@ -162,6 +163,26 @@ def check_vnf_status_and_error_point(action, status=None): class VnfLcmController(wsgi.Controller): + notification_type_list = ['VnfLcmOperationOccurrenceNotification', + 'VnfIdentifierCreationNotification', + 'VnfIdentifierDeletionNotification'] + operation_type_list = ['INSTANTIATE', + 'SCALE', + 'SCALE_TO_LEVEL', + 'CHANGE_FLAVOUR', + 'TERMINATE', + 'HEAL', + 'OPERATE', + 'CHANGE_EXT_CONN', + 'MODIFY_INFO'] + operation_state_list = ['STARTING', + 'PROCESSING', + 'COMPLETED', + 'FAILED_TEMP', + 'FAILED', + 'ROLLING_BACK', + 'ROLLED_BACK'] + _view_builder_class = vnf_lcm_view.ViewBuilder def __init__(self): @@ -169,6 +190,7 @@ class VnfLcmController(wsgi.Controller): self.rpc_api = vnf_lcm_rpc.VNFLcmRPCAPI() self._vnfm_plugin = manager.TackerManager.get_service_plugins()['VNFM'] self._view_builder_op_occ = vnf_op_occs_view.ViewBuilder() + self._view_builder_subscription = vnf_subscription_view.ViewBuilder() def _get_vnf_instance_href(self, vnf_instance): return '/vnflcm/v1/vnf_instances/%s' % vnf_instance.id @@ -958,13 +980,13 @@ class VnfLcmController(wsgi.Controller): def subscription_list(self, request): nextpage_opaque_marker = "" paging = 1 + filter_string = "" re_url = request.path_url query_params = request.query_string + if query_params: query_params = parse.unquote(query_params) - LOG.debug("query_params %s" % query_params) - if query_params: query_param_list = query_params.split('&') for query_param in query_param_list: query_param_key_value = query_param.split('=') @@ -972,18 +994,57 @@ class VnfLcmController(wsgi.Controller): msg = _("Request query parameter error") return self._make_problem_detail( msg, 400, title='Bad Request') + if query_param_key_value[0] == 'filter': + filter_string = query_param_key_value[1] if query_param_key_value[0] == 'nextpage_opaque_marker': nextpage_opaque_marker = query_param_key_value[1] if query_param_key_value[0] == 'page': paging = int(query_param_key_value[1]) + if filter_string: + # check enumerations columns + for filter_segment in filter_string.split(';'): + filter_segment = re.sub( + r'\(|\)|\'', '', filter_segment) + filter_name = filter_segment.split(',')[1] + filter_value = filter_segment.split(',')[2] + if filter_name == 'notificationTypes': + if filter_value not in self.notification_type_list: + msg = (_("notificationTypes value mismatch: %s") + % filter_value) + return self._make_problem_detail(msg, 400, + title='Bad Request') + elif filter_name == 'operationTypes': + if filter_value not in self.operation_type_list: + msg = (_("operationTypes value mismatch: %s") + % filter_value) + return self._make_problem_detail(msg, 400, + title='Bad Request') + elif filter_name == 'operationStates': + if filter_value not in self.operation_state_list: + msg = (_("operationStates value mismatch: %s") + % filter_value) + return self._make_problem_detail(msg, 400, + title='Bad Request') + try: - vnf_lcm_subscriptions = ( - subscription_obj.LccnSubscriptionRequest. - vnf_lcm_subscriptions_list(request.context)) + filter_string_parsed = self._view_builder_subscription. \ + validate_filter(filter_string) + if nextpage_opaque_marker: + start_index = paging - 1 + else: + start_index = None + + vnf_lcm_subscriptions, last = ( + subscription_obj.LccnSubscriptionList. + get_by_filters(request.context, + read_deleted='no', + filters=filter_string_parsed, + nextpage_opaque_marker=start_index)) + LOG.debug("vnf_lcm_subscriptions %s" % vnf_lcm_subscriptions) - subscription_data, last = self._view_builder.subscription_list( - vnf_lcm_subscriptions, nextpage_opaque_marker, paging) + subscription_data = self._view_builder_subscription. \ + subscription_list(vnf_lcm_subscriptions) LOG.debug("last %s" % last) except Exception as e: LOG.error(traceback.format_exc()) diff --git a/tacker/common/utils.py b/tacker/common/utils.py index ecb8112e6..a62de970d 100644 --- a/tacker/common/utils.py +++ b/tacker/common/utils.py @@ -670,3 +670,13 @@ def str_to_num(value): return float(value) except ValueError: return None + + +def str_to_bytes(value): + """Convert string to bytes""" + if isinstance(value, str): + value = bytes(value, 'utf-8') + elif value is not None: + value = bytes(value) + + return value diff --git a/tacker/db/db_sqlalchemy/models.py b/tacker/db/db_sqlalchemy/models.py index b935359a0..024edff1a 100644 --- a/tacker/db/db_sqlalchemy/models.py +++ b/tacker/db/db_sqlalchemy/models.py @@ -267,16 +267,6 @@ class VnfResource(model_base.BASE, models.SoftDeleteMixin, resource_status = sa.Column(sa.String(255), nullable=False) -class VnfLcmSubscriptions(model_base.BASE, models.SoftDeleteMixin, - models.TimestampMixin): - """Contains all info about vnf LCM Subscriptions.""" - - __tablename__ = 'vnf_lcm_subscriptions' - id = sa.Column(sa.String(36), nullable=False, primary_key=True) - callback_uri = sa.Column(sa.String(255), nullable=False) - subscription_authentication = sa.Column(sa.JSON, nullable=True) - - class VnfLcmFilters(model_base.BASE): """Contains all info about vnf LCM filters.""" @@ -288,13 +278,44 @@ class VnfLcmFilters(model_base.BASE): nullable=False) filter = sa.Column(sa.JSON, nullable=False) vnf_products_from_providers = sa.Column(sa.JSON, nullable=True) - notification_types = sa.Column(sa.VARBINARY(255), nullable=True) + vnfd_ids = sa.Column(sa.Text(), nullable=True) + vnfd_ids_len = sa.Column(sa.Integer, nullable=True) + vnf_provider = sa.Column(sa.Text(), nullable=True) + vnf_product_name = sa.Column(sa.Text(), nullable=True) + vnf_software_version = sa.Column(sa.Text(), nullable=True) + vnfd_versions = sa.Column(sa.Text(), nullable=True) + vnfd_versions_len = sa.Column(sa.Integer, nullable=True) + vnf_instance_ids = sa.Column(sa.Text(), nullable=True) + vnf_instance_ids_len = sa.Column(sa.Integer, nullable=True) + vnf_instance_names = sa.Column(sa.Text(), nullable=True) + vnf_instance_names_len = sa.Column(sa.Integer, nullable=True) + notification_types = sa.Column( + sa.LargeBinary( + length=__maxsize__), + nullable=True) notification_types_len = sa.Column(sa.Integer, nullable=True) operation_types = sa.Column( sa.LargeBinary( length=__maxsize__), nullable=True) operation_types_len = sa.Column(sa.Integer, nullable=True) + operation_states = sa.Column(sa.Text(), nullable=True) + operation_states_len = sa.Column(sa.Integer, nullable=True) + + +class VnfLcmSubscriptions(model_base.BASE, models.SoftDeleteMixin, + models.TimestampMixin): + """Contains all info about vnf LCM Subscriptions.""" + + __tablename__ = 'vnf_lcm_subscriptions' + id = sa.Column(sa.String(36), nullable=False, primary_key=True) + callback_uri = sa.Column(sa.String(255), nullable=False) + subscription_authentication = sa.Column(sa.JSON, nullable=True) + + subscription_filter = orm.relationship( + VnfLcmFilters, + primaryjoin='and_(VnfLcmSubscriptions.id == ' + 'VnfLcmFilters.subscription_uuid)') class VnfLcmOpOccs(model_base.BASE, models.SoftDeleteMixin, diff --git a/tacker/objects/vnf_lcm_subscriptions.py b/tacker/objects/vnf_lcm_subscriptions.py index eeded3b9a..51f7fa7f2 100644 --- a/tacker/objects/vnf_lcm_subscriptions.py +++ b/tacker/objects/vnf_lcm_subscriptions.py @@ -11,15 +11,21 @@ # under the License. from oslo_log import log as logging +from oslo_serialization import jsonutils as json from oslo_utils import timeutils +from oslo_versionedobjects import base as ovoo_base +from sqlalchemy.orm import joinedload from sqlalchemy.sql import text +from sqlalchemy_filters import apply_filters from tacker.common import exceptions +from tacker.common import utils from tacker.common.utils import convert_string_to_snakecase import tacker.conf 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 @@ -308,6 +314,45 @@ def _add_filter_data(context, subscription_id, filter): new_entries) +@db_api.context_manager.reader +def _vnf_lcm_subscription_list_by_filters(context, + read_deleted=None, filters=None, nextpage_opaque_marker=None): + query = api.model_query(context, models.VnfLcmSubscriptions, + read_deleted=read_deleted, + project_only=True) + binary_columns = ['notification_types', 'operation_types'] + + if filters: + filter_data = json.dumps(filters) + if 'ChangeNotificationsFilter' in filter_data: + query = query.join(models.VnfLcmFilters) + + if 'and' in filters: + filters_and = [] + for filter in filters['and']: + if filter['field'] in binary_columns: + converted_value = utils.str_to_bytes(filter['value']) + filter['value'] = converted_value + filters_and.append(filter) + + filters = {'and': filters_and} + else: + if filters['field'] in binary_columns: + converted_value = utils.str_to_bytes(filters['value']) + filters.update({'value': converted_value}) + + query = apply_filters(query, filters) + + if nextpage_opaque_marker: + start_offset = CONF.vnf_lcm.subscription_num * nextpage_opaque_marker + return query.order_by( + models.VnfLcmSubscriptions.created_at).limit( + CONF.vnf_lcm.subscription_num + 1).offset( + start_offset).all() + else: + return query.order_by(models.VnfLcmSubscriptions.created_at).all() + + @db_api.context_manager.writer def _vnf_lcm_subscriptions_create(context, values, filter): with db_api.context_manager.writer.using(context): @@ -370,6 +415,47 @@ def _destroy_vnf_lcm_subscription(context, subscriptionId): raise e +@db_api.context_manager.reader +def _subscription_get_by_id(context, subscription_uuid, columns_to_join=None): + + query = api.model_query(context, models.VnfLcmSubscriptions, + read_deleted="no", project_only=True). \ + filter_by(id=subscription_uuid) + + if columns_to_join: + for column in columns_to_join: + query = query.options(joinedload(column)) + + result = query.first() + + if not result: + raise exceptions.NotFound(resource='Subscription', + id=subscription_uuid) + + return result + + +def _make_subscription_list(context, subscription_list, db_subscription_list, + expected_attrs=None): + subscription_cls = LccnSubscription + + subscription_list.objects = [] + cnt = 0 + last_flg = True + for db_subscription in db_subscription_list: + cnt = cnt + 1 + if cnt == CONF.vnf_lcm.subscription_num + 1: + last_flg = False + break + subscription_obj = subscription_cls._from_db_object( + context, subscription_cls(context), db_subscription, + expected_attrs=expected_attrs) + subscription_list.objects.append(subscription_obj) + + subscription_list.obj_reset_changes() + return subscription_list, last_flg + + @base.TackerObjectRegistry.register class LccnSubscriptionRequest(base.TackerObject, base.TackerPersistentObject): @@ -440,3 +526,178 @@ class LccnSubscriptionRequest(base.TackerObject, base.TackerPersistentObject): raise e return 204 + + +@base.TackerObjectRegistry.register +class ChangeNotificationsFilter( + base.TackerObject, base.TackerPersistentObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.UUIDField(nullable=False), + 'subscription_uuid': fields.UUIDField(nullable=False), + 'filter': fields.StringField(nullable=True), + 'vnf_products_from_providers': + fields.StringField(nullable=True), + 'vnfd_ids': fields.StringField(nullable=True), + 'vnfd_ids_len': fields.IntegerField( + nullable=True, default=0), + 'vnf_provider': fields.StringField(nullable=True), + 'vnf_product_name': fields.StringField(nullable=True), + 'vnf_software_version': fields.StringField(nullable=True), + 'vnfd_versions': fields.StringField(nullable=True), + 'vnfd_versions_len': fields.IntegerField( + nullable=True, default=0), + 'vnf_instance_ids': fields.StringField(nullable=True), + 'vnf_instance_ids_len': fields.IntegerField( + nullable=True, default=0), + 'vnf_instance_names': fields.StringField(nullable=True), + 'vnf_instance_names_len': fields.IntegerField( + nullable=True, default=0), + 'notification_types': fields.StringField(nullable=True), + 'notification_types_len': fields.IntegerField( + nullable=True, default=0), + 'operation_types': fields.StringField(nullable=True), + 'operation_types_len': fields.IntegerField( + nullable=True, default=0), + 'operation_states': fields.StringField(nullable=True), + 'operation_states_len': fields.IntegerField( + nullable=True, default=0), + } + + +@base.TackerObjectRegistry.register +class ChangeNotificationsFilterList( + ovoo_base.ObjectListBase, base.TackerObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'objects': fields.ListOfObjectsField('ChangeNotificationsFilter') + } + + +@base.TackerObjectRegistry.register +class LccnSubscription(base.TackerObject, base.TackerPersistentObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.UUIDField(nullable=False), + 'callback_uri': fields.StringField(nullable=False), + 'filter': fields.ObjectField( + 'ChangeNotificationsFilter', nullable=True), + } + + ALL_ATTRIBUTES = { + 'id': ('id', 'string', + 'VnfLcmSubscriptions'), + 'vnfdIds': ('vnfd_ids', 'string', + 'VnfLcmFilters'), + 'vnfProvider': ('vnf_provider', 'string', + 'VnfLcmFilters'), + 'vnfProductName': ('vnf_product_name', 'string', + 'VnfLcmFilters'), + 'vnfSoftwareVersion': ('vnf_software_version', 'string', + 'VnfLcmFilters'), + 'vnfdVersions': ('vnfd_versions', 'string', + 'VnfLcmFilters'), + 'vnfInstanceIds': ('vnf_instance_ids', 'string', + 'VnfLcmFilters'), + 'vnfInstanceNames': ('vnf_instance_names', 'string', + 'VnfLcmFilters'), + 'notificationTypes': ('notification_types', 'string', + 'VnfLcmFilters'), + 'operationTypes': ('operation_types', 'string', + 'VnfLcmFilters'), + 'operationStates': ('operation_states', 'string', + 'VnfLcmFilters'), + 'callbackUri': ('callback_uri', 'string', + 'VnfLcmSubscriptions'), + } + + FLATTEN_ATTRIBUTES = utils.flatten_dict(ALL_ATTRIBUTES.copy()) + + @staticmethod + def _from_db_object(context, subscription, db_subscription, + expected_attrs=None): + expected_attrs = expected_attrs or ['filter'] + + subscription._context = context + + for key in subscription.fields: + if key in ['filter']: + continue + db_key = key + setattr(subscription, key, db_subscription[db_key]) + + subscription._context = context + subscription._extra_attributes_from_db_object( + subscription, db_subscription, expected_attrs) + + subscription.obj_reset_changes() + return subscription + + @staticmethod + def _extra_attributes_from_db_object(subscription, db_subscription, + expected_attrs=None): + """Method to help with migration of extra attributes to objects.""" + + if expected_attrs is None: + expected_attrs = ['filter'] + + if 'filter' in expected_attrs: + subscription._load_subscription_filter( + db_subscription.get('filter')) + + def _load_subscription_filter(self, db_filter=_NO_DATA_SENTINEL): + if db_filter is _NO_DATA_SENTINEL: + subscription = self.get_by_id( + self._context, self.id, + expected_attrs=['filter']) + if 'filter' in subscription: + self.filter = \ + subscription.filter + self.filter.obj_reset_changes(recursive=True) + self.obj_reset_changes(['filter']) + else: + self.filter = \ + objects.ChangeNotificationsFilterList(objects=[]) + elif db_filter: + self.filter = base.obj_make_list( + self._context, objects.ChangeNotificationsFilterList( + self._context), objects.ChangeNotificationsFilter, + db_filter) + self.obj_reset_changes(['filter']) + + @base.remotable_classmethod + def get_by_id(cls, context, id, expected_attrs=None): + db_subscription = _subscription_get_by_id( + context, id, columns_to_join=expected_attrs) + return cls._from_db_object(context, cls(), db_subscription, + expected_attrs=expected_attrs) + + +@base.TackerObjectRegistry.register +class LccnSubscriptionList(ovoo_base.ObjectListBase, base.TackerObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'objects': fields.ListOfObjectsField('LccnSubscription') + } + + @base.remotable_classmethod + def get_by_filters(cls, context, read_deleted=None, + filters=None, nextpage_opaque_marker=None): + + db_subscriptions = _vnf_lcm_subscription_list_by_filters(context, + read_deleted=read_deleted, + filters=filters, + nextpage_opaque_marker=nextpage_opaque_marker) + return _make_subscription_list(context, cls(), db_subscriptions) diff --git a/tacker/tests/functional/sol/vnflcm/base.py b/tacker/tests/functional/sol/vnflcm/base.py index 8c3730a20..78dbc4260 100644 --- a/tacker/tests/functional/sol/vnflcm/base.py +++ b/tacker/tests/functional/sol/vnflcm/base.py @@ -372,6 +372,17 @@ class BaseVnfLcmTest(base.BaseTackerTest): return resp, body + def _list_subscription_filter(self, **kwargs): + params = kwargs.get('params', {}) + filter_variable = params['filter'] + subscriptions_list_filter_url = "%s?%s" % ( + self.base_subscriptions_url, filter_variable) + + resp, subscription_body = self.http_client.do_request( + subscriptions_list_filter_url, "GET") + + return resp, subscription_body + def _create_vnf_instance(self, vnfd_id, vnf_instance_name=None, vnf_instance_description=None): request_body = {'vnfdId': vnfd_id} diff --git a/tacker/tests/functional/sol/vnflcm/test_vnf_instance_with_user_data.py b/tacker/tests/functional/sol/vnflcm/test_vnf_instance_with_user_data.py index 5e1f37eec..d0da0aceb 100644 --- a/tacker/tests/functional/sol/vnflcm/test_vnf_instance_with_user_data.py +++ b/tacker/tests/functional/sol/vnflcm/test_vnf_instance_with_user_data.py @@ -226,6 +226,7 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): - Create subscription. - Get subscription informations. - Get list of subscriptions + - Get list of subscriptions with filter - Create VNF package. - Upload VNF package. - Create VNF instance. @@ -261,6 +262,71 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): resp, _ = self._list_subscription() self.assertEqual(200, resp.status_code) + # Subscription list filter 1 + filter_expr = { + 'filter': "filter=(eq,id,{})".format(body.get('id'))} + resp, subscription_body = self._list_subscription_filter( + params=filter_expr) + self.assertEqual(200, resp.status_code) + self.assertEqual(1, len(subscription_body)) + + # Subscription list filter 2 + filter_expr = { + 'filter': "filter=(neq,callbackUri,{})".format( + 'http://localhost:{}{}'.format( + vnflcm_base.FAKE_SERVER_MANAGER.SERVER_PORT, + os.path.join(vnflcm_base.MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName)))} + resp, subscription_body = self._list_subscription_filter( + params=filter_expr) + self.assertEqual(200, resp.status_code) + self.assertEqual(0, len(subscription_body)) + + # Subscription list filter 3 + filter_expr = { + 'filter': "filter=(neq,id,{})".format(body.get('id'))} + resp, subscription_body = self._list_subscription_filter( + params=filter_expr) + self.assertEqual(200, resp.status_code) + self.assertEqual(0, len(subscription_body)) + + # Subscription list filter 4 + filter_expr = { + 'filter': "filter=(eq,callbackUri,{})".format( + 'http://localhost:{}{}'.format( + vnflcm_base.FAKE_SERVER_MANAGER.SERVER_PORT, + os.path.join(vnflcm_base.MOCK_NOTIFY_CALLBACK_URL, + self._testMethodName)))} + resp, subscription_body = self._list_subscription_filter( + params=filter_expr) + self.assertEqual(200, resp.status_code) + self.assertEqual(1, len(subscription_body)) + + # Subscription list filter 5 + filter_expr = { + 'filter': "filter=(in,operationTypes,{})".format("sample")} + resp, subscription_body = self._list_subscription_filter( + params=filter_expr) + self.assertEqual(400, resp.status_code) + self.assertEqual(3, len(subscription_body)) + + # Subscription list filter 6 + filter_expr = { + 'filter': "filter=(eq,vnfSoftwareVersion,{})".format('1.0')} + resp, subscription_body = self._list_subscription_filter( + params=filter_expr) + self.assertEqual(200, resp.status_code) + self.assertEqual(1, len(subscription_body)) + + # Subscription list filter 7 + filter_expr = { + 'filter': "filter=(eq,operationTypes,{})".format( + "SCALE_TO_LEVEL")} + resp, subscription_body = self._list_subscription_filter( + params=filter_expr) + self.assertEqual(200, resp.status_code) + self.assertEqual(0, len(subscription_body)) + # Pre Setting: Create vnf package. sample_name = 'functional' csar_package_path = os.path.abspath( diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index 41d1c2a4e..9a452020f 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -1662,3 +1662,54 @@ def get_change_ext_conn_request_obj(): get_change_ext_conn_request_body()) return ChangeExtConnRequest.obj_from_primitive( body, context) + + +def _fake_subscription_obj(**updates): + subscription = { + 'id': uuidsentinel.subscription_id, + 'filter': { + "vnfInstanceSubscriptionFilter": { + "vnfdIds": [uuidsentinel.vnfd_id], + "vnfProductsFromProviders": { + "vnfProvider": "Vnf Provider 1", + "vnfProducts": [ + { + "vnfProductName": "Vnf Product 1", + "versions": [ + { + "vnfSoftwareVersion": "v1", + "vnfdVersions": [ + "vnfd.v1.1" + ] + } + ] + } + ] + }, + "vnfInstanceIds": [ + uuidsentinel.vnf_instance_id + ], + "vnfInstanceNames": ["Vnf Name 1"] + }, + "notificationTypes": [ + "VnfLcmOperationOccurrenceNotification" + ], + "operationTypes": ["INSTANTIATE"], + "operationStates": ["STARTING"] + }, + 'callback_uri': 'http://localhost/sample_callback_uri'} + + if updates: + subscription.update(**updates) + + return subscription + + +def return_subscription_object(**updates): + vnf_lcm_subscription = _fake_subscription_obj(**updates) + return vnf_lcm_subscription + + +def return_vnf_subscription_list(**updates): + vnc_lcm_subscription = return_subscription_object(**updates) + return [vnc_lcm_subscription] diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 44c5a8022..9950dcb18 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -27,6 +27,7 @@ from webob import exc from oslo_config import cfg from oslo_serialization import jsonutils +from tacker.api.views import vnf_subscriptions as vnf_subscription_view from tacker.api.vnflcm.v1 import controller from tacker.api.vnflcm.v1 import sync_resource from tacker.common import exceptions @@ -3873,3 +3874,140 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.CREATED, resp.status_code) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(vnf_subscription_view.ViewBuilder, + "subscription_list") + @mock.patch.object(vnf_subscription_view.ViewBuilder, + "validate_filter") + @mock.patch.object(objects.LccnSubscriptionList, + "get_by_filters") + def test_subscription_list_all(self, + mock_subscription_list, + mock_subscription_filter, + mock_subscription_view, + mock_get_service_plugins): + mock_subscription_filter.return_value = None + last = True + req = fake_request.HTTPRequest.blank('/subscriptions') + req.method = 'GET' + mock_subscription_list.return_value = [fakes. + return_vnf_subscription_list(), last] + mock_subscription_view.return_value = fakes. \ + return_vnf_subscription_list() + resp = req.get_response(self.app) + expected_result = fakes.return_vnf_subscription_list() + self.assertEqual(200, resp.status_code) + self.assertEqual(expected_result, resp.json) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(vnf_subscription_view.ViewBuilder, + "subscription_list") + @mock.patch.object(vnf_subscription_view.ViewBuilder, + "validate_filter") + @mock.patch.object(objects.LccnSubscriptionList, + "get_by_filters") + def test_subscription_list_empty(self, + mock_subscription_list, + mock_subscription_filter, + mock_subscription_view, + mock_get_service_plugins): + mock_subscription_filter.return_value = None + last = True + req = fake_request.HTTPRequest.blank('/subscriptions') + req.method = 'GET' + mock_subscription_list.return_value = [fakes. + return_vnf_subscription_list(), last] + mock_subscription_view.return_value = [] + resp = req.get_response(self.app) + self.assertEqual(200, resp.status_code) + self.assertEqual([], resp.json) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(vnf_subscription_view.ViewBuilder, + "validate_filter") + def test_subscription_list_error(self, + mock_subscription_filter, + mock_get_service_plugins): + req = fake_request.HTTPRequest.blank( + '/subscriptions') + req.method = 'GET' + mock_subscription_filter.side_effect = Exception + + resp = req.get_response(self.app) + self.assertEqual(500, resp.status_code) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + @mock.patch.object(vnf_subscription_view.ViewBuilder, + "subscription_list") + @mock.patch.object(vnf_subscription_view.ViewBuilder, + "validate_filter") + @mock.patch.object(objects.LccnSubscriptionList, + "get_by_filters") + @ddt.data( + {'operator': "eq", 'key': 'id', + 'value': uuidsentinel.subscription_id}, + {'operator': "cont", 'key': 'callbackUri', + 'value': 'http://localhost/sample_callback_uri'}, + {'operator': "neq", 'key': 'id', + 'value': uuidsentinel.subscription_id}, + {'operator': "eq", 'key': 'notificationTypes', + 'value': 'VnfLcmOperationOccurrenceNotification'}, + {'operator': "neq", 'key': 'operationTypes', 'value': 'INSTANTIATE'}, + {'operator': "cont", 'key': 'operationStates', 'value': 'STARTING'} + ) + @ddt.unpack + def test_subscription_list_filter(self, + mock_subscription_list, + mock_subscription_filter, + mock_subscription_view, + mock_get_service_plugins, + operator, key, value): + """Tests all supported operators in filter expression.""" + filters = { + 'filter': ("(%s,%s,%s)" % (operator, key, value)) + } + query = urllib.parse.urlencode(filters) + + req = fake_request.HTTPRequest.blank( + '/subscriptions?' + query) + req.method = 'GET' + body_2 = {} + mock_subscription_filter.return_value = filters + last = True + if operator == 'neq': + body_2 = {key: uuidsentinel.subscription_id_2} + + mock_subscription_list.return_value = [fakes. + return_vnf_subscription_list(), last] + mock_subscription_view.return_value = fakes. \ + return_vnf_subscription_list() + + resp = req.get_response(self.app) + expected_result = fakes.return_vnf_subscription_list(**body_2) + + if operator == 'neq': + self.assertNotEqual(expected_result, resp.json) + else: + self.assertEqual(expected_result, resp.json) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': + test_nfvo_plugin.FakeVNFMPlugin()}) + def test_subscription_list_filter_error(self, + mock_get_service_plugins): + + req = fake_request.HTTPRequest.blank( + '/subscriptions?filter') + req.method = 'GET' + resp = req.get_response(self.app) + + self.assertEqual(400, resp.status_code)