From 87ea7257848114e504473fa0938d8969693e260c Mon Sep 17 00:00:00 2001 From: Aldinson Esto Date: Tue, 2 Mar 2021 01:41:43 +0900 Subject: [PATCH] Enhancement of get VNF LCM operation occurrence - Added support for getting Individual VNF LCM operation occurrence by its ID - The query information is enhanced by improving filtering expressions and operators - Attributes are also added including (but not limited to): * grant_id * _links * >retry * >fail * changedExtConnectivity - This patch has 2 BP features (Get VNFM LCM Operation Occurence and attributes of Change External Connectivity are also added). Since both features are related, they are merged to one patch. This is why this patch have 2 BPs. - Filtering for the following attributes: operationParams, error, resourceChanges and changedInfo is only limited to the parent attribute. Currently, child attributes/nested attributes are not searchable. Implements: blueprint support-fundamental-lcm Implements: blueprint support-change-external-connectivity Spec: https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-fundamental-vnf-lcm-based-on-ETSI-NFV.html Spec: https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-change-external-VNF-connectivity-operation.html Change-Id: Ie9b07c203807d08857be65298d9128b026a8fd37 --- ...-vnflcm-operation-occurrence-response.json | 82 ++++++++ ...-vnflcm-operation-occurrence-response.json | 10 + api-ref/source/v1/vnflcm.inc | 112 ++++++++++- tacker/api/common/attribute_filter.py | 12 ++ tacker/api/views/vnf_lcm.py | 20 +- tacker/api/views/vnf_lcm_op_occs.py | 118 ++++++++++++ tacker/api/vnflcm/v1/controller.py | 37 ++++ tacker/api/vnflcm/v1/router.py | 12 ++ tacker/db/db_sqlalchemy/models.py | 2 + ...ac34764da_add_column_to_vnf_lcm_op_occs.py | 38 ++++ .../alembic_migrations/versions/HEAD | 2 +- tacker/objects/fields.py | 3 +- tacker/objects/vnf_lcm_op_occs.py | 181 ++++++++++++++++-- tacker/policies/vnf_lcm.py | 11 ++ tacker/tests/functional/sol/vnflcm/base.py | 8 + .../test_vnf_instance_with_user_data.py | 53 ++++- tacker/tests/unit/objects/fakes.py | 20 ++ .../unit/objects/test_vnf_lcm_op_occs.py | 3 + tacker/tests/unit/vnflcm/fakes.py | 64 ++++++- tacker/tests/unit/vnflcm/test_controller.py | 138 +++++++++++++ 20 files changed, 901 insertions(+), 25 deletions(-) create mode 100644 api-ref/source/v1/samples/vnflcm/list-vnflcm-operation-occurrence-response.json create mode 100644 tacker/api/views/vnf_lcm_op_occs.py create mode 100644 tacker/db/migration/alembic_migrations/versions/3adac34764da_add_column_to_vnf_lcm_op_occs.py diff --git a/api-ref/source/v1/samples/vnflcm/list-vnflcm-operation-occurrence-response.json b/api-ref/source/v1/samples/vnflcm/list-vnflcm-operation-occurrence-response.json new file mode 100644 index 000000000..c3b1cd113 --- /dev/null +++ b/api-ref/source/v1/samples/vnflcm/list-vnflcm-operation-occurrence-response.json @@ -0,0 +1,82 @@ +[ + { + "id": "d85c6ae4-af16-42c0-96fc-82f7c014c468", + "operationState": "COMPLETED", + "stateEnteredTime": "2020-08-02T06:50:50.883373", + "startTime": "2020-08-02T06:41:34.883483", + "vnfInstanceId": "0b7b95a9-21d5-4ac4-80c8-9ae9f7323787", + "grantId": "3432cebe-db0a-11e8-9023-005056317abe", + "operation": "INSTANTIATE", + "isAutomaticInvocation": false, + "operationParams": "{ + 'flavourId': 'default', + 'instantiationLevelId': 'vnf-min', + }", + "isCancelPending": false, + "resourceChanges": { + "affectedVnfcs": [ + { + "id": "36e24439-829c-4803-a413-385cd658d544", + "vduId": "VDU", + "changeType": "ADDED", + "computeResource": { + "vimConnectionId": "f26f181d-7891-4720-b022-b074ec1733ef", + "resourceId": "e0510ba9-3a53-4fcf-9dcc-58dea5c048b0", + "vimLevelResourceType": "OS::Nova::Server", + }, + "affectedVnfcCpIds": [ + "VDU1_CP0", + "VDU1_CP1" + ], + "addedStorageResourceIds": [ + "81ae44f6-b65b-47aa-a578-e53b7a50a574" + ] + } + ], + "affectedVirtualLinks": [ + { + "id": "9836f7f2-5af4-4df5-a89f-933479448ef7", + "vnfVirtualLinkDescId": "internalNW", + "changeType": "ADDED", + "networkResource": { + "vimConnectionId": "f26f181d-7891-4720-b022-b074ec1733ef", + "resourceId": "400692e5-b2db-478e-acb1-b77a92635ec6", + "vimLevelResourceType": "OS::Neutron::Net" + } + } + ], + "affectedVirtualStorages": [ + { + "id": "81ae44f6-b65b-47aa-a578-e53b7a50a574", + "virtualStorageDescId": "Storage", + "changeType": "ADDED", + "storageResource": { + "vimConnectionId": "f26f181d-7891-4720-b022-b074ec1733ef", + "resourceId": "842f527e-0092-4f11-aede-f981ba4fd884", + "vimLevelResourceType": "OS::Cinder::Volume" + } + } + ] + }, + "_links": { + "self": { + "href": "http://sample.com/vnflcm/v1/vnf_lcm_op_occs/d85c6ae4-af16-42c0-96fc-82f7c014c468" + }, + "vnfInstance": { + "href": "http://sample.com/vnflcm/v1/vnf_instances/0b7b95a9-21d5-4ac4-80c8-9ae9f7323787" + }, + "grant":{ + "href":"http://sample.com/grant/v1/grants/3432cebe-db0a-11e8-9023-005056317abe" + }, + "retry":{ + "href":"http://sample1.com/vnflcm/v1/vnf_lcm_op_occs/d85c6ae4-af16-42c0-96fc-82f7c014c468/retry" + }, + "rollback":{ + "href":"http://sample1.com/vnflcm/v1/vnf_lcm_op_occs/d85c6ae4-af16-42c0-96fc-82f7c014c468/rollback" + }, + "fail":{ + "href":"http://sample1.com/vnflcm/v1/vnf_lcm_op_occs/d85c6ae4-af16-42c0-96fc-82f7c014c468/fail" + } + } + } +] diff --git a/api-ref/source/v1/samples/vnflcm/show-vnflcm-operation-occurrence-response.json b/api-ref/source/v1/samples/vnflcm/show-vnflcm-operation-occurrence-response.json index 7a05da7d5..1bc123d1f 100644 --- a/api-ref/source/v1/samples/vnflcm/show-vnflcm-operation-occurrence-response.json +++ b/api-ref/source/v1/samples/vnflcm/show-vnflcm-operation-occurrence-response.json @@ -4,6 +4,7 @@ "stateEnteredTime": "2020-08-02T06:50:50.883373", "startTime": "2020-08-02T06:41:34.883483", "vnfInstanceId": "0b7b95a9-21d5-4ac4-80c8-9ae9f7323787", + "grantId": "3432cebe-db0a-11e8-9023-005056317abe", "operation": "INSTANTIATE", "isAutomaticInvocation": false, "operationParams": "{ @@ -65,6 +66,15 @@ }, "grant": { "href": "/grant/v1/grants/3432cebe-db0a-11e8-9023-005056317abe" + }, + "retry":{ + "href":"http://sample1.com/vnflcm/v1/vnf_lcm_op_occs/d85c6ae4-af16-42c0-96fc-82f7c014c468/retry" + }, + "rollback":{ + "href":"http://sample1.com/vnflcm/v1/vnf_lcm_op_occs/d85c6ae4-af16-42c0-96fc-82f7c014c468/rollback" + }, + "fail":{ + "href":"http://sample1.com/vnflcm/v1/vnf_lcm_op_occs/d85c6ae4-af16-42c0-96fc-82f7c014c468/fail" } } } diff --git a/api-ref/source/v1/vnflcm.inc b/api-ref/source/v1/vnflcm.inc index 1e74aa499..0d1ab7b67 100644 --- a/api-ref/source/v1/vnflcm.inc +++ b/api-ref/source/v1/vnflcm.inc @@ -712,6 +712,116 @@ Response Parameters - stateEnteredTime: state_entered_time - startTime: start_time - vnfInstanceId: vnf_lcm_vnf_instance_id + - grantId: grant_id + - operation: operation + - isAutomaticInvocation: is_automatic_invocation + - operationParams: operation_params + - isCancelPending: is_cancel_pending + - error: error + - title: error_title + - status: error_status + - detail: error_detail + - resourceChanges: resource_changes + - affectedVnfcs: affected_vnfcs + - id: affected_vnfcs_id + - vduId: affected_vnfcs_vdu_id + - changeType: affected_vnfcs_change_type + - computeResource: vnfc_resource_info_compute_resource + - vimConnectionId: vim_connection_id + - resourceId: resource_handle_resource_id + - vimLevelResourceType: resource_handle_vim_level_resource_type + - affectedVnfcCpIds: affected_vnfc_cp_ids + - addedStorageResourceIds: added_storage_resource_ids + - removedStorageResourceIds: removed_storage_resource_ids + - affectedVirtualLinks: affected_virtual_links + - id: affected_virtual_links_id + - vnfVirtualLinkDescId: vnf_virtual_link_resource_info_vnf_virtual_link_desc_id + - changeType: affected_virtual_links_change_type + - networkResource: vnf_virtual_link_resource_info_network_resource + - vimConnectionId: vim_connection_id + - resourceId: resource_handle_resource_id + - vimLevelResourceType: resource_handle_vim_level_resource_type + - affectedVirtualStorages: affected_virtual_storages + - id: affected_virtual_storages_id + - virtualStorageDescId: affected_virtual_storages_virtual_storage_desc_id + - changeType: affected_virtual_storages_change_type + - storageResource: virtual_storage_resource_info_storage_resource + - vimConnectionId: vim_connection_id + - resourceId: resource_handle_resource_id + - vimLevelResourceType: resource_handle_vim_level_resource_type + - changedInfo: changed_info + - vnfInstanceName: changed_info_vnf_instance_name + - vnfInstanceDescription: changed_info_vnf_instance_description + - metadata: changed_info_metadata + - vimConnectionInfo: changed_info_vim_connection_info + - id: vim_connection_info_id + - vimId: vim_connection_info_vim_id + - vimType: vim_connection_info_vim_type + - interfaceInfo: vim_connection_info_interface_info + - endpoint: vim_connection_info_interface_info_endpoint + - accessInfo: vim_connection_info_access_info + - username: vim_connection_info_access_info_username + - region: vim_connection_info_access_info_region + - password: vim_connection_info_access_info_password + - tenant: vim_connection_info_access_info_tenant + - vnfPkgId: changed_info_vnf_pkg_id + - vnfdId: changed_info_vnfd_id + - vnfProvider: changed_info_vnf_provider + - vnfProductName: changed_info_vnf_product_name + - vnfSotwareVersion: changed_info_vnf_sotware_version + - vnfdVersion: changed_info_vnfd_version + - changedExtConnectivity: changed_ext_connectivity + - id: changed_ext_connectivity_id + - resourceHandle: resource_handle + - vimConnectionId: vim_connection_id + - resourceId: resource_handle_resource_id + - vimLevelResourceType: resource_handle_vim_level_resource_type + - extLinkPorts: ext_link_ports + - id: ext_link_port_id + - resourceHandle: resource_handle + - vimConnectionId: vim_connection_id + - resourceId: resource_handle_resource_id + - vimLevelResourceType: resource_handle_vim_level_resource_type + - cpInstanceId: cp_instance_id + - _links: vnf_instance_links + +Response Example +---------------- + +.. literalinclude:: samples/vnflcm/show-vnflcm-operation-occurrence-response.json + :language: javascript + +List VNF LCM operation occurrence +================================= + +.. rest_method:: GET /vnflcm/v1/vnf_lcm_op_occs + +The API consumer can use this method to query status information about multiple VNF lifecycle management operation +occurrences. + +Response Codes +-------------- + +.. rest_status_code:: success status.yaml + + - 200 + +.. rest_status_code:: error status.yaml + + - 400 + - 403 + +Response Parameters +------------------- + +.. rest_parameters:: parameters_vnflcm.yaml + + - id: vnf_lcm_op_occ_id_response + - operationState: operation_state + - stateEnteredTime: state_entered_time + - startTime: start_time + - vnfInstanceId: vnf_lcm_vnf_instance_id + - grantId: grant_id - operation: operation - isAutomaticInvocation: is_automatic_invocation - operationParams: operation_params @@ -774,7 +884,7 @@ Response Parameters Response Example ---------------- -.. literalinclude:: samples/vnflcm/show-vnflcm-operation-occurrence-response.json +.. literalinclude:: samples/vnflcm/list-vnflcm-operation-occurrence-response.json :language: javascript Roll back a VNF lifecycle operation diff --git a/tacker/api/common/attribute_filter.py b/tacker/api/common/attribute_filter.py index ab43791d0..f4381bc7d 100644 --- a/tacker/api/common/attribute_filter.py +++ b/tacker/api/common/attribute_filter.py @@ -163,6 +163,18 @@ def _parse_filter(filter_rule): try: tokens = filter_rule.split(',') filter_type = None + + # TODO(esto-aln): This condition and the lines below will be removed + # if JSON will be supported via OR Mapping. Currently this condition + # allows support of JSON strings '"{...}"' for filtering + if (tokens[2].startswith("'\"{") and + tokens[len(tokens) - 1].endswith("}\"'")): + tokens[2] = ','.join(tokens[2:]) + + # retain first 3 indices and remove the rest + # to process as string + tokens = tokens[0:3] + if len(tokens) >= 3: if tokens[0] in _filters.SUPPORTED_OP_ONE: filter_type = 'simple_filter_expr_one' diff --git a/tacker/api/views/vnf_lcm.py b/tacker/api/views/vnf_lcm.py index 1f2eab940..fab34b39d 100644 --- a/tacker/api/views/vnf_lcm.py +++ b/tacker/api/views/vnf_lcm.py @@ -89,6 +89,8 @@ class ViewBuilder(base.BaseViewBuilder): return vim_connections + # TODO(esto-aln): This method will be transferred to + # tacker/api/views/vnf_lcm_op_occs.py in the future def _get_lcm_op_occs_links(self, vnf_lcm_op_occs): _links = { "self": { @@ -101,6 +103,12 @@ class ViewBuilder(base.BaseViewBuilder): % {"endpoint": CONF.vnf_lcm.endpoint_url, "id": vnf_lcm_op_occs.vnf_instance_id} }, + "retry": { + "href": + '%(endpoint)s/vnflcm/v1/vnf_lcm_op_occs/%(id)s/retry' + % {"endpoint": CONF.vnf_lcm.endpoint_url, + "id": vnf_lcm_op_occs.id} + }, "rollback": { "href": '%(endpoint)s/vnflcm/v1/vnf_lcm_op_occs/%(id)s/rollback' @@ -111,6 +119,12 @@ class ViewBuilder(base.BaseViewBuilder): "href": '%(endpoint)s/vnflcm/v1/vnf_lcm_op_occs/%(id)s/grant' % {"endpoint": CONF.vnf_lcm.endpoint_url, "id": vnf_lcm_op_occs.id} + }, + "fail": { + "href": + '%(endpoint)s/vnflcm/v1/vnf_lcm_op_occs/%(id)s/fail' + % {"endpoint": CONF.vnf_lcm.endpoint_url, + "id": vnf_lcm_op_occs.id} } } @@ -129,11 +143,13 @@ class ViewBuilder(base.BaseViewBuilder): vnf_instance_dict.update(links) return vnf_instance_dict + # TODO(esto-aln): This method will be transferred to + # tacker/api/views/vnf_lcm_op_occs.py in the future def _get_vnf_lcm_op_occs(self, vnf_lcm_op_occs): vnf_lcm_op_occs_dict = vnf_lcm_op_occs.to_dict() - vnf_lcm_op_occs_dict.pop('error_point') vnf_lcm_op_occs_dict = utils.convert_snakecase_to_camelcase( vnf_lcm_op_occs_dict) + vnf_lcm_op_occs_dict.pop('errorPoint') links = self._get_lcm_op_occs_links(vnf_lcm_op_occs) @@ -257,5 +273,7 @@ class ViewBuilder(base.BaseViewBuilder): def subscription_show(self, vnf_lcm_subscriptions): return self._get_vnf_lcm_subscription(vnf_lcm_subscriptions) + # TODO(esto-aln): This method will be transferred to + # tacker/api/views/vnf_lcm_op_occs.py in the future def show_lcm_op_occs(self, vnf_lcm_op_occs): return self._get_vnf_lcm_op_occs(vnf_lcm_op_occs) diff --git a/tacker/api/views/vnf_lcm_op_occs.py b/tacker/api/views/vnf_lcm_op_occs.py new file mode 100644 index 000000000..d5d0d6373 --- /dev/null +++ b/tacker/api/views/vnf_lcm_op_occs.py @@ -0,0 +1,118 @@ +# 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. + +from oslo_log import log as logging + +from tacker.api import views as base +from tacker.common import utils +import tacker.conf +from tacker.objects import vnf_lcm_op_occs as _vnf_lcm_op_occs + +CONF = tacker.conf.CONF + +LOG = logging.getLogger(__name__) + + +class ViewBuilder(base.BaseViewBuilder): + + FLATTEN_ATTRIBUTES = _vnf_lcm_op_occs.VnfLcmOpOcc.FLATTEN_ATTRIBUTES + COMPLEX_ATTRIBUTES = _vnf_lcm_op_occs.VnfLcmOpOcc.COMPLEX_ATTRIBUTES + FLATTEN_COMPLEX_ATTRIBUTES = [key for key in FLATTEN_ATTRIBUTES.keys() + if '/' in key] + + def _get_lcm_op_occs_links(self, vnf_lcm_op_occs): + _links = { + "self": { + "href": '%(endpoint)s/vnflcm/v1/vnf_lcm_op_occs/%(id)s' + % {"endpoint": CONF.vnf_lcm.endpoint_url, + "id": vnf_lcm_op_occs.id} + }, + "vnfInstance": { + "href": '%(endpoint)s/vnflcm/v1/vnf_instances/%(id)s' + % {"endpoint": CONF.vnf_lcm.endpoint_url, + "id": vnf_lcm_op_occs.vnf_instance_id} + }, + "retry": { + "href": + '%(endpoint)s/vnflcm/v1/vnf_lcm_op_occs/%(id)s/retry' + % {"endpoint": CONF.vnf_lcm.endpoint_url, + "id": vnf_lcm_op_occs.id} + }, + "rollback": { + "href": + '%(endpoint)s/vnflcm/v1/vnf_lcm_op_occs/%(id)s/rollback' + % {"endpoint": CONF.vnf_lcm.endpoint_url, + "id": vnf_lcm_op_occs.id} + }, + "grant": { + "href": '%(endpoint)s/vnflcm/v1/vnf_lcm_op_occs/%(id)s/grant' + % {"endpoint": CONF.vnf_lcm.endpoint_url, + "id": vnf_lcm_op_occs.id} + }, + "fail": { + "href": + '%(endpoint)s/vnflcm/v1/vnf_lcm_op_occs/%(id)s/fail' + % {"endpoint": CONF.vnf_lcm.endpoint_url, + "id": vnf_lcm_op_occs.id} + } + } + + return {"_links": _links} + + def _get_vnf_lcm_op_occs_list(self, vnf_lcm_op_occs, include_fields=None): + vnf_lcm_op_occs_dict = vnf_lcm_op_occs.to_dict( + include_fields=include_fields) + + vnf_lcm_op_occs_dict = utils.convert_snakecase_to_camelcase( + vnf_lcm_op_occs_dict) + vnf_lcm_op_occs_dict.pop('errorPoint', None) + + links = self._get_lcm_op_occs_links(vnf_lcm_op_occs) + + vnf_lcm_op_occs_dict.update(links) + return vnf_lcm_op_occs_dict + + def index(self, request, vnf_lcm_op_occs, all_fields=True, + exclude_fields=None, fields=None, exclude_default=False): + + # Find out which fields are to be returned in the response. + if all_fields: + include_fields = set(self.FLATTEN_ATTRIBUTES.keys()) + if fields: + fields = set(fields.split(',')) + attributes = set(self.COMPLEX_ATTRIBUTES).intersection(fields) + for attribute in attributes: + add_fields = set([key for key in self.FLATTEN_ATTRIBUTES. + keys() if key.startswith(attribute)]) + fields = fields.union(add_fields) + + include_fields = set( + _vnf_lcm_op_occs.VnfLcmOpOcc.SIMPLE_ATTRIBUTES).union(fields) + elif exclude_default: + include_fields = set( + _vnf_lcm_op_occs.VnfLcmOpOcc.SIMPLE_ATTRIBUTES) + elif exclude_fields: + exclude_fields = set(exclude_fields.split(',')) + exclude_additional_attributes = set( + self.COMPLEX_ATTRIBUTES).intersection(exclude_fields) + for attribute in exclude_additional_attributes: + fields = set([key for key in self.FLATTEN_ATTRIBUTES.keys() + if key.startswith(attribute)]) + exclude_fields = exclude_fields.union(fields) + + include_fields = set(self.FLATTEN_ATTRIBUTES.keys()) - \ + exclude_fields + + return [ + self._get_vnf_lcm_op_occs_list( + vnf_lcm_op_occ, include_fields=include_fields) + for vnf_lcm_op_occ in vnf_lcm_op_occs] diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index 338d053f9..e4d8f727c 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -42,6 +42,7 @@ from tacker._i18n import _ 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.vnflcm.v1 import sync_resource from tacker.common import exceptions from tacker.common import utils @@ -52,6 +53,7 @@ from tacker.extensions import vnfm from tacker import manager from tacker import objects from tacker.objects import fields +from tacker.objects import vnf_lcm_op_occs as vnf_lcm_op_occs_obj from tacker.objects import vnf_lcm_subscriptions as subscription_obj from tacker.plugins.common import constants from tacker.policies import vnf_lcm as vnf_lcm_policies @@ -185,6 +187,7 @@ class VnfLcmController(wsgi.Controller): super(VnfLcmController, self).__init__() 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() def _get_vnf_instance_href(self, vnf_instance): return '/vnflcm/v1/vnf_instances/%s' % vnf_instance.id @@ -1503,6 +1506,40 @@ class VnfLcmController(wsgi.Controller): return self._make_problem_detail(error_msg, 500, title='Internal Server Error') + @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') + + all_fields = request.GET.get('all_fields') + exclude_default = request.GET.get('exclude_default') + fields = request.GET.get('fields') + exclude_fields = request.GET.get('exclude_fields') + filters = request.GET.get('filter') + 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) + + 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') + + 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) + def _make_problem_detail( self, detail, diff --git a/tacker/api/vnflcm/v1/router.py b/tacker/api/vnflcm/v1/router.py index 9011970dd..a6fafc98a 100644 --- a/tacker/api/vnflcm/v1/router.py +++ b/tacker/api/vnflcm/v1/router.py @@ -138,3 +138,15 @@ class VnflcmAPIRouter(wsgi.Router): methods = {"GET": "subscription_show", "DELETE": "delete_subscription"} self._setup_route(mapper, "/subscriptions/{subscriptionId}", methods, controller, default_resource) + + # {apiRoot}/vnflcm/v1/vnf_lcm_op_occs/{vnfLcmOpOccId}/retry resource + methods = {"POST": "retry"} + self._setup_route(mapper, + "/vnf_lcm_op_occs/{id}/retry", + methods, controller, default_resource) + + # Allowed methods on + # {apiRoot}/vnflcm/v1/vnf_lcm_op_occs resource + methods = {"GET": "list_lcm_op_occs"} + self._setup_route(mapper, "/vnf_lcm_op_occs", + methods, controller, default_resource) diff --git a/tacker/db/db_sqlalchemy/models.py b/tacker/db/db_sqlalchemy/models.py index 351c15a7e..3ba1b38e8 100644 --- a/tacker/db/db_sqlalchemy/models.py +++ b/tacker/db/db_sqlalchemy/models.py @@ -305,6 +305,7 @@ class VnfLcmOpOccs(model_base.BASE, models.SoftDeleteMixin, vnf_instance_id = sa.Column(sa.String(36), sa.ForeignKey('vnf_instances.id'), nullable=False) + grant_id = sa.Column(sa.String(36), nullable=True) state_entered_time = sa.Column(sa.DateTime(), nullable=False) start_time = sa.Column(sa.DateTime(), nullable=False) operation_state = sa.Column(sa.String(length=255), nullable=False) @@ -315,6 +316,7 @@ class VnfLcmOpOccs(model_base.BASE, models.SoftDeleteMixin, error = sa.Column(sa.JSON(), nullable=True) resource_changes = sa.Column(sa.JSON(), nullable=True) changed_info = sa.Column(sa.JSON(), nullable=True) + changed_ext_connectivity = sa.Column(sa.JSON(), nullable=True) error_point = sa.Column(sa.Integer, nullable=False) diff --git a/tacker/db/migration/alembic_migrations/versions/3adac34764da_add_column_to_vnf_lcm_op_occs.py b/tacker/db/migration/alembic_migrations/versions/3adac34764da_add_column_to_vnf_lcm_op_occs.py new file mode 100644 index 000000000..0caf52650 --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/3adac34764da_add_column_to_vnf_lcm_op_occs.py @@ -0,0 +1,38 @@ +# 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_column_to_vnf_lcm_op_occs + +Revision ID: 3adac34764da +Revises: 62d18199909e +Create Date: 2021-02-16 16:19:12.100380 + +""" + +# revision identifiers, used by Alembic. +revision = '3adac34764da' +down_revision = '7186440a306b' + +from alembic import op + +import sqlalchemy as sa +from tacker.db import migration + + +def upgrade(active_plugins=None, options=None): + op.add_column('vnf_lcm_op_occs', + sa.Column('grant_id', sa.VARCHAR(length=36), nullable=True)) + op.add_column('vnf_lcm_op_occs', + sa.Column('changed_ext_connectivity', sa.JSON(), nullable=True)) diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index 2c26318e3..3a103665c 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -7186440a306b +3adac34764da diff --git a/tacker/objects/fields.py b/tacker/objects/fields.py index 7b5a04999..c1951e583 100644 --- a/tacker/objects/fields.py +++ b/tacker/objects/fields.py @@ -225,8 +225,7 @@ class LcmOccsOperationState(BaseTackerEnum): FAILED_TEMP = 'FAILED_TEMP' FAILED = 'FAILED' - ALL = (STARTING, PROCESSING, COMPLETED, - FAILED_TEMP, FAILED) + ALL = (STARTING, PROCESSING, COMPLETED, FAILED_TEMP, FAILED) class LcmOccsOperationType(BaseTackerEnum): diff --git a/tacker/objects/vnf_lcm_op_occs.py b/tacker/objects/vnf_lcm_op_occs.py index d3c6c3b1a..548501263 100644 --- a/tacker/objects/vnf_lcm_op_occs.py +++ b/tacker/objects/vnf_lcm_op_occs.py @@ -15,10 +15,13 @@ from datetime import datetime from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import timeutils +from oslo_versionedobjects import base as ovoo_base from sqlalchemy import exc from sqlalchemy.orm import joinedload +from sqlalchemy_filters import apply_filters 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 @@ -55,11 +58,34 @@ def _vnf_lcm_op_occ_update(context, values): if values.changed_info: update.update({'changed_info': jsonutils.dumps( values.changed_info.to_dict())}) + if 'changed_ext_connectivity' in values: + if values.changed_ext_connectivity: + update.update({'changed_ext_connectivity': jsonutils.dumps( + [chg_ext_conn.to_dict() for chg_ext_conn in + values.changed_ext_connectivity])}) api.model_query(context, models.VnfLcmOpOccs). \ filter_by(id=values.id). \ update(update, synchronize_session=False) +def _make_vnf_lcm_op_occs_list(context, op_occ_list, + db_op_occ_list): + lcm_op_occ_class = VnfLcmOpOcc + + op_occ_list.objects = [] + for db_op_occ in db_op_occ_list: + if(db_op_occ['changed_info'] and + isinstance(db_op_occ['changed_info'], str)): + db_op_occ['changed_info'] = jsonutils.loads( + db_op_occ['changed_info']) + vnf_lcm_op_occ_obj = lcm_op_occ_class._from_db_object( + context, lcm_op_occ_class(context), db_op_occ) + op_occ_list.objects.append(vnf_lcm_op_occ_obj) + + op_occ_list.obj_reset_changes() + return op_occ_list + + @db_api.context_manager.reader def _vnf_lcm_op_occs_get_by_id(context, vnf_lcm_op_occ_id): @@ -91,6 +117,19 @@ def _vnf_lcm_op_occs_get_by_vnf_instance_id(context, vnf_instance_id): return result +@db_api.context_manager.reader +def _vnf_lcm_op_occs_get_by_filters(context, read_deleted=None, + filters=None): + + query = api.model_query(context, models.VnfLcmOpOccs, + read_deleted=read_deleted, project_only=True) + + if filters: + query = apply_filters(query, filters) + + return query.all() + + @db_api.context_manager.reader def _vnf_notify_get_by_id(context, vnf_instance_id, columns_to_join=None): @@ -170,6 +209,7 @@ class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat, 'state_entered_time': fields.DateTimeField(nullable=False), 'start_time': fields.DateTimeField(nullable=False), 'vnf_instance_id': fields.StringField(nullable=False), + 'grant_id': fields.StringField(nullable=True), 'operation': fields.StringField(nullable=False), 'is_automatic_invocation': fields.BooleanField(default=False), 'operation_params': fields.StringField(nullable=True), @@ -180,9 +220,40 @@ class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat, 'ResourceChanges', nullable=True, default=None), 'changed_info': fields.ObjectField( 'VnfInfoModifications', nullable=True, default=None), + 'changed_ext_connectivity': fields.ListOfObjectsField( + 'ExtVirtualLinkInfo', nullable=True, default=[]), 'error_point': fields.IntegerField(nullable=True, default=0) } + ALL_ATTRIBUTES = { + 'id': ('id', 'string', 'VnfLcmOpOccs'), + 'operationState': ('operation_state', 'string', 'VnfLcmOpOccs'), + 'stateEnteredTime': + ('state_entered_time', 'datetime', 'VnfLcmOpOccs'), + 'startTime': ('start_time', 'datetime', 'VnfLcmOpOccs'), + 'vnfInstanceId': ('vnf_instance_id', 'string', 'VnfLcmOpOccs'), + 'grantId': ('grant_id', 'string', 'VnfLcmOpOccs'), + 'operation': ('operation', 'string', 'VnfLcmOpOccs'), + 'isAutomaticInvocation': + ('is_automatic_invocation', 'boolean', 'VnfLcmOpOccs'), + 'isCancelPending': ('is_cancel_pending', 'string', 'VnfLcmOpOccs'), + 'errorPoint': ('error_point', 'number', 'VnfLcmOpOccs'), + 'operationParams': ('operation_params', 'string', 'VnfLcmOpOccs'), + 'error': ('error', 'string', 'VnfLcmOpOccs'), + 'resourceChanges': ('resource_changes', 'string', 'VnfLcmOpOccs'), + 'changedInfo': ('changed_info', 'string', 'VnfLcmOpOccs') + } + + FLATTEN_ATTRIBUTES = utils.flatten_dict(ALL_ATTRIBUTES.copy()) + + SIMPLE_ATTRIBUTES = ['id', 'operationState', 'stateEnteredTime', + 'startTime', 'vnfInstanceId', 'grantId', 'operation', + 'isAutomaticInvocation', + 'isCancelPending', 'errorPoint'] + + COMPLEX_ATTRIBUTES = ['error', 'resourceChanges', 'changedInfo', + 'operationParams', 'changedExtConnectivity'] + @base.remotable def create(self): updates = self.obj_clone() @@ -197,7 +268,9 @@ class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat, def _from_db_object(context, vnf_lcm_op_occ_obj, db_vnf_lcm_op_occ): special_fields = ['error', - 'resource_changes', 'changed_info'] + 'resource_changes', + 'changed_info', + 'changed_ext_connectivity'] for key in vnf_lcm_op_occ_obj.fields: if key in special_fields: continue @@ -214,6 +287,12 @@ class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat, changed_info = VnfInfoModifications.obj_from_primitive( db_vnf_lcm_op_occ['changed_info'], context) vnf_lcm_op_occ_obj.changed_info = changed_info + if db_vnf_lcm_op_occ['changed_ext_connectivity']: + changed_ext_conn = \ + [objects.ExtVirtualLinkInfo.obj_from_primitive( + chg_ext_conn, context) for chg_ext_conn in + db_vnf_lcm_op_occ['changed_ext_connectivity']] + vnf_lcm_op_occ_obj.changed_ext_connectivity = changed_ext_conn vnf_lcm_op_occ_obj._context = context vnf_lcm_op_occ_obj.obj_reset_changes() @@ -238,6 +317,11 @@ class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat, obj_data = VnfInfoModifications._from_dict( primitive.get('changed_info')) primitive.update({'changed_info': obj_data}) + if 'changed_ext_connectivity' in primitive.keys(): + obj_data = [objects.ExtVirtualLinkInfo.obj_from_primitive( + chg_ext_conn, context) for chg_ext_conn in + primitive.get('changed_ext_connectivity')] + primitive.update({'changed_ext_connectivity': obj_data}) vnf_lcm_op_occ = VnfLcmOpOcc._from_dict(primitive) return vnf_lcm_op_occ @@ -252,6 +336,7 @@ class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat, state_entered_time = data_dict.get('state_entered_time') start_time = data_dict.get('start_time') vnf_instance_id = data_dict.get('vnf_instance_id') + grant_id = data_dict.get('grant_id') operation = data_dict.get('operation') is_automatic_invocation = data_dict.get('is_automatic_invocation') operation_params = data_dict.get('operation_params') @@ -259,12 +344,14 @@ class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat, error = data_dict.get('error') resource_changes = data_dict.get('resource_changes') changed_info = data_dict.get('changed_info') + changed_ext_connectivity = data_dict.get('changed_ext_connectivity') error_point = data_dict.get('error_point') obj = cls(operation_state=operation_state, state_entered_time=state_entered_time, start_time=start_time, vnf_instance_id=vnf_instance_id, + grant_id=grant_id, operation=operation, is_automatic_invocation=is_automatic_invocation, operation_params=operation_params, @@ -272,28 +359,76 @@ class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat, error=error, resource_changes=resource_changes, changed_info=changed_info, + changed_ext_connectivity=changed_ext_connectivity, error_point=error_point ) return obj - def to_dict(self): - data = {'id': self.id, - 'operation_state': self.operation_state, - 'state_entered_time': self.state_entered_time, - 'start_time': self.start_time, - 'vnf_instance_id': self.vnf_instance_id, - 'operation': self.operation, - 'is_automatic_invocation': self.is_automatic_invocation, - 'operation_params': self.operation_params, - 'is_cancel_pending': self.is_cancel_pending, - 'error_point': self.error_point} + def _get_error(self, include_fields=None): + key = 'error' + if key in include_fields: + return {key: self.error.to_dict()} + + def _get_resource_changes(self, include_fields=None): + key = 'resourceChanges' + if key in include_fields: + return {key: self.resource_changes.to_dict()} + + def _get_changed_info(self, include_fields=None): + key = 'changedInfo' + if key in include_fields: + return {key: self.changed_info.to_dict()} + + def _get_operation_params(self, include_fields=None): + key = 'operationParams' + if key in include_fields: + return {key: self.operation_params} + + def _get_changed_ext_connectivity(self, include_fields=None): + key = 'changedExtConnectivity' + return {key: [chg_ext_conn.to_dict() for chg_ext_conn in + self.changed_ext_connectivity]} + + def to_dict(self, include_fields=None): + data = dict() + if not include_fields: + include_fields = set(self.FLATTEN_ATTRIBUTES.keys()) + + # add simple fields + to_fields = set(self.SIMPLE_ATTRIBUTES).intersection(include_fields) + for field in to_fields: + data[field] = getattr(self, self.FLATTEN_ATTRIBUTES[field][0]) + + # add complex attributes if self.error: - data.update({'error': self.error.to_dict()}) + error = self._get_error(include_fields=include_fields) + if error: + data.update(error) + if self.resource_changes: - data.update({'resource_changes': self.resource_changes.to_dict()}) + resource_changes = self._get_resource_changes( + include_fields=include_fields) + if resource_changes: + data.update(resource_changes) + if self.changed_info: - data.update({'changed_info': self.changed_info.to_dict()}) + changed_info = self._get_changed_info( + include_fields=include_fields) + if changed_info: + data.update(changed_info) + + if self.operation_params: + operation_params = self._get_operation_params( + include_fields=include_fields) + if operation_params: + data.update(operation_params) + + if self.changed_ext_connectivity: + changed_ext_connectivity = self._get_changed_ext_connectivity( + include_fields=include_fields) + if changed_ext_connectivity: + data.update(changed_ext_connectivity) return data @@ -309,6 +444,22 @@ class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat, return cls._from_db_object(context, cls(), db_vnf_lcm_op_occs) +@base.TackerObjectRegistry.register +class VnfLcmOpOccList(ovoo_base.ObjectListBase, base.TackerObject): + + VERSION = '1.0' + + fields = { + 'objects': fields.ListOfObjectsField('VnfLcmOpOcc') + } + + @base.remotable_classmethod + def get_by_filters(cls, context, read_deleted=None, filters=None): + db_vnf_lcm_op_occs = _vnf_lcm_op_occs_get_by_filters( + context, read_deleted=read_deleted, filters=filters) + return _make_vnf_lcm_op_occs_list(context, cls(), db_vnf_lcm_op_occs) + + @base.TackerObjectRegistry.register class ResourceChanges(base.TackerObject, base.TackerPersistentObject): diff --git a/tacker/policies/vnf_lcm.py b/tacker/policies/vnf_lcm.py index 0f0ed4684..5a8a8d307 100644 --- a/tacker/policies/vnf_lcm.py +++ b/tacker/policies/vnf_lcm.py @@ -99,6 +99,17 @@ rules = [ } ] ), + policy.DocumentedRuleDefault( + name=VNFLCM % 'list_lcm_op_occs', + check_str=base.RULE_ADMIN_OR_OWNER, + description="Query VNF LCM operation occurrence", + operations=[ + { + 'method': 'GET', + 'path': '/vnflcm/v1/vnf_lcm_op_occs' + } + ] + ), policy.DocumentedRuleDefault( name=VNFLCM % 'index', check_str=base.RULE_ADMIN_OR_OWNER, diff --git a/tacker/tests/functional/sol/vnflcm/base.py b/tacker/tests/functional/sol/vnflcm/base.py index e66a78162..090aa22a5 100644 --- a/tacker/tests/functional/sol/vnflcm/base.py +++ b/tacker/tests/functional/sol/vnflcm/base.py @@ -487,6 +487,14 @@ class BaseVnfLcmTest(base.BaseTackerTest): return resp, response_body + def _list_op_occs(self, filter_string=''): + show_url = os.path.join( + self.base_vnf_lcm_op_occs_url) + resp, response_body = self.http_client.do_request( + show_url + filter_string, "GET") + + return resp, response_body + def _wait_terminate_vnf_instance(self, id, timeout=None): start_time = int(time.time()) 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 d00c60c9b..7b1e8604a 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 @@ -778,6 +778,8 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): - Get vnflcmOpOccId to retry. - Retry instantiation operation. - Get opOccs information. + - Get opOccs list. + - Delete VNF instance. - Delete subscription. """ # Create subscription and register it. @@ -855,6 +857,10 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): resp, op_occs_info = self._show_op_occs(vnflcm_op_occ_id) self._assert_occ_show(resp, op_occs_info) + # occ-list + resp, op_occs_info = self._list_op_occs() + self._assert_occ_list(resp, op_occs_info) + # Delete VNF resp, _ = self._delete_vnf_instance(vnf_instance_id) self._wait_lcm_done(vnf_instance_id=vnf_instance_id) @@ -877,7 +883,9 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): - Get vnfcmOpOccId to retry. - Retry Scale-Out operation. - Get opOccs information. - - Terminate VNF. + - Get opOccs list. + - Terminate VNF instance. + - Delete VNF instance. - Delete subscription. """ # Create subscription and register it. @@ -964,6 +972,10 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): resp, op_occs_info = self._show_op_occs(vnflcm_op_occ_id) self._assert_occ_show(resp, op_occs_info) + # occ-list + resp, op_occs_info = self._list_op_occs() + self._assert_occ_list(resp, op_occs_info) + # Terminate VNF stack = self._get_heat_stack(vnf_instance_id) resources_list = self._get_heat_resource_list(stack.id) @@ -1000,6 +1012,7 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): - Get vnflcmOpOccId to rollback. - Rollback instantiation operation. - Get opOccs information. + - Get opOccs list - Delete subscription. """ # Create subscription and register it. @@ -1072,6 +1085,10 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): resp, op_occs_info = self._show_op_occs(vnflcm_op_occ_id) self._assert_occ_show(resp, op_occs_info) + # occ-list + resp, op_occs_info = self._list_op_occs() + self._assert_occ_list(resp, op_occs_info) + # Delete VNF resp, _ = self._delete_vnf_instance(vnf_instance_id) self._wait_lcm_done(vnf_instance_id=vnf_instance_id) @@ -1094,6 +1111,7 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): - Get vnfcmOpOccId to rollback. - Rollback Scale-Out operation. - Get opOccs information. + - get opOccs List. - Terminate VNF. - Delete subscription. """ @@ -1176,6 +1194,10 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): resp, op_occs_info = self._show_op_occs(vnflcm_op_occ_id) self._assert_occ_show(resp, op_occs_info) + # occ-list + resp, op_occs_info = self._list_op_occs() + self._assert_occ_list(resp, op_occs_info) + # Terminate VNF stack = self._get_heat_stack(vnf_instance_id) resources_list = self._get_heat_resource_list(stack.id) @@ -1283,6 +1305,10 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): resp, op_occs_info = self._show_op_occs(vnflcm_op_occ_id) self._assert_occ_show(resp, op_occs_info) + # occ-list + resp, op_occs_info = self._list_op_occs() + self._assert_occ_list(resp, op_occs_info) + # Delete Stack stack = self._get_heat_stack(vnf_instance_id) self._delete_heat_stack(stack.id) @@ -1391,6 +1417,10 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): resp, op_occs_info = self._show_op_occs(vnflcm_op_occ_id) self._assert_occ_show(resp, op_occs_info) + # occ-list + resp, op_occs_info = self._list_op_occs() + self._assert_occ_list(resp, op_occs_info) + # Terminate VNF stack = self._get_heat_stack(vnf_instance_id) resources_list = self._get_heat_resource_list(stack.id) @@ -1562,3 +1592,24 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): self.assertIsNotNone(_links.get('vnfInstance').get('href')) self.assertIsNotNone(_links.get('grant')) self.assertIsNotNone(_links.get('grant').get('href')) + + def _assert_occ_list(self, resp, op_occs_list): + self.assertEqual(200, resp.status_code) + + # Only check required parameters. + for op_occs_info in op_occs_list: + self.assertIsNotNone(op_occs_info.get('id')) + self.assertIsNotNone(op_occs_info.get('operationState')) + self.assertIsNotNone(op_occs_info.get('stateEnteredTime')) + self.assertIsNotNone(op_occs_info.get('vnfInstanceId')) + self.assertIsNotNone(op_occs_info.get('operation')) + self.assertIsNotNone(op_occs_info.get('isAutomaticInvocation')) + self.assertIsNotNone(op_occs_info.get('isCancelPending')) + + _links = op_occs_info.get('_links') + self.assertIsNotNone(_links.get('self')) + self.assertIsNotNone(_links.get('self').get('href')) + self.assertIsNotNone(_links.get('vnfInstance')) + self.assertIsNotNone(_links.get('vnfInstance').get('href')) + self.assertIsNotNone(_links.get('grant')) + self.assertIsNotNone(_links.get('grant').get('href')) diff --git a/tacker/tests/unit/objects/fakes.py b/tacker/tests/unit/objects/fakes.py index ae7b7b322..553db1e7f 100644 --- a/tacker/tests/unit/objects/fakes.py +++ b/tacker/tests/unit/objects/fakes.py @@ -512,3 +512,23 @@ def get_vnf(vnfd_id, vim_id): 'placement_attr': "test_placement_attr", 'vim_id': vim_id } + + +def get_changed_ext_conn_data(): + return [{ + "id": uuidsentinel.change_ext_conn_id, + "resource_handle": { + "vim_connection_id": uuidsentinel.vim_connection_id, + "resource_id": uuidsentinel.vl_resource_id, + "vim_level_resource_type": "OS::Neutron::Net", + }, + "ext_link_ports": [{ + "id": uuidsentinel.ext_link_ports_id, + "resource_handle": { + "vim_connection_id": uuidsentinel.vim_connection_id, + "resource_id": uuidsentinel.port_resource_id, + "vim_level_resource_type": "OS::Neutron::Port", + }, + "cp_instance_id": uuidsentinel.cp_instance_id, + }] + }] diff --git a/tacker/tests/unit/objects/test_vnf_lcm_op_occs.py b/tacker/tests/unit/objects/test_vnf_lcm_op_occs.py index d5e74b84a..9d4c7825e 100644 --- a/tacker/tests/unit/objects/test_vnf_lcm_op_occs.py +++ b/tacker/tests/unit/objects/test_vnf_lcm_op_occs.py @@ -90,10 +90,13 @@ class TestVnfLcmOpOcc(SqlTestCase): problem_obj.detail = 'test_err' changed_info = objects.vnf_lcm_op_occs.VnfInfoModifications( context=self.context, **fakes.get_changed_info_data()) + changed_ext_conn = [objects.ExtVirtualLinkInfo.obj_from_primitive( + i, self.context) for i in fakes.get_changed_ext_conn_data()] vnf_lcm_op_occs.operation_state = 'FAILED_TEMP' vnf_lcm_op_occs.error = problem_obj vnf_lcm_op_occs.id = uuidsentinel.vnf_lcm_op_occs_id vnf_lcm_op_occs.changed_info = changed_info + vnf_lcm_op_occs.changed_ext_connectivity = changed_ext_conn vnf_lcm_op_occs.save() self.assertEqual('FAILED_TEMP', vnf_lcm_op_occs.operation_state) diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index 8d14f42ab..21654bea9 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -1265,20 +1265,28 @@ VNFLCMOPOCC_RESPONSE = { "href": CONF.vnf_lcm.endpoint_url + '/vnflcm/v1/vnf_instances/' 'f26f181d-7891-4720-b022-b074ec1733ef' }, + "retry": { + "href": CONF.vnf_lcm.endpoint_url + '/vnflcm/v1/vnf_lcm_op_occs/' + 'f26f181d-7891-4720-b022-b074ec1733ef/retry' + }, "rollback": { "href": CONF.vnf_lcm.endpoint_url + '/vnflcm/v1/vnf_lcm_op_occs/' 'f26f181d-7891-4720-b022-b074ec1733ef/rollback' }, "grant": { "href": CONF.vnf_lcm.endpoint_url + '/vnflcm/v1/vnf_lcm_op_occs/' - 'f26f181d-7891-4720-b022-b074ec1733ef/grant' - }}, + 'f26f181d-7891-4720-b022-b074ec1733ef/grant', + }, + "fail": { + "href": CONF.vnf_lcm.endpoint_url + '/vnflcm/v1/vnf_lcm_op_occs/' + 'f26f181d-7891-4720-b022-b074ec1733ef/fail'}}, 'operationState': 'COMPLETED', 'stateEnteredTime': datetime.datetime(1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC), 'startTime': datetime.datetime(1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC), 'vnfInstanceId': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'grantId': 'f26f181d-7891-4720-b022-b074ec1733ef', 'operation': 'MODIFY_INFO', 'isAutomaticInvocation': False, 'operationParams': '{"is_reverse": False, "is_auto": False}', @@ -1336,7 +1344,24 @@ VNFLCMOPOCC_RESPONSE = { 'vnfProductName': 'fake_vnf_product_name', 'vnfSoftwareVersion': 'fake_vnf_software_version', 'vnfdVersion': 'fake_vnfd_version' - } + }, + "changedExtConnectivity": [{ + "id": constants.UUID, + "resourceHandle": { + "vimConnectionId": constants.UUID, + "resourceId": constants.UUID, + "vimLevelResourceType": "OS::Neutron::Net", + }, + "extLinkPorts": [{ + "id": constants.UUID, + "resourceHandle": { + "vimConnectionId": constants.UUID, + "resourceId": constants.UUID, + "vimLevelResourceType": "OS::Neutron::Port", + }, + "cpInstanceId": constants.UUID, + }] + }] } VNFLCMOPOCC_INDEX_RESPONSE = [VNFLCMOPOCC_RESPONSE] @@ -1436,6 +1461,28 @@ def fake_vnf_lcm_op_occs(): } changed_info_obj = objects.VnfInfoModifications(**changed_info) + changed_ext_connectivity = [{ + "id": constants.UUID, + "resource_handle": { + "vim_connection_id": constants.UUID, + "resource_id": constants.UUID, + "vim_level_resource_type": "OS::Neutron::Net", + }, + "ext_link_ports": [{ + "id": constants.UUID, + "resource_handle": { + "vim_connection_id": constants.UUID, + "resource_id": constants.UUID, + "vim_level_resource_type": "OS::Neutron::Port", + }, + "cp_instance_id": constants.UUID, + }] + }] + changed_ext_connectivity_obj = \ + [objects.ExtVirtualLinkInfo.obj_from_primitive( + chg_ext_conn, context) for chg_ext_conn in + changed_ext_connectivity] + dt = datetime.datetime(1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) vnf_lcm_op_occs = { 'id': constants.UUID, @@ -1443,13 +1490,15 @@ def fake_vnf_lcm_op_occs(): 'state_entered_time': dt, 'start_time': dt, 'vnf_instance_id': constants.UUID, + 'grant_id': constants.UUID, 'operation': 'MODIFY_INFO', 'is_automatic_invocation': False, 'operation_params': '{"is_reverse": False, "is_auto": False}', 'is_cancel_pending': False, 'error': error_obj, 'resource_changes': resource_changes_obj, - 'changed_info': changed_info_obj + 'changed_info': changed_info_obj, + 'changed_ext_connectivity': changed_ext_connectivity_obj, } return vnf_lcm_op_occs @@ -1484,3 +1533,10 @@ def vnf_data(status='ACTIVE'): name='test', status=status, vim_id=uuidsentinel.vim_id) + + +def return_vnf_lcm_opoccs_list(): + vnf_lcm_op_occs = fake_vnf_lcm_op_occs() + 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 02e0a3fbc..a211f9035 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -3366,3 +3366,141 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.INTERNAL_SERVER_ERROR, resp.status_code) + + @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") + def test_op_occ_list(self, mock_op_occ_list): + req = fake_request.HTTPRequest.blank('/vnflcm/v1/vnf_lcm_op_occs') + + complex_attributes = [ + 'error', + 'resourceChanges', + 'operationParams', + 'changedInfo'] + + vnf_lcm_op_occ = fakes.return_vnf_lcm_opoccs_list() + 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) + + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") + @ddt.data( + {'filter': '(eq,id,f26f181d-7891-4720-b022-b074ec1733ef)'}, + {'filter': '(neq,operationState,COMPLETED)'}, + {'filter': '(gte,stateEnteredTime,2020-03-14 04:10:15+00:00)'}, + {'filter': '(eq,isAutomaticInvocation,False)'}, + {'filter': '(lt,errorPoint,4)'}, + {'filter': + """(eq,error,'"{"title": "ERROR", + "status": 500, "detail": "ERROR"}"')"""}, + {'filter': + """(in,changedInfo,'"{"vnfInstanceName": "test"}"')"""}, + {'filter': + """(neq,operationParams,'"{"terminationType": "FORCEFUL"}"')"""}, + ) + def test_op_occ_filter_attributes(self, filter_params, + mock_op_occ_list): + query = urllib.parse.urlencode(filter_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_list() + 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) + + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") + def test_op_occ_filter_attributes_invalid_filter(self, mock_op_occ_list): + query = urllib.parse.urlencode({'filter': '(lt,non_existing,4)'}) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_lcm_op_occs?' + query) + + vnf_lcm_op_occ = fakes.return_vnf_lcm_opoccs_list() + mock_op_occ_list.return_value = vnf_lcm_op_occ + self.assertRaises( + exceptions.ValidationError, self.controller.list_lcm_op_occs, req) + + @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") + def test_op_occ_attribute_selector_all_fields(self, mock_op_occ_list): + params = {'all_fields': 'True'} + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnflcm/v1/vnf_lcm_op_occs?' + + query) + + vnf_lcm_op_occ = fakes.return_vnf_lcm_opoccs_list() + expected_result = fakes.index_response() + 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) + + @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") + @ddt.data( + {'fields': 'error'}, + {'fields': 'resourceChanges'}, + {'fields': 'operationParams'}, + {'fields': 'changedInfo'} + ) + def test_op_occ_attribute_selector_fields(self, filter_params, + mock_op_occ_list): + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_lcm_op_occs?' + query) + + complex_attributes = [ + 'error', + 'resourceChanges', + 'operationParams', + 'changedInfo'] + + remove_attributes = [ + x for x in complex_attributes if x != filter_params['fields']] + + vnf_lcm_op_occ = fakes.return_vnf_lcm_opoccs_list() + 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) + + @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") + @ddt.data( + {'exclude_fields': 'error'}, + {'exclude_fields': 'resourceChanges'}, + {'exclude_fields': 'operationParams'}, + {'exclude_fields': 'changedInfo'} + ) + def test_op_occ_attribute_selector_exclude_fields(self, filter_params, + mock_op_occ_list): + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_lcm_op_occs?' + query) + + remove_attributes = [filter_params['exclude_fields']] + + vnf_lcm_op_occ = fakes.return_vnf_lcm_opoccs_list() + 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) + + @mock.patch.object(objects.VnfLcmOpOccList, "get_by_filters") + def test_op_occ_attribute_selector_fields_error(self, mock_op_occ_list): + query = urllib.parse.urlencode({'fields': 'non_existent_column'}) + req = fake_request.HTTPRequest.blank( + '/vnflcm/v1/vnf_lcm_op_occs?' + query) + + vnf_lcm_op_occ = fakes.return_vnf_lcm_opoccs_list() + mock_op_occ_list.return_value = vnf_lcm_op_occ + self.assertRaises( + exceptions.ValidationError, self.controller.list_lcm_op_occs, req)