commit 6bf70be97cb94f7ba5ac3fa1b885304959c81f76 Author: Koichi Edagawa Date: Mon Aug 31 16:43:31 2020 +0900 Support Flow of the Get Operation Status Supported Flow of the Get Operation Status as the part of LCM notifications for VNF based on ETSI NFV-SOL specification. Implements: blueprint support-etsi-nfv-specs Spec: https://specs.openstack.org/openstack/tacker-specs/specs/victoria/support-notification-api-based-on-etsi-nfv-sol.html Change-Id: Id23d025aeba568c4c67adc745171c0540140d233 (cherry picked from commit 13065dfbdde4228106f77d2b503260d4496464c6) diff --git a/api-ref/source/v1/parameters_vnflcm.yaml b/api-ref/source/v1/parameters_vnflcm.yaml index 3258f0a..8d774c4 100644 --- a/api-ref/source/v1/parameters_vnflcm.yaml +++ b/api-ref/source/v1/parameters_vnflcm.yaml @@ -12,6 +12,12 @@ vnf_instance_id: in: path required: true type: string +vnf_lcm_op_occ_id: + description: | + Identifier of the VNF lifecycle management operation occurrence. + in: path + required: true + type: string # variables in body added_storage_resource_ids: @@ -714,6 +720,15 @@ is_automatic_invocation: in: body required: true type: boolean +is_cancel_pending: + description: | + If the VNF LCM operation occurrence is in "STARTING", + "PROCESSING" or "ROLLING_BACK" state and the + operation is being cancelled, this attribute shall be set to + true. Otherwise, it shall be set to false. + in: body + required: true + type: boolean is_dynamic: description: | Indicates whether this set of addresses was assigned dynamically (true) @@ -827,6 +842,45 @@ notification_vnf_lcm_op_occ_id: in: body required: true type: string +operation: + description: | + Type of the actual LCM operation represented by this + VNF LCM operation occurrence. + in: body + required: true + type: string +operation_params: + description: | + Input parameters of the LCM operation. This attribute + shall be formatted according to the request data type of + the related LCM operation. + The following mapping between operationType and the + data type of this attribute shall apply: + + INSTANTIATE: InstantiateVnfRequest + + SCALE: ScaleVnfRequest + + HEAL: HealVnfRequest + + TERMINATE: TerminateVnfRequest + + MODIFY_INFO: VnfInfoModificationRequest + + This attribute shall be present if this data type is returned + in a response to reading an individual resource, and may + be present according to the chosen attribute selector + parameter if this data type is returned in a response to a + query of a container resource. + in: body + required: false + type: string +operation_state: + description: | + The state of the LCM operation. + in: body + required: true + type: string removed_storage_resource_ids: description: | References to VirtualStorage resources that @@ -840,6 +894,14 @@ removed_storage_resource_ids: in: body required: false type: array +resource_changes: + description: | + This attribute contains information about the cumulative + changes to virtualised resources that were performed so + far by the LCM operation since its start, if applicable. + in: body + required: false + type: object resource_handle: description: | Reference to the resource realizing this VL. @@ -884,6 +946,18 @@ scale_status_scale_level: in: body required: true type: string +start_time: + description: | + Date-time of the start of the operation. + in: body + required: true + type: string +state_entered_time: + description: | + Date-time when the current state has been entered. + in: body + required: true + type: string subnet_id: description: | Subnet defined by the identifier of the subnet resource in the VIM. @@ -1170,6 +1244,18 @@ vnf_instance_vnfd_version: in: body required: true type: string +vnf_lcm_op_occ_id_response: + description: | + Identifier of this VNF lifecycle management operation occurrence. + in: body + required: true + type: string +vnf_lcm_vnf_instance_id: + description: | + Identifier of the VNF instance to which the operation applies. + in: body + required: true + type: string vnf_link_port_cp_instance_id: description: | When the link port is used for external connectivity by the VNF, this 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 new file mode 100644 index 0000000..7a05da7 --- /dev/null +++ b/api-ref/source/v1/samples/vnflcm/show-vnflcm-operation-occurrence-response.json @@ -0,0 +1,70 @@ +{ + "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", + "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": "/grant/v1/grants/3432cebe-db0a-11e8-9023-005056317abe" + } + } +} diff --git a/api-ref/source/v1/vnflcm.inc b/api-ref/source/v1/vnflcm.inc index a7433d6..2471048 100644 --- a/api-ref/source/v1/vnflcm.inc +++ b/api-ref/source/v1/vnflcm.inc @@ -570,6 +570,110 @@ Response Example .. literalinclude:: samples/vnflcm/list-vnf-instance-response.json :language: javascript +Show VNF LCM operation occurrence +================================= + +.. rest_method:: GET /vnflcm/v1/vnf_lcm_op_occs/{vnfLcmOpOccId} + +The client can use this method to retrieve status information about a VNF lifecycle management operation occurrence +by reading an "Individual VNF LCM operation occurrence" resource. + +Response Codes +-------------- + +.. rest_status_code:: success status.yaml + + - 200 + +.. rest_status_code:: error status.yaml + + - 400 + - 401 + - 403 + - 404 + +Request Parameters +------------------ + +.. rest_parameters:: parameters_vnflcm.yaml + + - vnfLcmOpOccId: vnf_lcm_op_occ_id + +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 + - 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 + - _links: vnf_instance_links + +Response Example +---------------- + +.. literalinclude:: samples/vnflcm/show-vnflcm-operation-occurrence-response.json + :language: javascript + Create a new subscription ========================= diff --git a/tacker/api/views/vnf_lcm.py b/tacker/api/views/vnf_lcm.py index 5d454ed..2ae4ff8 100644 --- a/tacker/api/views/vnf_lcm.py +++ b/tacker/api/views/vnf_lcm.py @@ -89,7 +89,35 @@ class ViewBuilder(base.BaseViewBuilder): return vim_connections - def _get_vnf_instance_info(self, vnf_instance): + 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} + }, + "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} + } + } + + return {"_links": _links} + + def _get_vnf_instance_info(self, + vnf_instance, api_version=None): vnf_instance_dict = vnf_instance.to_dict() if vnf_instance_dict.get('vim_connection_info'): vnf_instance_dict['vim_connection_info'] = \ @@ -108,6 +136,17 @@ class ViewBuilder(base.BaseViewBuilder): vnf_instance_dict.update(links) return vnf_instance_dict + 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) + + 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 create(self, vnf_instance): return self._get_vnf_instance_info(vnf_instance) @@ -224,3 +263,6 @@ class ViewBuilder(base.BaseViewBuilder): def subscription_show(self, vnf_lcm_subscriptions): return self._get_vnf_lcm_subscription(vnf_lcm_subscriptions) + + 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/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index 730bbb9..9370ccd 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -545,6 +545,24 @@ class VnfLcmController(wsgi.Controller): vnf_instance = self._get_vnf_instance(context, id) self._heal(context, vnf_instance, vnf, body) + @wsgi.response(http_client.OK) + @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND)) + def show_lcm_op_occs(self, request, id): + context = request.environ['tacker.context'] + context.can(vnf_lcm_policies.VNFLCM % 'show_lcm_op_occs') + + try: + vnf_lcm_op_occs = objects.VnfLcmOpOcc.get_by_id(context, id) + except exceptions.NotFound as occ_e: + return self._make_problem_detail(str(occ_e), + 404, title='VnfLcmOpOcc NOT FOUND') + except Exception as e: + LOG.error(traceback.format_exc()) + return self._make_problem_detail(str(e), + 500, title='Internal Server Error') + + return self._view_builder.show_lcm_op_occs(vnf_lcm_op_occs) + @wsgi.response(http_client.CREATED) @validation.schema(vnf_lcm.register_subscription) def register_subscription(self, request, body): diff --git a/tacker/api/vnflcm/v1/router.py b/tacker/api/vnflcm/v1/router.py index 4d206bb..7e28e02 100644 --- a/tacker/api/vnflcm/v1/router.py +++ b/tacker/api/vnflcm/v1/router.py @@ -78,6 +78,12 @@ class VnflcmAPIRouter(wsgi.Router): methods, controller, default_resource) # Allowed methods on + # /vnflcm/v1/vnf_lcm_op_occs/{vnfLcmOpOccId} resource + methods = {"GET": "show_lcm_op_occs"} + self._setup_route(mapper, "/vnf_lcm_op_occs/{id}", + methods, controller, default_resource) + + # Allowed methods on # /vnflcm/v1/vnf_instances/{vnfInstanceId}/terminate resource methods = {"POST": "terminate"} self._setup_route(mapper, diff --git a/tacker/objects/fields.py b/tacker/objects/fields.py index bba05ec..6cf1dda 100644 --- a/tacker/objects/fields.py +++ b/tacker/objects/fields.py @@ -186,6 +186,37 @@ class VnfcState(BaseTackerEnum): ALL = (STARTED, STOPPED) +class InstanceOperationalState(BaseTackerEnum): + STARTING = 'STARTING' + PROCESSING = 'PROCESSING' + COMPLETED = 'COMPLETED' + FAILED_TEMP = 'FAILED_TEMP' + ROLLING_BACK = 'ROLLING_BACK' + ROLLED_BACK = 'ROLLED_BACK' + + ALL = (STARTING, PROCESSING, COMPLETED, FAILED_TEMP, + ROLLING_BACK, ROLLED_BACK) + + +class InstanceOperationalStateField(BaseEnumField): + AUTO_TYPE = InstanceOperationalState() + + +class InstanceOperation(BaseTackerEnum): + INSTANTIATE = 'INSTANTIATE' + SCALE = 'SCALE' + TERMINATE = 'TERMINATE' + HEAL = 'HEAL' + MODIFY_INFO = 'MODIFY_INFO' + + ALL = (INSTANTIATE, SCALE, + TERMINATE, HEAL, MODIFY_INFO) + + +class InstanceOperationField(BaseEnumField): + AUTO_TYPE = InstanceOperation() + + class LcmOccsOperationState(BaseTackerEnum): STARTING = 'STARTING' PROCESSING = 'PROCESSING' diff --git a/tacker/policies/vnf_lcm.py b/tacker/policies/vnf_lcm.py index cb9c398..c77a0c8 100644 --- a/tacker/policies/vnf_lcm.py +++ b/tacker/policies/vnf_lcm.py @@ -78,6 +78,17 @@ rules = [ ] ), policy.DocumentedRuleDefault( + name=VNFLCM % 'show_lcm_op_occs', + check_str=base.RULE_ADMIN_OR_OWNER, + description="Query an Individual VNF LCM operation occurrence", + operations=[ + { + 'method': 'GET', + 'path': '/vnflcm/v1/vnf_lcm_op_occs/{vnfLcmOpOccId}' + } + ] + ), + policy.DocumentedRuleDefault( name=VNFLCM % 'index', check_str=base.RULE_ADMIN_OR_OWNER, description="Query VNF instances.", diff --git a/tacker/tests/unit/conductor/fakes.py b/tacker/tests/unit/conductor/fakes.py index 7d713f0..ff94dc9 100644 --- a/tacker/tests/unit/conductor/fakes.py +++ b/tacker/tests/unit/conductor/fakes.py @@ -103,6 +103,7 @@ def get_vnf_package_vnfd(): def get_lcm_op_occs_data(): return { + "id": uuidsentinel.lcm_op_occs_id, "tenant_id": uuidsentinel.tenant_id, 'operation_state': 'PROCESSING', 'state_entered_time': diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index de2d259..d327e34 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. - +from copy import deepcopy import datetime import iso8601 import os @@ -32,6 +32,9 @@ from tacker.tests import constants from tacker.tests import uuidsentinel from tacker import wsgi +import tacker.conf +CONF = tacker.conf.CONF + def return_default_vim(): default_vim = { @@ -140,12 +143,44 @@ def return_vnf_instance_model( def return_vnf_instance( instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED, + scale_status=None, **updates): if instantiated_state == fields.VnfInstanceState.NOT_INSTANTIATED: data = _model_non_instantiated_vnf_instance(**updates) data['instantiation_state'] = instantiated_state vnf_instance_obj = objects.VnfInstance(**data) + + elif scale_status: + data = _model_non_instantiated_vnf_instance(**updates) + data['instantiation_state'] = instantiated_state + vnf_instance_obj = objects.VnfInstance(**data) + + get_instantiated_vnf_info = { + 'flavour_id': uuidsentinel.flavour_id, + 'vnf_state': 'STARTED', + 'instance_id': uuidsentinel.instance_id + } + instantiated_vnf_info = get_instantiated_vnf_info + + s_status = {"aspect_id": "SP1", "scale_level": 1} + scale_status = objects.ScaleInfo(**s_status) + + instantiated_vnf_info.update( + {"ext_cp_info": [], + 'ext_virtual_link_info': [], + 'ext_managed_virtual_link_info': [], + 'vnfc_resource_info': [], + 'vnf_virtual_link_resource_info': [], + 'virtual_storage_resource_info': [], + "flavour_id": "simple", + "scale_status": [scale_status], + "vnf_instance_id": "171f3af2-a753-468a-b5a7-e3e048160a79", + "additional_params": {"key": "value"}, + 'vnf_state': "STARTED"}) + info_data = objects.InstantiatedVnfInfo(**instantiated_vnf_info) + + vnf_instance_obj.instantiated_vnf_info = info_data else: data = _model_non_instantiated_vnf_instance(**updates) data['instantiation_state'] = instantiated_state @@ -704,6 +739,27 @@ def get_instantiate_vnf_request_with_ext_virtual_links(**updates): return instantiate_vnf_request +def _get_vnf(**updates): + vnf_data = { + 'tenant_id': uuidsentinel.tenant_id, + 'name': "fake_name", + 'vnfd_id': uuidsentinel.vnfd_id, + 'vnf_instance_id': uuidsentinel.instance_id, + 'mgmt_ip_address': "fake_mgmt_ip_address", + 'status': 'ACTIVE', + 'description': 'fake_description', + 'placement_attr': 'fake_placement_attr', + 'vim_id': 'uuidsentinel.vim_id', + 'error_reason': 'fake_error_reason', + 'attributes': { + "scale_group": '{"scaleGroupDict" : {"SP1": {"maxLevel" : 3}}}'}} + + if updates: + vnf_data.update(**updates) + + return vnf_data + + def get_dummy_grant_response(): return {'VDU1': {'checksum': {'algorithm': 'fake algo', 'hash': 'fake hash'}, @@ -756,3 +812,210 @@ def wsgi_app_v1(fake_auth_context=None): uuidsentinel.project_id, is_admin=True) api_v1 = InjectContext(ctxt, inner_app_v1) return api_v1 + + +VNFLCMOPOCC_RESPONSE = { + '_links': { + "self": { + "href": CONF.vnf_lcm.endpoint_url + '/vnflcm/v1/vnf_lcm_op_occs/' + 'f26f181d-7891-4720-b022-b074ec1733ef' + }, + "vnfInstance": { + "href": CONF.vnf_lcm.endpoint_url + '/vnflcm/v1/vnf_instances/' + 'f26f181d-7891-4720-b022-b074ec1733ef' + }, + "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' + }}, + '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', + 'operation': 'MODIFY_INFO', + 'isAutomaticInvocation': False, + 'operationParams': '{"is_reverse": False, "is_auto": False}', + 'error': { + 'status': 500, + 'detail': "name 'con' is not defined", + 'title': "ERROR" + }, + 'id': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'isCancelPending': False, + 'resourceChanges': { + 'affectedVnfcs': [{ + 'id': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'vduId': 'VDU1', + 'changeType': 'ADDED', + 'computeResource': { + 'vimConnectionId': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'resourceId': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'vimLevelResourceType': "OS::Nova::Server", + }, + 'affectedVnfcCpIds': [], + 'addedStorageResourceIds': [], + 'removedStorageResourceIds': [] + }], + 'affectedVirtualLinks': [{ + 'id': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'vnfVirtualLinkDescId': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'changeType': 'ADDED', + 'networkResource': { + 'vimConnectionId': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'resourceId': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'vimLevelResourceType': 'COMPUTE' + } + }], + 'affectedVirtualStorages': [{ + 'id': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'virtualStorageDescId': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'changeType': 'ADDED', + 'storageResource': { + 'vimConnectionId': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'resourceId': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'vimLevelResourceType': 'COMPUTE' + } + }] + }, + 'changedInfo': { + 'vimConnectionInfo': [], + 'vimConnectionInfoDeleteIds': [], + 'vnfPkgId': None, + 'vnfInstanceName': 'fake_name', + 'vnfInstanceDescription': "fake_vnf_instance_description", + 'vnfdId': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'vnfProvider': 'fake_vnf_provider', + 'vnfProductName': 'fake_vnf_product_name', + 'vnfSoftwareVersion': 'fake_vnf_software_version', + 'vnfdVersion': 'fake_vnfd_version' + } +} + +VNFLCMOPOCC_INDEX_RESPONSE = [VNFLCMOPOCC_RESPONSE] + + +def index_response(remove_attrs=None, vnf_lcm_op_occs_updates=None): + # Returns VNFLCMOPOCC_RESPONSE + # parameter remove_attrs is a list of attribute names + # to be removed before returning the response + if not remove_attrs: + return VNFLCMOPOCC_INDEX_RESPONSE + vnf_lcm_op_occs = deepcopy(VNFLCMOPOCC_RESPONSE) + for attr in remove_attrs: + vnf_lcm_op_occs.pop(attr, None) + if vnf_lcm_op_occs_updates: + vnf_lcm_op_occs.update(vnf_lcm_op_occs_updates) + return [vnf_lcm_op_occs] + + +def fake_vnf_lcm_op_occs(): + error = {"status": 500, "detail": "name 'con' is not defined", + "title": "ERROR"} + error_obj = objects.ProblemDetails(**error) + + compute_resource = { + "vim_connection_id": + "f26f181d-7891-4720-b022-b074ec1733ef", + "resource_id": + "f26f181d-7891-4720-b022-b074ec1733ef", + "vim_level_resource_type": "OS::Nova::Server"} + compute_resource_obj = objects.ResourceHandle(**compute_resource) + affected_vnfcs = { + "id": "f26f181d-7891-4720-b022-b074ec1733ef", + "vdu_id": "VDU1", + "change_type": "ADDED", + "compute_resource": compute_resource_obj, + "affected_vnfc_cp_ids": [], + "added_storage_resource_ids": [], + "removed_storage_sesource_ids": [] + } + affected_vnfcs_obj = objects.AffectedVnfc(**affected_vnfcs) + + network_resource = { + "vim_connection_id": + "f26f181d-7891-4720-b022-b074ec1733ef", + "resource_id": + "f26f181d-7891-4720-b022-b074ec1733ef", + "vim_level_resource_type": "COMPUTE" + } + network_resource_obj = \ + objects.ResourceHandle(**network_resource) + affected_virtual_links = { + "id": "f26f181d-7891-4720-b022-b074ec1733ef", + "vnf_virtual_link_desc_id": + "f26f181d-7891-4720-b022-b074ec1733ef", + "change_type": "ADDED", + "network_resource": network_resource_obj, + } + affected_virtual_links_obj = \ + objects.AffectedVirtualLink(**affected_virtual_links) + + storage_resource = { + "vim_connection_id": + "f26f181d-7891-4720-b022-b074ec1733ef", + "resource_id": + "f26f181d-7891-4720-b022-b074ec1733ef", + "vim_level_resource_type": "COMPUTE"} + storage_resource_obj = \ + objects.ResourceHandle(**storage_resource) + affected_virtual_storages = { + "id": "f26f181d-7891-4720-b022-b074ec1733ef", + "virtual_storage_desc_id": + "f26f181d-7891-4720-b022-b074ec1733ef", + "change_type": "ADDED", + "storage_resource": storage_resource_obj, + } + affected_virtual_storages_obj = \ + objects.AffectedVirtualStorage(**affected_virtual_storages) + + resource_changes = { + "affected_vnfcs": [affected_vnfcs_obj], + "affected_virtual_links": [affected_virtual_links_obj], + "affected_virtual_storages": [affected_virtual_storages_obj] + } + resource_changes_obj = objects.ResourceChanges(**resource_changes) + + changed_info = { + "vnf_instance_name": "fake_name", + "vnf_instance_description": + "fake_vnf_instance_description", + "metadata": {}, + "vnfd_id": "f26f181d-7891-4720-b022-b074ec1733ef", + "vnf_provider": "fake_vnf_provider", + "vnf_product_name": "fake_vnf_product_name", + "vnf_software_version": "fake_vnf_software_version", + "vnfd_version": "fake_vnfd_version" + } + changed_info_obj = objects.VnfInfoModifications(**changed_info) + + vnf_lcm_op_occs = { + 'id': constants.UUID, + 'operation_state': 'COMPLETED', + 'state_entered_time': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.UTC), + 'start_time': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.UTC), + 'vnf_instance_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 + } + + return vnf_lcm_op_occs + + +def return_vnf_lcm_opoccs_obj(): + 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 6b890c5..87fb7f0 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -14,11 +14,15 @@ # under the License. from unittest import mock -import ddt +import codecs +import os +import ddt +import json from oslo_serialization import jsonutils from six.moves import http_client import urllib +import webob from webob import exc from tacker.api.vnflcm.v1 import controller @@ -27,6 +31,7 @@ from tacker.conductor.conductorrpc.vnf_lcm_rpc import VNFLcmRPCAPI from tacker import context import tacker.db.vnfm.vnfm_db from tacker.extensions import nfvo +from tacker.extensions import vnfm from tacker.manager import TackerManager from tacker import objects from tacker.objects import fields @@ -40,6 +45,13 @@ from tacker.tests import uuidsentinel from tacker.vnfm import vim_client +def _get_template(name): + filename = os.path.abspath(os.path.join(os.path.dirname(__file__), + '../../etc/samples/' + str(name))) + f = codecs.open(filename, encoding='utf-8', errors='strict') + return f.read() + + class FakeVNFMPlugin(mock.Mock): def __init__(self): @@ -59,6 +71,100 @@ class FakeVNFMPlugin(mock.Mock): self.cp32_id = '3d1bd2a2-bf0e-44d1-87af-a2c6b2cad3ed' self.cp32_update_id = '064c0d99-5a61-4711-9597-2a44dc5da14b' + def get_vnfd(self, *args, **kwargs): + if 'VNF1' in args: + return {'id': self.vnf1_vnfd_id, + 'name': 'VNF1', + 'attributes': {'vnfd': _get_template( + 'test-nsd-vnfd1.yaml')}} + elif 'VNF2' in args: + return {'id': self.vnf3_vnfd_id, + 'name': 'VNF2', + 'attributes': {'vnfd': _get_template( + 'test-nsd-vnfd2.yaml')}} + + def get_vnfds(self, *args, **kwargs): + if {'name': ['VNF1']} in args: + return [{'id': self.vnf1_vnfd_id}] + elif {'name': ['VNF3']} in args: + return [{'id': self.vnf3_vnfd_id}] + else: + return [] + + def get_vnfs(self, *args, **kwargs): + if {'vnfd_id': [self.vnf1_vnfd_id]} in args: + return [{'id': self.vnf1_vnf_id}] + elif {'vnfd_id': [self.vnf3_vnfd_id]} in args: + return [{'id': self.vnf3_vnf_id}] + else: + return None + + def get_vnf(self, *args, **kwargs): + if self.vnf1_vnf_id in args: + return self.get_dummy_vnf_error() + elif self.vnf3_vnf_id in args: + return self.get_dummy_vnf_not_error() + else: + return self.get_dummy_vnf_active() + + def get_vnf_resources(self, *args, **kwargs): + if self.vnf1_vnf_id in args: + return self.get_dummy_vnf1_details() + elif self.vnf1_update_vnf_id in args: + return self.get_dummy_vnf1_update_details() + elif self.vnf3_vnf_id in args: + return self.get_dummy_vnf3_details() + elif self.vnf3_update_vnf_id in args: + return self.get_dummy_vnf3_update_details() + + def get_dummy_vnf1_details(self): + return [{'name': 'CP11', 'id': self.cp11_id}, + {'name': 'CP12', 'id': self.cp12_id}] + + def get_dummy_vnf1_update_details(self): + return [{'name': 'CP11', 'id': self.cp11_update_id}, + {'name': 'CP12', 'id': self.cp12_update_id}] + + def get_dummy_vnf3_details(self): + return [{'name': 'CP32', 'id': self.cp32_id}] + + def get_dummy_vnf3_update_details(self): + return [{'name': 'CP32', 'id': self.cp32_update_id}] + + def get_dummy_vnf_active(self): + return {'tenant_id': uuidsentinel.tenant_id, + 'name': "fake_name", + 'vnfd_id': uuidsentinel.vnfd_id, + 'vnf_instance_id': uuidsentinel.instance_id, + 'mgmt_ip_address': "fake_mgmt_ip_address", + 'status': 'ACTIVE', + 'description': 'fake_description', + 'placement_attr': 'fake_placement_attr', + 'vim_id': 'uuidsentinel.vim_id', + 'error_reason': 'fake_error_reason', + 'attributes': { + "scale_group": '{"scaleGroupDict":' + + '{"SP1": {"maxLevel" : 3}}}'}} + + def get_dummy_vnf_error(self): + return {'tenant_id': uuidsentinel.tenant_id, + 'name': "fake_name", + 'vnfd_id': uuidsentinel.vnfd_id, + 'vnf_instance_id': uuidsentinel.instance_id, + 'mgmt_ip_address': "fake_mgmt_ip_address", + 'status': 'ERROR', + 'description': 'fake_description', + 'placement_attr': 'fake_placement_attr', + 'vim_id': 'uuidsentinel.vim_id', + 'error_reason': 'fake_error_reason', + 'attributes': { + "scale_group": '{"scaleGroupDict":' + + '{"SP1": {"maxLevel" : 3}}}'}} + + def get_dummy_vnf_not_error(self): + msg = _('VNF %(vnf_id)s could not be found') + raise vnfm.VNFNotFound(explanation=msg) + @ddt.ddt class TestController(base.TestCase): @@ -104,6 +210,27 @@ class TestController(base.TestCase): return vnf_dict + def _make_problem_detail( + self, + detail, + status, + title=None, + type=None, + instance=None): + res = webob.Response(content_type='application/problem+json') + problemDetails = {} + if type: + problemDetails['type'] = type + if title: + problemDetails['title'] = title + problemDetails['detail'] = detail + problemDetails['status'] = status + if instance: + problemDetails['instance'] = instance + res.text = json.dumps(problemDetails) + res.status_int = status + return res + @mock.patch.object(objects.VnfInstance, 'save') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.vnf_package.VnfPackage, 'get_by_id') @@ -1631,3 +1758,30 @@ class TestController(base.TestCase): '/vnflcm/v1/vnf_instances?' + query) self.assertRaises(exceptions.ValidationError, self.controller.index, req) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(objects.VnfLcmOpOcc, "get_by_id") + def test_show_lcm_op_occs(self, mock_get_by_id, + mock_get_service_plugins): + req = fake_request.HTTPRequest.blank( + '/vnf_lcm_op_occs/%s' % constants.UUID) + mock_get_by_id.return_value = fakes.return_vnf_lcm_opoccs_obj() + expected_result = fakes.VNFLCMOPOCC_RESPONSE + res_dict = self.controller.show_lcm_op_occs(req, constants.UUID) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(objects.VnfLcmOpOcc, "get_by_id") + def test_show_lcm_op_occs_not_found(self, mock_get_by_id, + mock_get_service_plugins): + req = fake_request.HTTPRequest.blank( + '/vnfpkgm/v1/vnf_packages/%s' % constants.UUID) + mock_get_by_id.side_effect = exceptions.NotFound() + + req.headers['Content-Type'] = 'application/json' + req.method = 'GET' + resp = req.get_response(self.app) + + self.assertEqual(http_client.NOT_FOUND, resp.status_code)