From 81dca79807b9669af04d3f1ff9f53d362b5a76b4 Mon Sep 17 00:00:00 2001 From: Koichi Edagawa Date: Mon, 24 Jan 2022 10:17:29 +0900 Subject: [PATCH] Support handling large query results by ETSI NFV This patch provides supporting handling large query results in a response of target APIs. The features to be added are like the following. - Paging query results according to ETSI NFV SOL013 - Fetching entire records forcibly Implements: blueprint paging-query-result Change-Id: I587fd9a998032cc1d23b72d755c60fb7a859c7ee --- ...-paging-query-result-9267729be1456b0d.yaml | 13 ++ tacker/api/vnflcm/v1/controller.py | 216 +++++++++++++----- tacker/conf/vnf_lcm.py | 14 +- tacker/objects/vnf_lcm_subscriptions.py | 8 +- tacker/tests/unit/vnflcm/fakes.py | 6 +- tacker/tests/unit/vnflcm/test_controller.py | 186 +++++++++++++-- 6 files changed, 358 insertions(+), 85 deletions(-) create mode 100644 releasenotes/notes/support-paging-query-result-9267729be1456b0d.yaml diff --git a/releasenotes/notes/support-paging-query-result-9267729be1456b0d.yaml b/releasenotes/notes/support-paging-query-result-9267729be1456b0d.yaml new file mode 100644 index 000000000..9ad8d8ae2 --- /dev/null +++ b/releasenotes/notes/support-paging-query-result-9267729be1456b0d.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Support handling large query results according to ETSI NFV SOL013. + This feature provides paged response regarding a query request of + target APIs. In addition to that, fetching entire records at once + becomes available. +issues: + - Regarding handling large query results according to ETSI NFV SOL013, + "vnfpkgm/v1/vnf_packages" API does not have the paging feature yet + because of longer time to be implemented than the other APIs. Since + there is less possibility to be paged in actual use case, the + implementation will be done in the next release. diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index 61d48489f..06391205d 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -192,6 +192,9 @@ class VnfLcmController(wsgi.Controller): 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() + self._nextpages_vnf_instances = {} + self._nextpages_lcm_op_occs = {} + self._nextpages_subscriptions = {} def _get_vnf_instance_href(self, vnf_instance): return '{}vnflcm/v1/vnf_instances/{}'.format( @@ -555,6 +558,13 @@ class VnfLcmController(wsgi.Controller): return vnf_package_info[0] + def _delete_expired_nextpages(self, nextpages): + for k, v in list(nextpages.items()): + if timeutils.is_older_than(v['created_time'], + CONF.vnf_lcm.nextpage_expiration_time): + LOG.debug('Old nextpages are deleted. id: %s' % k) + nextpages.pop(k) + @wsgi.response(http_client.OK) @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND)) def show(self, request, id): @@ -571,18 +581,54 @@ class VnfLcmController(wsgi.Controller): @wsgi.response(http_client.OK) @wsgi.expected_errors((http_client.FORBIDDEN, http_client.BAD_REQUEST)) - @api_common.validate_supported_params({'filter'}) + @api_common.validate_supported_params({'filter', 'nextpage_opaque_marker', + 'all_records'}) def index(self, request): - context = request.environ['tacker.context'] - context.can(vnf_lcm_policies.VNFLCM % 'index') + if 'tacker.context' in request.environ: + context = request.environ['tacker.context'] + 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) + nextpage = request.GET.get('nextpage_opaque_marker') + allrecords = request.GET.get('all_records') - return self._view_builder.index(vnf_instances) + result = [] + + if allrecords != 'yes' and nextpage: + self._delete_expired_nextpages(self._nextpages_vnf_instances) + + if nextpage in self._nextpages_vnf_instances: + result = self._nextpages_vnf_instances.pop( + nextpage)['nextpage'] + else: + vnf_instances = objects.VnfInstanceList.get_by_filters( + request.context, filters=filters) + + result = self._view_builder.index(vnf_instances) + + res = webob.Response(content_type='application/json') + res.status_int = 200 + + if (allrecords != 'yes' and + len(result) > CONF.vnf_lcm.vnf_instance_num): + nextpageid = uuidutils.generate_uuid() + links = ('Link', '<%s?nextpage_opaque_marker=%s>; rel="next"' % ( + request.path_url, nextpageid)) + res.headerlist.append(links) + res.body = jsonutils.dump_as_bytes( + result[: CONF.vnf_lcm.vnf_instance_num]) + + self._delete_expired_nextpages(self._nextpages_vnf_instances) + + remain = result[CONF.vnf_lcm.vnf_instance_num:] + self._nextpages_vnf_instances.update({nextpageid: + {'created_time': timeutils.utcnow(), 'nextpage': remain}}) + else: + res.body = jsonutils.dump_as_bytes(result) + + return res @check_vnf_state(action="delete", instantiation_state=[fields.VnfInstanceState.NOT_INSTANTIATED], @@ -1012,13 +1058,16 @@ class VnfLcmController(wsgi.Controller): @wsgi.response(http_client.OK) def subscription_list(self, request): - nextpage_opaque_marker = "" + nextpage_opaque_marker = None paging = 1 filter_string = "" + ignore_nextpages = False + subscription_data = [] - re_url = request.path_url query_params = request.query_string + allrecords = request.GET.get('all_records') + if query_params: query_params = parse.unquote(query_params) query_param_list = query_params.split('&') @@ -1034,6 +1083,7 @@ class VnfLcmController(wsgi.Controller): nextpage_opaque_marker = query_param_key_value[1] if query_param_key_value[0] == 'page': paging = int(query_param_key_value[1]) + ignore_nextpages = True if filter_string: # check enumerations columns @@ -1061,51 +1111,58 @@ class VnfLcmController(wsgi.Controller): return self._make_problem_detail(msg, 400, title='Bad Request') - try: - filter_string_parsed = self._view_builder_subscription. \ - validate_filter(filter_string) - if nextpage_opaque_marker: - start_index = paging - 1 - else: - start_index = None + nextpage = nextpage_opaque_marker + if allrecords != 'yes' and not ignore_nextpages and nextpage: + self._delete_expired_nextpages(self._nextpages_subscriptions) - vnf_lcm_subscriptions, last = ( - subscription_obj.LccnSubscriptionList. - get_by_filters(request.context, - read_deleted='no', - filters=filter_string_parsed, - nextpage_opaque_marker=start_index)) + if nextpage in self._nextpages_subscriptions: + subscription_data = self._nextpages_subscriptions.pop( + nextpage)['nextpage'] + else: + try: + filter_string_parsed = self._view_builder_subscription. \ + validate_filter(filter_string) + if nextpage_opaque_marker: + start_index = paging - 1 + else: + start_index = None - LOG.debug("vnf_lcm_subscriptions %s" % vnf_lcm_subscriptions) - 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()) - return self._make_problem_detail( - str(e), 500, title='Internal Server Error') + vnf_lcm_subscriptions = ( + subscription_obj.LccnSubscriptionList. + get_by_filters(request.context, + read_deleted='no', + filters=filter_string_parsed, + nextpage_opaque_marker=start_index)) - if subscription_data == 400: - msg = _("Number of records exceeds nextpage_opaque_marker") - return self._make_problem_detail(msg, 400, title='Bad Request') + LOG.debug("vnf_lcm_subscriptions %s" % vnf_lcm_subscriptions) + subscription_data = self._view_builder_subscription. \ + subscription_list(vnf_lcm_subscriptions) + except Exception as e: + LOG.error(traceback.format_exc()) + return self._make_problem_detail( + str(e), 500, title='Internal Server Error') # make response res = webob.Response(content_type='application/json') - res.body = jsonutils.dump_as_bytes(subscription_data) res.status_int = 200 - if nextpage_opaque_marker: - if not last: - ln = '<%s?page=%s>;rel="next"; title*="next chapter"' % ( - re_url, paging + 1) - # Regarding the setting in http header related to - # nextpage control, RFC8288 and NFV-SOL013 - # specifications have not been confirmed. - # Therefore, it is implemented by setting "page", - # which is a general control method of WebAPI, - # as "URI-Reference" of Link header. - links = ('Link', ln) - res.headerlist.append(links) + if (allrecords != 'yes' and not ignore_nextpages and + len(subscription_data) > CONF.vnf_lcm.subscription_num): + nextpageid = uuidutils.generate_uuid() + links = ('Link', '<%s?nextpage_opaque_marker=%s>; rel="next"' % ( + request.path_url, nextpageid)) + res.headerlist.append(links) + + remain = subscription_data[CONF.vnf_lcm.subscription_num:] + subscription_data = ( + subscription_data[: CONF.vnf_lcm.subscription_num]) + + self._delete_expired_nextpages(self._nextpages_subscriptions) + self._nextpages_subscriptions.update({nextpageid: + {'created_time': timeutils.utcnow(), 'nextpage': remain}}) + + res.body = jsonutils.dump_as_bytes(subscription_data) + LOG.debug("subscription_list res %s" % res) return res @@ -1674,8 +1731,9 @@ class VnfLcmController(wsgi.Controller): @wsgi.response(http_client.OK) @wsgi.expected_errors((http_client.FORBIDDEN, http_client.BAD_REQUEST)) def list_lcm_op_occs(self, request): - context = request.environ['tacker.context'] - context.can(vnf_lcm_policies.VNFLCM % 'list_lcm_op_occs') + if 'tacker.context' in request.environ: + context = request.environ['tacker.context'] + context.can(vnf_lcm_policies.VNFLCM % 'list_lcm_op_occs') all_fields = request.GET.get('all_fields') exclude_default = request.GET.get('exclude_default') @@ -1685,25 +1743,57 @@ class VnfLcmController(wsgi.Controller): if not (all_fields or fields or exclude_fields): exclude_default = True - self._view_builder_op_occ.validate_attribute_fields( - all_fields=all_fields, fields=fields, - exclude_fields=exclude_fields, - exclude_default=exclude_default) + nextpage = request.GET.get('nextpage_opaque_marker') + allrecords = request.GET.get('all_records') - filters = self._view_builder_op_occ.validate_filter(filters) + result = [] - try: - vnf_lcm_op_occs = \ - vnf_lcm_op_occs_obj.VnfLcmOpOccList.get_by_filters( - request.context, read_deleted='no', filters=filters) - except Exception as e: - LOG.exception(traceback.format_exc()) - return self._make_problem_detail( - str(e), 500, title='Internal Server Error') + if allrecords != 'yes' and nextpage: + self._delete_expired_nextpages(self._nextpages_lcm_op_occs) - return self._view_builder_op_occ.index(request, vnf_lcm_op_occs, - all_fields=all_fields, exclude_fields=exclude_fields, - fields=fields, exclude_default=exclude_default) + if nextpage in self._nextpages_lcm_op_occs: + result = self._nextpages_lcm_op_occs.pop(nextpage)['nextpage'] + else: + self._view_builder_op_occ.validate_attribute_fields( + all_fields=all_fields, fields=fields, + exclude_fields=exclude_fields, + exclude_default=exclude_default) + + filters = self._view_builder_op_occ.validate_filter(filters) + + try: + vnf_lcm_op_occs = ( + vnf_lcm_op_occs_obj.VnfLcmOpOccList.get_by_filters( + request.context, read_deleted='no', filters=filters)) + except Exception as e: + LOG.exception(traceback.format_exc()) + return self._make_problem_detail( + str(e), 500, title='Internal Server Error') + + result = self._view_builder_op_occ.index(request, vnf_lcm_op_occs, + all_fields=all_fields, exclude_fields=exclude_fields, + fields=fields, exclude_default=exclude_default) + + res = webob.Response(content_type='application/json') + res.status_int = 200 + + if allrecords != 'yes' and len(result) > CONF.vnf_lcm.lcm_op_occ_num: + nextpageid = uuidutils.generate_uuid() + links = ('Link', '<%s?nextpage_opaque_marker=%s>; rel="next"' % ( + request.path_url, nextpageid)) + res.headerlist.append(links) + res.body = jsonutils.dump_as_bytes( + result[: CONF.vnf_lcm.lcm_op_occ_num]) + + self._delete_expired_nextpages(self._nextpages_lcm_op_occs) + + remain = result[CONF.vnf_lcm.lcm_op_occ_num:] + self._nextpages_lcm_op_occs.update({nextpageid: + {'created_time': timeutils.utcnow(), 'nextpage': remain}}) + else: + res.body = jsonutils.dump_as_bytes(result) + + return res def _make_problem_detail( self, diff --git a/tacker/conf/vnf_lcm.py b/tacker/conf/vnf_lcm.py index 511d711c8..6d66a8ca2 100644 --- a/tacker/conf/vnf_lcm.py +++ b/tacker/conf/vnf_lcm.py @@ -47,7 +47,19 @@ OPTS = [ cfg.BoolOpt( 'verify_notification_ssl', default=True, - help="Verify the certificate to send notification by ssl")] + help="Verify the certificate to send notification by ssl"), + cfg.IntOpt( + 'lcm_op_occ_num', + default=100, + help="Number of lcm_op_occs contained in 1 page"), + cfg.IntOpt( + 'vnf_instance_num', + default=100, + help="Number of vnf_instances contained in 1 page"), + cfg.IntOpt( + 'nextpage_expiration_time', + default=3600, + help="Expiration time (sec) for paging")] vnf_lcm_group = cfg.OptGroup('vnf_lcm', title='vnf_lcm options', diff --git a/tacker/objects/vnf_lcm_subscriptions.py b/tacker/objects/vnf_lcm_subscriptions.py index c82805803..aa1151857 100644 --- a/tacker/objects/vnf_lcm_subscriptions.py +++ b/tacker/objects/vnf_lcm_subscriptions.py @@ -457,20 +457,14 @@ def _make_subscription_list(context, subscription_list, db_subscription_list, 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 + return subscription_list @base.TackerObjectRegistry.register diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index 542931605..caa8ec099 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -1682,8 +1682,12 @@ def fake_vnf_lcm_op_occs(): return vnf_lcm_op_occs -def return_vnf_lcm_opoccs_obj(): +def return_vnf_lcm_opoccs_obj(**updates): vnf_lcm_op_occs = fake_vnf_lcm_op_occs() + + if updates: + vnf_lcm_op_occs.update(**updates) + obj = objects.VnfLcmOpOcc(**vnf_lcm_op_occs) return obj diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 7547b2f74..4832a15f6 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -19,6 +19,7 @@ import ddt from http import client as http_client import json import os +import re from unittest import mock import urllib import webob @@ -1697,14 +1698,14 @@ class TestController(base.TestCase): expected_result = [fakes.fake_vnf_instance_response(), fakes.fake_vnf_instance_response( fields.VnfInstanceState.INSTANTIATED)] - self.assertEqual(expected_result, resp) + self.assertEqual(expected_result, resp.json) @mock.patch.object(objects.VnfInstanceList, "get_by_filters") def test_index_empty_response(self, mock_vnf_list): req = fake_request.HTTPRequest.blank('/vnf_instances') mock_vnf_list.return_value = [] resp = self.controller.index(req) - self.assertEqual([], resp) + self.assertEqual([], resp.json) @mock.patch.object(TackerManager, 'get_service_plugins', return_value={'VNFM': @@ -1859,7 +1860,7 @@ class TestController(base.TestCase): expected_result = [fakes.fake_vnf_instance_response(), fakes.fake_vnf_instance_response( fields.VnfInstanceState.INSTANTIATED)] - self.assertEqual(expected_result, res_dict) + self.assertEqual(expected_result, res_dict.json) @mock.patch.object(objects.VnfInstanceList, "get_by_filters") def test_index_filter_combination(self, mock_vnf_list): @@ -1882,7 +1883,7 @@ class TestController(base.TestCase): expected_result = [fakes.fake_vnf_instance_response(), fakes.fake_vnf_instance_response( fields.VnfInstanceState.INSTANTIATED)] - self.assertEqual(expected_result, res_dict) + self.assertEqual(expected_result, res_dict.json) @mock.patch.object(objects.VnfInstanceList, "get_by_filters") @ddt.data( @@ -1935,7 +1936,7 @@ class TestController(base.TestCase): expected_result = [fakes.fake_vnf_instance_response(), fakes.fake_vnf_instance_response( fields.VnfInstanceState.INSTANTIATED)] - self.assertEqual(expected_result, res_dict) + self.assertEqual(expected_result, res_dict.json) @mock.patch.object(objects.VnfInstanceList, "get_by_filters") @ddt.data( @@ -2017,6 +2018,46 @@ class TestController(base.TestCase): self.assertRaises(exceptions.ValidationError, self.controller.index, req) + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") + @ddt.data( + {'params': {'all_records': 'yes'}, + 'result_names': ['sample1', 'sample2', 'sample3', 'sample4']}, + {'params': {'all_records': 'yes', 'nextpage_opaque_marker': 'abc'}, + 'result_names': ['sample1', 'sample2', 'sample3', 'sample4']}, + {'params': {'nextpage_opaque_marker': 'abc'}, + 'result_names': []}, + {'params': {}, + 'result_names': ['sample2']} + ) + def test_index_paging(self, values, mock_vnf_list): + cfg.CONF.set_override('vnf_instance_num', 1, group='vnf_lcm') + query = urllib.parse.urlencode(values['params']) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_instances?' + query) + + mock_vnf_list.return_value = [ + fakes.return_vnf_instance(**{'vnf_instance_name': 'sample1'}), + fakes.return_vnf_instance(**{'vnf_instance_name': 'sample2'}), + fakes.return_vnf_instance(**{'vnf_instance_name': 'sample3'}), + fakes.return_vnf_instance(**{'vnf_instance_name': 'sample4'}) + ] + + expected_result = [] + for name in values['result_names']: + expected_result.append(fakes.fake_vnf_instance_response( + **{'vnfInstanceName': name})) + + res_dict = self.controller.index(req) + + if 'Link' in res_dict.headers: + next_url = re.findall('<(.*)>', res_dict.headers['Link'])[0] + query = urllib.parse.urlparse(next_url).query + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_instances?' + query) + res_dict = self.controller.index(req) + + self.assertEqual(expected_result, res_dict.json) + @mock.patch.object(objects.VnfInstanceList, "get_by_filters") @ddt.data( {'attribute_not_exist': 'some_value'}, @@ -2024,7 +2065,6 @@ class TestController(base.TestCase): {'fields': {}}, {'exclude_fields': {}}, {'exclude_default': {}}, - {'nextpage_opaque_marker': 1}, {'attribute_not_exist': 'some_value', 'filter': {}}, {'attribute_not_exist': 'some_value', 'fields': {}} ) @@ -2044,7 +2084,6 @@ class TestController(base.TestCase): {'fields': {}}, {'exclude_fields': {}}, {'exclude_default': {}}, - {'nextpage_opaque_marker': 1}, {'attribute_not_exist': 'some_value', 'filter': {}}, {'attribute_not_exist': 'some_value', 'fields': {}} ) @@ -3760,9 +3799,61 @@ class TestController(base.TestCase): expected_result = fakes.index_response( remove_attrs=complex_attributes) mock_op_occ_list.return_value = vnf_lcm_op_occ - res_dict = self.controller.list_lcm_op_occs(req) + resp = self.controller.list_lcm_op_occs(req) - self.assertEqual(expected_result, res_dict) + self.assertEqual( + jsonutils.loads(jsonutils.dump_as_bytes(expected_result)), + resp.json) + + @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") + @ddt.data( + {'params': {'all_records': 'yes'}, + 'result_names': ['INSTANTIATE', 'SCALE', 'HEAL', 'TERMINATE']}, + {'params': {'all_records': 'yes', 'nextpage_opaque_marker': 'abc'}, + 'result_names': ['INSTANTIATE', 'SCALE', 'HEAL', 'TERMINATE']}, + {'params': {'nextpage_opaque_marker': 'abc'}, + 'result_names': []}, + {'params': {}, + 'result_names': ['SCALE']} + ) + def test_op_occ_list_paging(self, values, mock_op_occ_list): + cfg.CONF.set_override('lcm_op_occ_num', 1, group='vnf_lcm') + query = urllib.parse.urlencode(values['params']) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_lcm_op_occs?' + query) + + complex_attributes = [ + 'error', + 'resourceChanges', + 'operationParams', + 'changedInfo'] + + vnf_lcm_op_occ = [ + fakes.return_vnf_lcm_opoccs_obj(**{'operation': 'INSTANTIATE'}), + fakes.return_vnf_lcm_opoccs_obj(**{'operation': 'SCALE'}), + fakes.return_vnf_lcm_opoccs_obj(**{'operation': 'HEAL'}), + fakes.return_vnf_lcm_opoccs_obj(**{'operation': 'TERMINATE'}) + ] + + expected_result = [] + for name in values['result_names']: + expected_result += fakes.index_response( + remove_attrs=complex_attributes, + vnf_lcm_op_occs_updates={'operation': name}) + + mock_op_occ_list.return_value = vnf_lcm_op_occ + resp = self.controller.list_lcm_op_occs(req) + + if 'Link' in resp.headers: + next_url = re.findall('<(.*)>', resp.headers['Link'])[0] + query = urllib.parse.urlparse(next_url).query + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_lcm_op_occs?' + query) + resp = self.controller.list_lcm_op_occs(req) + + self.assertEqual( + jsonutils.loads(jsonutils.dump_as_bytes(expected_result)), + resp.json) @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") @ddt.data( @@ -3797,7 +3888,9 @@ class TestController(base.TestCase): mock_op_occ_list.return_value = vnf_lcm_op_occ res_dict = self.controller.list_lcm_op_occs(req) - self.assertEqual(expected_result, res_dict) + self.assertEqual( + jsonutils.loads(jsonutils.dump_as_bytes(expected_result)), + res_dict.json) @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") def test_op_occ_filter_attributes_invalid_filter(self, mock_op_occ_list): @@ -3822,7 +3915,9 @@ class TestController(base.TestCase): mock_op_occ_list.return_value = vnf_lcm_op_occ res_dict = self.controller.list_lcm_op_occs(req) - self.assertEqual(expected_result, res_dict) + self.assertEqual( + jsonutils.loads(jsonutils.dump_as_bytes(expected_result)), + res_dict.json) @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") @ddt.data( @@ -3850,7 +3945,9 @@ class TestController(base.TestCase): expected_result = fakes.index_response(remove_attrs=remove_attributes) mock_op_occ_list.return_value = vnf_lcm_op_occ res_dict = self.controller.list_lcm_op_occs(req) - self.assertEqual(expected_result, res_dict) + self.assertEqual( + jsonutils.loads(jsonutils.dump_as_bytes(expected_result)), + res_dict.json) @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") @ddt.data( @@ -3871,7 +3968,9 @@ class TestController(base.TestCase): expected_result = fakes.index_response(remove_attrs=remove_attributes) mock_op_occ_list.return_value = vnf_lcm_op_occ res_dict = self.controller.list_lcm_op_occs(req) - self.assertEqual(expected_result, res_dict) + self.assertEqual( + jsonutils.loads(jsonutils.dump_as_bytes(expected_result)), + res_dict.json) @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") def test_op_occ_attribute_selector_fields_error(self, mock_op_occ_list): @@ -4326,6 +4425,67 @@ class TestController(base.TestCase): 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( + {'params': {'all_records': 'yes'}, + 'result_names': ['subscription_id_1', 'subscription_id_2', + 'subscription_id_3', 'subscription_id_4']}, + {'params': {'all_records': 'yes', 'nextpage_opaque_marker': 'abc'}, + 'result_names': ['subscription_id_1', 'subscription_id_2', + 'subscription_id_3', 'subscription_id_4']}, + {'params': {'nextpage_opaque_marker': 'abc'}, + 'result_names': []}, + {'params': {}, + 'result_names': ['subscription_id_2']} + ) + def test_subscription_list_paging(self, + values, + mock_subscription_list, + mock_subscription_filter, + mock_subscription_view, + mock_get_service_plugins): + mock_subscription_filter.return_value = None + last = True + cfg.CONF.set_override('subscription_num', 1, group='vnf_lcm') + query = urllib.parse.urlencode(values['params']) + req = fake_request.HTTPRequest.blank('/subscriptions?' + query) + req.method = 'GET' + subscription_list = [ + fakes.return_subscription_object( + **{'id': uuidsentinel.subscription_id_1}), + fakes.return_subscription_object( + **{'id': uuidsentinel.subscription_id_2}), + fakes.return_subscription_object( + **{'id': uuidsentinel.subscription_id_3}), + fakes.return_subscription_object( + **{'id': uuidsentinel.subscription_id_4}) + ] + mock_subscription_list.return_value = [subscription_list, last] + mock_subscription_view.return_value = subscription_list + resp = self.controller.subscription_list(req) + + if 'Link' in resp.headers: + next_url = re.findall('<(.*)>', resp.headers['Link'])[0] + query = urllib.parse.urlparse(next_url).query + req = fake_request.HTTPRequest.blank('/subscriptions?' + query) + resp = self.controller.subscription_list(req) + + expected_result = [] + for name in values['result_names']: + expected_result.append(fakes.return_subscription_object( + **{'id': eval('uuidsentinel.' + name)})) + + 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()})