commit 7e418376de2b2c730fc28cee2ba9517ccb8908a6 Author: Aldinson Esto Date: Wed Aug 19 19:49:17 2020 +0900 Support LCM notifications for VNF based on ETSI Supported for Flow of sending notifications as defined in ETSI SOL003. Notify LCM status to the cannback URL registered in subscription Support for transmission processing from each LCM 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: Idff7a98490ee87bb034d346cfda943bf03620b57 (cherry picked from commit f8f58e8cec5fb3f99864f540b130fa426c2a4c5f) diff --git a/api-ref/source/v1/parameters_vnflcm.yaml b/api-ref/source/v1/parameters_vnflcm.yaml index 806c943..3258f0a 100644 --- a/api-ref/source/v1/parameters_vnflcm.yaml +++ b/api-ref/source/v1/parameters_vnflcm.yaml @@ -14,6 +14,149 @@ vnf_instance_id: type: string # variables in body +added_storage_resource_ids: + description: | + References to VirtualStorage resources that + have been added. + Each value refers to a + VirtualStorageResourceInfo item in the + VnfInstance that was added to the VNFC. + It shall be provided if at least one storage + resource was added to the VNFC. + in: body + required: false + type: array +affected_virtual_links: + description: | + Information about VL instances that were affected during + the lifecycle operation. + in: body + required: false + type: array +affected_virtual_links_change_type: + description: | + Signals the type of change. + Permitted values: + + ADDED + + REMOVED + + MODIFIED + + TEMPORARY + + LINK_PORT_ADDED + + LINK_PORT_REMOVED + + For a temporary resource, an + AffectedVirtualLink structure exists as long as + the temporary resource exists. + in: body + required: true + type: string +affected_virtual_links_id: + description: | + Identifier of the virtual link instance, identifying + the applicable "vnfVirtualLinkResourceInfo" + entry in the "VnfInstance" data type. + in: body + required: true + type: string +affected_virtual_storages: + description: | + Information about virtualised storage instances that were + affected during the lifecycle operation. + in: body + required: false + type: array +affected_virtual_storages_change_type: + description: | + Signals the type of change. + Permitted values: + + ADDED + + REMOVED + + MODIFIED + + TEMPORARY + + For a temporary resource, an + AffectedVirtualStorage structure exists as long + as the temporary resource exists. + in: body + required: true + type: string +affected_virtual_storages_id: + description: | + Identifier of the storage instance, identifying the + applicable "virtualStorageResourceInfo" entry + in the "VnfInstance" data type. + in: body + required: true + type: string +affected_virtual_storages_virtual_storage_desc_id: + description: | + Identifier of the related VirtualStorage + descriptor in the VNFD. + in: body + required: true + type: string +affected_vnfc_cp_ids: + description: | + Identifiers of CP(s) of the VNFC instance that + were affected by the change. + Shall be present for those affected CPs of the + VNFC instance that are associated to an + external CP of the VNF instance. + May be present for further affected CPs of the + VNFC instance. + in: body + required: false + type: array +affected_vnfcs: + description: | + Information about VNFC instances that were affected + during the lifecycle operation. + in: body + required: false + type: array +affected_vnfcs_change_type: + description: | + Signals the type of change + Permitted values: + + ADDED + + REMOVED + + MODIFIED + + TEMPORARY + + For a temporary resource, an AffectedVnfc + structure exists as long as the temporary + resource exists. + in: body + required: true + type: string +affected_vnfcs_id: + description: | + Identifier of the Vnfc instance, identifying the + applicable "vnfcResourceInfo" entry in the + "VnfInstance" data type. + in: body + required: true + type: string +affected_vnfcs_vdu_id: + description: | + Identifier of the related VDU in the VNFD. + in: body + required: true + type: string authentication: description: | Authentication parameters to configure the use of @@ -118,6 +261,83 @@ cause: in: body required: false type: string +changed_info: + description: | + Information about the changed VNF instance information, + including VNF configurable properties, if applicable. + in: body + required: false + type: object +changed_info_metadata: + description: | + If present, this attribute signals modifications of the + "metadata" attribute in "VnfInstance". + in: body + required: false + type: string +changed_info_vim_connection_info: + description: | + If present, this attribute signals modifications of certain + entries in the "vimConnectionInfo" attribute array in "VnfInstance". + in: body + required: false + type: array +changed_info_vnf_instance_description: + description: | + If present, this attribute signals modifications of the + "vnfInstanceDescription" attribute in "VnfInstance". + in: body + required: false + type: string +changed_info_vnf_instance_name: + description: | + If present, this attribute signals modifications of the + "vnfInstanceName" attribute in "VnfInstance". + in: body + required: false + type: string +changed_info_vnf_pkg_id: + description: | + If present, this attribute signals modifications of the + "vnfPkgId" attribute in "VnfInstance". + in: body + required: false + type: string +changed_info_vnf_product_name: + description: | + If present, this attribute signals modifications of the + "vnfProductName" attribute in "VnfInstance". + in: body + required: false + type: string +changed_info_vnf_provider: + description: | + If present, this attribute signals modifications of the + "vnfProvider" attribute in "VnfInstance". + in: body + required: false + type: string +changed_info_vnf_sotware_version: + description: | + If present, this attribute signals modifications of the + "vnfSoftwareVersion" attribute in "VnfInstance". + in: body + required: false + type: string +changed_info_vnfd_id: + description: | + If present, this attribute signals modifications of the + "vnfdId" attribute in "VnfInstance". + in: body + required: false + type: string +changed_info_vnfd_version: + description: | + If present, this attribute signals modifications of the + "vnfdVersion" attribute in "VnfInstance". + in: body + required: false + type: string cp_config: description: | List of instance data that need to be configured on the CP instances @@ -147,6 +367,41 @@ cpd_id: in: body required: true type: string +error: + description: | + If "operationState" is "FAILED_TEMP" or "FAILED" or + "operationState" is "PROCESSING" or "ROLLING_BACK" + and previous value of "operationState" was + "FAILED_TEMP", this attribute shall be present and + contain error information, unless it has been requested to + be excluded via an attribute selector. + in: body + required: false + type: object +error_detail: + description: | + A human-readable explanation specific to this occurrence + of the problem. + in: body + required: true + type: string +error_status: + description: | + The HTTP status code for this occurrence of the problem. + in: body + required: true + type: string +error_title: + description: | + A URI reference according to IETF RFC 3986 that + identifies the problem type. It is encouraged that the URI + provides human-readable documentation for the problem + (e.g. using HTML) when dereferenced. When this + member is not present, its value is assumed to be + "about:blank". + in: body + required: false + type: string ext_cp_info: description: | Information about the external CPs exposed by the VNF instance. @@ -449,6 +704,16 @@ ip_over_ethernet_cp_info: in: body required: false type: object +is_automatic_invocation: + description: | + Set to true if this VNF LCM operation occurrence has + been triggered by an automated procedure inside the + VNFM (i.e. ScaleVnf triggered by autoscale, + or HealVnf triggered by auto-heal). + Set to false otherwise. + in: body + required: true + type: boolean is_dynamic: description: | Indicates whether this set of addresses was assigned dynamically (true) @@ -495,6 +760,86 @@ mac_address_cp_info: in: body required: false type: string +notification_id: + description: | + Identifier of this notification. If a notification is sent + multiple times due to multiple subscriptions, the "id" + attribute of all these notifications shall have the same + value. + in: body + required: true + type: string +notification_operation: + description: | + The lifecycle management operation. + in: body + required: true + type: string +notification_operation_state: + description: | + The state of the VNF LCM operation occurrence. + in: body + required: true + type: string +notification_status: + description: | + Indicates whether this notification reports about the start + of a lifecycle operation or the result of a lifecycle + operation. + Permitted values: + + START: Informs about the start of the VNF LCM + operation occurrence. + + RESULT: Informs about the final or intermediate + result of the VNF LCM operation occurrence. + in: body + required: true + type: string +notification_subscription_id: + description: | + Identifier of the subscription that this notification relates to. + in: body + required: true + type: string +notification_time_tamp: + description: | + Date-time of the generation of the notification. + in: body + required: true + type: string +notification_type: + description: | + Discriminator for the different notification types. + in: body + required: true + type: string +notification_vnf_instance_id: + description: | + The identifier of the VNF instance affected. + in: body + required: true + type: string +notification_vnf_lcm_op_occ_id: + description: | + The identifier of the VNF lifecycle management + operation occurrence associated to the notification. + in: body + required: true + type: string +removed_storage_resource_ids: + description: | + References to VirtualStorage resources that + have been removed. + The value contains the identifier of a + VirtualStorageResourceInfo item that has been + removed from the VNFC, and might no longer + exist in the VnfInstance. + It shall be provided if at least one storage + resource was removed from the VNFC. + in: body + required: false + type: array resource_handle: description: | Reference to the resource realizing this VL. @@ -594,6 +939,30 @@ vim_connection_info_access_info: in: body required: false type: string +vim_connection_info_access_info_password: + description: | + The password to use for access. + in: body + required: true + type: string +vim_connection_info_access_info_region: + description: | + The OpenStack region to use for the VIM connection. + in: body + required: true + type: string +vim_connection_info_access_info_tenant: + description: | + The OpenStack tenant to use for the VIM connection. + in: body + required: true + type: string +vim_connection_info_access_info_username: + description: | + The username to use for access. + in: body + required: true + type: string vim_connection_info_id: description: | The identifier of the VIM Connection. This identifier is managed by diff --git a/api-ref/source/v1/samples/vnflcm/notification-request.json b/api-ref/source/v1/samples/vnflcm/notification-request.json new file mode 100644 index 0000000..24c1547 --- /dev/null +++ b/api-ref/source/v1/samples/vnflcm/notification-request.json @@ -0,0 +1,100 @@ +{ + "id": "fb864bd0-25f3-4180-b591-2910086c0013", + "notificationType": "VnfLcmOperationOccurrenceNotification", + "subscriptionId": "c25ae285-e245-4898-9df7-0527841efdc4", + "timeStamp": "2020-08-02T06:50:50.883373", + "notificationStatus": "RESULT", + "operationState": "COMPLETED", + "vnfInstanceId": "0b7b95a9-21d5-4ac4-80c8-9ae9f7323787", + "operation": "INSTANTIATE", + "isAutomaticInvocation": false, + "vnfLcmOpOccId": "1266b5b4-84d3-4c3a-acbf-aa515fada6b0", + "affectedVnfcs": [ + { + "id": "36e24439-829c-4803-a413-385cd658d544", + "vduId": "VDU1", + "changeType": "ADDED", + "computeResource": { + "vimConnectionId": null, + "resourceId": "e0510ba9-3a53-4fcf-9dcc-58dea5c048b0", + "vimLevelResourceType": "OS::Nova::Server" + }, + "metadata": { + "stackId": "f0faec36-721a-4245-8f50-4a19edec757a" + }, + "affectedVnfcCpIds": [ + "VDU1_CP0", + "VDU1_CP1" + ], + "addedStorageResourceIds": [] + }, + { + "id": "51f930d7-f8b9-4a7f-8b02-18c47ec31340", + "vduId": "VDU2", + "changeType": "ADDED", + "computeResource": { + "vimConnectionId": null, + "resourceId": "", + "vimLevelResourceType": null + }, + "metadata": {}, + "affectedVnfcCpIds": [ + "VDU2_CP0", + "VDU2_CP1" + ], + "addedStorageResourceIds": [] + }, + { + "id": "52e4253a-7c9e-4161-bb74-0642d5072271", + "vduId": "VDU3", + "changeType": "ADDED", + "computeResource": { + "vimConnectionId": null, + "resourceId": "", + "vimLevelResourceType": null + }, + "metadata": {}, + "affectedVnfcCpIds": [ + "VDU3_CP0", + "VDU3_CP1" + ], + "addedStorageResourceIds": [] + } + ], + "affectedVirtualLinks": [ + { + "id": "9836f7f2-5af4-4df5-a89f-933479448ef7", + "vnfVirtualLinkDescId": "internalNW1", + "changeType": "ADDED", + "networkResource": { + "vimConnectionId": null, + "resourceId": "400692e5-b2db-478e-acb1-b77a92635ec6", + "vimLevelResourceType": "OS::Neutron::Net" + }, + "metadata": {} + }, + { + "id": "8d6c4a5f-cbd1-4fd8-b240-867ed1d48385", + "vnfVirtualLinkDescId": "internalNW2", + "changeType": "ADDED", + "networkResource": { + "vimConnectionId": null, + "resourceId": "", + "vimLevelResourceType": null + }, + "metadata": {} + } + ], + "affectedVirtualStorages": [], + "_links": { + "vnfInstance": { + "href": "http://sample.com/vnflcm/v1/vnf_instances/0b7b95a9-21d5-4ac4-80c8-9ae9f7323787" + }, + "subscription": { + "href": "http://sample.com/vnflcm/v1/subscriptions/c25ae285-e245-4898-9df7-0527841efdc4" + }, + "vnfLcmOpOcc": { + "href": "http://sample.com/vnflcmv1/vnf_lcm_op_occs/1266b5b4-84d3-4c3a-acbf-aa515fada6b0" + } + } +} \ No newline at end of file diff --git a/api-ref/source/v1/vnflcm.inc b/api-ref/source/v1/vnflcm.inc index 55ae641..a7433d6 100644 --- a/api-ref/source/v1/vnflcm.inc +++ b/api-ref/source/v1/vnflcm.inc @@ -758,3 +758,100 @@ Response Example .. literalinclude:: samples/vnflcm/list-subscription-response.json :language: javascript + +Notification +================= + +.. rest_method:: POST URI is provided by the client when creating the subscription. + +The POST method delivers a notification from the API producer to an API consumer. The API consumer shall have +previously created an "Individual subscription" resource with a matching filter. + +Response Codes +-------------- + +.. rest_status_code:: success status.yaml + + - 204 + +.. rest_status_code:: error status.yaml + + - 401 + - 403 + +Request Parameters +------------------ + +.. rest_parameters:: parameters_vnflcm.yaml + + + - id: notification_id + - notificationType: notification_type + - subscriptionId: notification_subscription_id + - timeStamp: notification_time_tamp + - notificationStatus: notification_status + - operationState: notification_operation_state + - vnfInstanceId: notification_vnf_instance_id + - operation: notification_operation + - isAutomaticInvocation: is_automatic_invocation + - vnfLcmOpOccId: notification_vnf_lcm_op_occ_id + - 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 + - virtualLinkDescId: 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 + - error: error + - title: error_title + - status: error_status + - detail: error_detail + - _links: vnf_instance_links + +Response Example +---------------- + +.. literalinclude:: samples/vnflcm/notification-request.json + :language: javascript diff --git a/tacker/api/views/vnf_lcm.py b/tacker/api/views/vnf_lcm.py index 6649da6..5d454ed 100644 --- a/tacker/api/views/vnf_lcm.py +++ b/tacker/api/views/vnf_lcm.py @@ -158,10 +158,7 @@ class ViewBuilder(base.BaseViewBuilder): paging): # filter processing lcmsubscription = [] - - # last_flg is True if nextpage_opaque_marker is set last_flg = False - start_num = CONF.vnf_lcm.subscription_num * (paging - 1) # Subscription_data counter for comparing # subscription_data and start_num diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index 21472d4..730bbb9 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -15,18 +15,21 @@ from oslo_log import log as logging from oslo_serialization import jsonutils +from oslo_utils import encodeutils +from oslo_utils import timeutils from oslo_utils import uuidutils import ast +import functools import json import re import traceback import six from six.moves import http_client +from six.moves.urllib import parse import webob -from urllib import parse from tacker._i18n import _ from tacker.api.schemas import vnf_lcm @@ -37,10 +40,12 @@ from tacker.common import utils from tacker.conductor.conductorrpc import vnf_lcm_rpc import tacker.conf from tacker.extensions import nfvo +from tacker.extensions import vnfm from tacker import manager from tacker import objects from tacker.objects import fields 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 from tacker.vnfm import vim_client from tacker import wsgi @@ -63,7 +68,7 @@ def check_vnf_state(action, instantiation_state=None, task_state=(None,)): task_state = set(task_state) def outer(f): - @six.wraps(f) + @functools.wraps(f) def inner(self, context, vnf_instance, *args, **kw): if instantiation_state is not None and \ vnf_instance.instantiation_state not in \ @@ -85,6 +90,31 @@ def check_vnf_state(action, instantiation_state=None, task_state=(None,)): return outer +def check_vnf_status(action, status=None): + """Decorator to check vnf status are valid for particular action. + + If the vnf is in the wrong state, it will raise conflict exception. + """ + + if status is not None and not isinstance(status, set): + status = set(status) + + def outer(f): + @functools.wraps(f) + def inner(self, context, vnf_instance, vnf, *args, **kw): + if status is not None and \ + vnf['status'] not in \ + status: + raise exceptions.VnfConflictState( + attr='status', + uuid=vnf['id'], + state=vnf['status'], + action=action) + return f(self, context, vnf_instance, vnf, *args, **kw) + return inner + return outer + + class VnfLcmController(wsgi.Controller): notification_type_list = ['VnfLcmOperationOccurrenceNotification', @@ -117,6 +147,9 @@ class VnfLcmController(wsgi.Controller): def _get_vnf_instance_href(self, vnf_instance): return '/vnflcm/v1/vnf_instances/%s' % vnf_instance.id + def _get_vnf_lcm_op_occs_href(self, vnf_lcm_op_occs_id): + return '/vnflcm/v1/vnf_lcm_op_occs/%s' % vnf_lcm_op_occs_id + def _get_vnf_instance(self, context, id): # check if id is of type uuid format if not uuidutils.is_uuid_like(id): @@ -131,6 +164,24 @@ class VnfLcmController(wsgi.Controller): return vnf_instance + def _get_vnf(self, context, id): + # check if id is of type uuid format + if not uuidutils.is_uuid_like(id): + msg = _("Can not find requested vnf: %s") % id + raise webob.exc.HTTPNotFound(explanation=msg) + + try: + vnf = self._vnfm_plugin.get_vnf(context, id) + except vnfm.VNFNotFound: + msg = _("Can not find requested vnf: %s") % id + raise webob.exc.HTTPNotFound(explanation=msg) + except Exception as exc: + msg = _("Encountered error while fetching vnf: %s") % id + LOG.debug("{}: {}".format(msg, six.text_type(exc))) + raise webob.exc.HTTPInternalServerError(explanation=six. + text_type(exc)) + return vnf + def _validate_flavour_and_inst_level(self, context, req_body, vnf_instance): inst_levels = {} @@ -197,6 +248,65 @@ class VnfLcmController(wsgi.Controller): % vim_id raise webob.exc.HTTPBadRequest(explanation=msg) + def _notification_process(self, context, vnf_instance, + lcm_operation, request, is_auto=False): + vnf_lcm_op_occs_id = uuidutils.generate_uuid() + error_point = 0 + if lcm_operation == fields.LcmOccsOperationType.HEAL: + request_dict = { + 'vnfc_instance_id': request.vnfc_instance_id, + 'cause': request.cause + } + operation_params = str(request_dict) + else: + # lcm is instantiation by default + operation_params = str(request.additional_params) + try: + # call create lcm op occs here + LOG.debug('Create LCM OP OCCS') + vnf_lcm_op_occs = objects.VnfLcmOpOcc( + context=context, + id=vnf_lcm_op_occs_id, + operation_state=fields.LcmOccsOperationState.STARTING, + start_time=timeutils.utcnow(), + state_entered_time=timeutils.utcnow(), + vnf_instance_id=vnf_instance.id, + is_cancel_pending=is_auto, + operation=lcm_operation, + is_automatic_invocation=is_auto, + operation_params=operation_params, + error_point=error_point) + vnf_lcm_op_occs.create() + except Exception: + msg = _("Failed to create LCM occurrence") + raise webob.exc.HTTPInternalServerError(explanation=msg) + + vnf_lcm_url = self._get_vnf_lcm_op_occs_href(vnf_lcm_op_occs_id) + notification = { + 'notificationType': + fields.LcmOccsNotificationType.VNF_OP_OCC_NOTIFICATION, + 'notificationStatus': fields.LcmOccsNotificationStatus.START, + 'operationState': fields.LcmOccsOperationState.STARTING, + 'vnfInstanceId': vnf_instance.id, + 'operation': lcm_operation, + 'isAutomaticInvocation': is_auto, + 'vnfLcmOpOccId': vnf_lcm_op_occs_id, + '_links': { + 'vnfInstance': { + 'href': self._get_vnf_instance_href(vnf_instance)}, + 'vnfLcmOpOcc': { + 'href': vnf_lcm_url}}} + + # call send notification + try: + self.rpc_api.send_notification(context, notification) + except Exception as ex: + LOG.error( + "Encoutered problem sending notification {}".format( + encodeutils.exception_to_unicode(ex))) + + return vnf_lcm_op_occs_id + @wsgi.response(http_client.CREATED) @wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN)) @validation.schema(vnf_lcm.create) @@ -247,6 +357,18 @@ class VnfLcmController(wsgi.Controller): # add default vim to vim_connection_info setattr(vnf_instance, 'vim_connection_info', [vim_con_info]) + + # create notification data + notification = { + 'notificationType': + fields.LcmOccsNotificationType.VNF_ID_CREATION_NOTIFICATION, + 'vnfInstanceId': vnf_instance.id, + 'links': { + 'vnfInstance': { + 'href': self._get_vnf_instance_href(vnf_instance)}}} + + # call send nootification + self.rpc_api.send_notification(context, notification) vnf_instance.save() result = self._view_builder.create(vnf_instance) @@ -291,10 +413,21 @@ class VnfLcmController(wsgi.Controller): vnf_instance = self._get_vnf_instance(context, id) self._delete(context, vnf_instance) + notification = { + "notificationType": "VnfIdentifierDeletionNotification", + "vnfInstanceId": vnf_instance.id, + "links": { + "vnfInstance": + "href:{apiRoot}/vnflcm/v1/vnf_instances/{vnfInstanceId}"}} + # send notification + self.rpc_api.send_notification(context, notification) + @check_vnf_state(action="instantiate", instantiation_state=[fields.VnfInstanceState.NOT_INSTANTIATED], task_state=[None]) - def _instantiate(self, context, vnf_instance, request_body): + @check_vnf_status(action="instantiate", + status=[constants.INACTIVE]) + def _instantiate(self, context, vnf_instance, vnf, request_body): req_body = utils.convert_camelcase_to_snakecase(request_body) try: @@ -313,8 +446,13 @@ class VnfLcmController(wsgi.Controller): vnf_instance.task_state = fields.VnfInstanceTaskState.INSTANTIATING vnf_instance.save() - self.rpc_api.instantiate(context, vnf_instance, - instantiate_vnf_request) + # lcm op process + vnf_lcm_op_occs_id = \ + self._notification_process(context, vnf_instance, + fields.LcmOccsOperationType.INSTANTIATE, + instantiate_vnf_request) + self.rpc_api.instantiate(context, vnf_instance, vnf, + instantiate_vnf_request, vnf_lcm_op_occs_id) @wsgi.response(http_client.ACCEPTED) @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND, @@ -324,14 +462,15 @@ class VnfLcmController(wsgi.Controller): context = request.environ['tacker.context'] context.can(vnf_lcm_policies.VNFLCM % 'instantiate') + vnf = self._get_vnf(context, id) vnf_instance = self._get_vnf_instance(context, id) - self._instantiate(context, vnf_instance, body) + self._instantiate(context, vnf_instance, vnf, body) @check_vnf_state(action="terminate", instantiation_state=[fields.VnfInstanceState.INSTANTIATED], task_state=[None]) - def _terminate(self, context, vnf_instance, request_body): + def _terminate(self, context, vnf_instance, request_body, vnf): req_body = utils.convert_camelcase_to_snakecase(request_body) terminate_vnf_req = \ objects.TerminateVnfRequest.obj_from_primitive( @@ -339,7 +478,15 @@ class VnfLcmController(wsgi.Controller): vnf_instance.task_state = fields.VnfInstanceTaskState.TERMINATING vnf_instance.save() - self.rpc_api.terminate(context, vnf_instance, terminate_vnf_req) + + # lcm op process + vnf_lcm_op_occs_id = \ + self._notification_process(context, vnf_instance, + fields.LcmOccsOperationType.TERMINATE, + terminate_vnf_req) + + self.rpc_api.terminate(context, vnf_instance, vnf, + terminate_vnf_req, vnf_lcm_op_occs_id) @wsgi.response(http_client.ACCEPTED) @wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN, @@ -349,13 +496,16 @@ class VnfLcmController(wsgi.Controller): context = request.environ['tacker.context'] context.can(vnf_lcm_policies.VNFLCM % 'terminate') + vnf = self._get_vnf(context, id) vnf_instance = self._get_vnf_instance(context, id) - self._terminate(context, vnf_instance, body) + self._terminate(context, vnf_instance, body, vnf) @check_vnf_state(action="heal", instantiation_state=[fields.VnfInstanceState.INSTANTIATED], task_state=[None]) - def _heal(self, context, vnf_instance, request_body): + @check_vnf_status(action="heal", + status=[constants.ACTIVE]) + def _heal(self, context, vnf_instance, vnf_dict, request_body): req_body = utils.convert_camelcase_to_snakecase(request_body) heal_vnf_request = objects.HealVnfRequest(context=context, **req_body) inst_vnf_info = vnf_instance.instantiated_vnf_info @@ -374,7 +524,14 @@ class VnfLcmController(wsgi.Controller): vnf_instance.task_state = fields.VnfInstanceTaskState.HEALING vnf_instance.save() - self.rpc_api.heal(context, vnf_instance, heal_vnf_request) + # call notification process + vnf_lcm_op_occs_id = \ + self._notification_process(context, vnf_instance, + fields.LcmOccsOperationType.HEAL, + heal_vnf_request) + + self.rpc_api.heal(context, vnf_instance, vnf_dict, heal_vnf_request, + vnf_lcm_op_occs_id) @wsgi.response(http_client.ACCEPTED) @wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN, @@ -384,8 +541,9 @@ class VnfLcmController(wsgi.Controller): context = request.environ['tacker.context'] context.can(vnf_lcm_policies.VNFLCM % 'heal') + vnf = self._get_vnf(context, id) vnf_instance = self._get_vnf_instance(context, id) - self._heal(context, vnf_instance, body) + self._heal(context, vnf_instance, vnf, body) @wsgi.response(http_client.CREATED) @validation.schema(vnf_lcm.register_subscription) diff --git a/tacker/common/exceptions.py b/tacker/common/exceptions.py index c43bf98..29c945f 100644 --- a/tacker/common/exceptions.py +++ b/tacker/common/exceptions.py @@ -232,6 +232,11 @@ class VnfInstanceConflictState(Conflict): "%(action)s while the vnf instance is in this state.") +class VnfConflictState(Conflict): + message = _("Vnf %(uuid)s in %(attr)s %(state)s. Cannot " + "%(action)s while the vnf is in this state.") + + class FlavourNotFound(NotFound): message = _("No flavour with id '%(flavour_id)s'.") @@ -342,6 +347,10 @@ class LimitExceeded(TackerException): super(LimitExceeded, self).__init__(*args, **kwargs) +class NotificationProcessingError(TackerException): + message = _("Notification Processing Failed: %(error)s") + + class UserDataUpdateCreateFailed(TackerException): msg_fmt = _("User data for VNF package %(id)s cannot be updated " "or created after %(retries)d retries.") diff --git a/tacker/conductor/conductor_server.py b/tacker/conductor/conductor_server.py index f353eaa..e74bb7a 100644 --- a/tacker/conductor/conductor_server.py +++ b/tacker/conductor/conductor_server.py @@ -12,29 +12,38 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +import copy import datetime import functools import inspect +import json import os +import requests import shutil import sys +import time +import traceback + from glance_store import exceptions as store_exceptions from oslo_config import cfg from oslo_log import log as logging import oslo_messaging +from oslo_serialization import jsonutils from oslo_service import periodic_task from oslo_service import service from oslo_utils import encodeutils from oslo_utils import excutils from oslo_utils import timeutils +from oslo_utils import uuidutils +from sqlalchemy import exc as sqlexc from sqlalchemy.orm import exc as orm_exc import yaml from tacker.common import coordination from tacker.common import csar_utils from tacker.common import exceptions +from tacker.common import log from tacker.common import safe_utils from tacker.common import topics from tacker.common import utils @@ -50,6 +59,7 @@ from tacker.objects.vnf_package import VnfPackagesList from tacker.plugins.common import constants from tacker import service as tacker_service from tacker import version +from tacker.vnflcm import utils as vnflcm_utils from tacker.vnflcm import vnflcm_driver from tacker.vnfm import plugin @@ -115,7 +125,7 @@ def revert_upload_vnf_package(function): or isinstance(exp, exceptions.VNFPackageURLInvalid)): # Delete the csar file from the glance store. glance_store.delete_csar(context, vnf_package.id, - vnf_package.location_glance_store) + vnf_package.location_glance_store) csar_utils.delete_csar_data(vnf_package.id) @@ -407,6 +417,22 @@ class Conductor(manager.Manager): return file_path_and_data + @coordination.synchronized('{vnf_package[id]}') + def _update_package_usage_state(self, context, vnf_package): + """Update vnf package usage state to IN_USE/NOT_IN_USE + + If vnf package is not used by any of the vnf instances, it's usage + state should be set to NOT_IN_USE otherwise it should be set to + IN_USE. + """ + result = vnf_package.is_package_in_use(context) + if result: + vnf_package.usage_state = fields.PackageUsageStateType.IN_USE + else: + vnf_package.usage_state = fields.PackageUsageStateType.NOT_IN_USE + + vnf_package.save() + @periodic_task.periodic_task(spacing=CONF.vnf_package_delete_interval) def _run_cleanup_vnf_packages(self, context): """Delete orphan extracted csar zip and files from extracted path @@ -438,87 +464,399 @@ class Conductor(manager.Manager): {'zip': csar_path, 'folder': csar_zip_temp_path, 'uuid': vnf_pack.id}) - @coordination.synchronized('{vnf_instance[id]}') - def instantiate(self, context, vnf_instance, instantiate_vnf): - # Check if vnf is already instantiated. - vnf_instance = objects.VnfInstance.get_by_id(context, - vnf_instance.id) - if vnf_instance.instantiation_state == \ - fields.VnfInstanceState.INSTANTIATED: - LOG.error("Vnf instance %(id)s is already in %(state)s state.", - {"id": vnf_instance.id, - "state": vnf_instance.instantiation_state}) - return - - self.vnflcm_driver.instantiate_vnf(context, vnf_instance, - instantiate_vnf) - - vnf_package_vnfd = objects.VnfPackageVnfd.get_by_id(context, - vnf_instance.vnfd_id) - vnf_package = objects.VnfPackage.get_by_id(context, - vnf_package_vnfd.package_uuid, expected_attrs=['vnfd']) + @log.log + def _get_vnf_notify(self, context, id): try: - self._update_package_usage_state(context, vnf_package) - except Exception: - LOG.error("Failed to update usage_state of vnf package %s", - vnf_package.id) + vnf_notif = objects.VnfLcmOpOcc.get_by_id(context, id) + except exceptions.VnfInstanceNotFound: + error_msg = _("Can not find requested vnf instance: %s") % id + raise exceptions.NotificationProcessingError(error=error_msg) + except (sqlexc.SQLAlchemyError, Exception): + error_msg = _("Can not find requested vnf instance: %s") % id + raise exceptions.NotificationProcessingError(error=error_msg) - @coordination.synchronized('{vnf_package[id]}') - def _update_package_usage_state(self, context, vnf_package): - """Update vnf package usage state to IN_USE/NOT_IN_USE + return vnf_notif - If vnf package is not used by any of the vnf instances, it's usage - state should be set to NOT_IN_USE otherwise it should be set to - IN_USE. - """ - result = vnf_package.is_package_in_use(context) - if result: - vnf_package.usage_state = fields.PackageUsageStateType.IN_USE + def _send_lcm_op_occ_notification( + self, + context, + vnf_lcm_op_occs_id, + old_vnf_instance, + vnf_instance, + request_obj, + **kwargs): + + operation = kwargs.get('operation', + fields.LcmOccsOperationType.INSTANTIATE) + operation_state = kwargs.get('operation_state', + fields.LcmOccsOperationState.PROCESSING) + evacuate_end_list = kwargs.get('evacuate_end_list', None) + is_automatic_invocation = \ + kwargs.get('is_automatic_invocation', False) + error = kwargs.get('error', None) + + if old_vnf_instance: + vnf_instance_id = old_vnf_instance.id else: - vnf_package.usage_state = fields.PackageUsageStateType.NOT_IN_USE + vnf_instance_id = vnf_instance.id - vnf_package.save() + try: + LOG.debug("Update vnf lcm %s %s", + (vnf_lcm_op_occs_id, + operation_state)) + vnf_notif = self._get_vnf_notify(context, vnf_lcm_op_occs_id) + vnf_notif.operation_state = operation_state + if operation_state == fields.LcmOccsOperationState.FAILED_TEMP: + error_details = objects.ProblemDetails( + context=context, + status=500, + details=error + ) + vnf_notif.error = error_details + vnf_notif.save() + except Exception: + error_msg = ( + "Failed to update operation state of vnf instance %s" % + vnf_instance_id) + LOG.error(error_msg) + raise exceptions.NotificationProcessingError(error=error_msg) + + # send notification + try: + notification_data = { + 'notificationType': + fields.LcmOccsNotificationType.VNF_OP_OCC_NOTIFICATION, + 'notificationStatus': fields.LcmOccsNotificationStatus.START, + 'operationState': operation_state, + 'vnfInstanceId': vnf_instance_id, + 'operation': operation, + 'isAutomaticInvocation': is_automatic_invocation, + 'vnfLcmOpOccId': vnf_lcm_op_occs_id, + '_links': { + 'vnfInstance': { + 'href': + '/vnflcm/v1/vnf_instances/%s' + % vnf_instance_id}, + 'vnfLcmOpOcc': { + 'href': + '/vnflcmv1/vnf_lcm_op_occs/%s' + % vnf_lcm_op_occs_id}}} + + if(operation_state == fields.LcmOccsOperationState.COMPLETED or + operation_state == fields.LcmOccsOperationState.FAILED_TEMP): + affected_resources = vnflcm_utils._get_affected_resources( + old_vnf_instance=old_vnf_instance, + new_vnf_instance=vnf_instance, + extra_list=evacuate_end_list) + affected_resources_snake_case = \ + utils.convert_camelcase_to_snakecase(affected_resources) + resource_change_obj = \ + jsonutils.dumps(affected_resources_snake_case) + changed_resource = objects.ResourceChanges.obj_from_primitive( + resource_change_obj, context) + vnf_notif.resource_changes = changed_resource + vnf_notif.save() + notification_data['affectedVnfcs'] = \ + affected_resources.get('affectedVnfcs', []) + notification_data['affectedVirtualLinks'] = \ + affected_resources.get('affectedVirtualLinks', []) + notification_data['affectedVirtualStorages'] = \ + affected_resources.get('affectedVirtualStorages', []) + notification_data['notificationStatus'] = \ + fields.LcmOccsNotificationStatus.RESULT + + if operation_state == fields.LcmOccsOperationState.FAILED_TEMP: + notification_data['error'] = error + + # send notification + self.send_notification(context, notification_data) + except Exception as ex: + LOG.error( + "Failed to send notification {}. Details: {}".format( + vnf_lcm_op_occs_id, str(ex))) + + def send_notification(self, context, notification): + try: + LOG.debug("send_notification start notification[%s]" + % notification) + if (notification.get('notificationType') == + 'VnfLcmOperationOccurrenceNotification'): + vnf_lcm_subscriptions = \ + objects.LccnSubscriptionRequest.vnf_lcm_subscriptions_get( + context, + operation_type=notification.get('operation'), + notification_type=notification.get('notificationType') + ) + else: + vnf_lcm_subscriptions = \ + objects.LccnSubscriptionRequest.vnf_lcm_subscriptions_get( + context, + notification_type=notification.get('notificationType') + ) + if not vnf_lcm_subscriptions: + LOG.warn( + "vnf_lcm_subscription not found id[%s]" % + notification.get('vnfInstanceId')) + return -1 + + notification['id'] = uuidutils.generate_uuid() + + # Notification shipping + for line in vnf_lcm_subscriptions: + notification['subscriptionId'] = line.id.decode() + if (notification.get('notificationType') == + 'VnfLcmOperationOccurrenceNotification'): + notification['_links'] = {} + notification['_links']['subscription'] = {} + notification['_links']['subscription']['href'] = \ + CONF.vnf_lcm.endpoint_url + \ + "/vnflcm/v1/subscriptions/" + line.id.decode() + else: + notification['links'] = {} + notification['links']['subscription'] = {} + notification['links']['subscription']['href'] = \ + CONF.vnf_lcm.endpoint_url + \ + "/vnflcm/v1/subscriptions/" + line.id.decode() + notification['timeStamp'] = datetime.datetime.utcnow( + ).isoformat() + try: + for num in range(CONF.vnf_lcm.retry_num): + LOG.warn("send notify[%s]" % json.dumps(notification)) + response = requests.post( + line.callback_uri.decode(), + data=json.dumps(notification)) + if response.status_code == 204: + LOG.info( + "send success notify[%s]" % + json.dumps(notification)) + break + else: + LOG.warning( + "Notification failed id[%s] status[%s] \ + callback_uri[%s]" % + (notification['id'], + response.status_code, + line.callback_uri.decode())) + LOG.debug( + "retry_wait %s" % + CONF.vnf_lcm.retry_wait) + time.sleep(CONF.vnf_lcm.retry_wait) + if num == CONF.vnf_lcm.retry_num: + LOG.warn("Number of retries \ + exceeded retry count") + + continue + except Exception as e: + LOG.warn("send error[%s]" % str(e)) + LOG.warn(traceback.format_exc()) + continue + + except Exception as e: + LOG.warn("Internal Sever Error[%s]" % str(e)) + LOG.warn(traceback.format_exc()) + return -2 + return 0 @coordination.synchronized('{vnf_instance[id]}') - def terminate(self, context, vnf_instance, terminate_vnf_req): - # Check if vnf is in instantiated state. - vnf_instance = objects.VnfInstance.get_by_id(context, - vnf_instance.id) - if vnf_instance.instantiation_state == \ - fields.VnfInstanceState.NOT_INSTANTIATED: - LOG.error("Terminate action cannot be performed on vnf %(id)s " - "which is in %(state)s state.", - {"id": vnf_instance.id, - "state": vnf_instance.instantiation_state}) - return - - self.vnflcm_driver.terminate_vnf(context, vnf_instance, - terminate_vnf_req) - - vnf_package_vnfd = objects.VnfPackageVnfd.get_by_id(context, - vnf_instance.vnfd_id) - vnf_package = objects.VnfPackage.get_by_id(context, - vnf_package_vnfd.package_uuid, expected_attrs=['vnfd']) + def instantiate( + self, + context, + vnf_instance, + instantiate_vnf, + vnf_lcm_op_occs_id): + try: - self._update_package_usage_state(context, vnf_package) - except Exception: - LOG.error("Failed to update usage_state of vnf package %s", - vnf_package.id) + # Update vnf_lcm_op_occs table and send notification "PROCESSING" + self._send_lcm_op_occ_notification( + context=context, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, + old_vnf_instance=None, + vnf_instance=vnf_instance, + request_obj=instantiate_vnf + ) + + # Check if vnf is already instantiated. + vnf_instance = objects.VnfInstance.get_by_id(context, + vnf_instance.id) + if vnf_instance.instantiation_state == \ + fields.VnfInstanceState.INSTANTIATED: + LOG.error("Vnf instance %(id)s is already in %(state)s state.", + {"id": vnf_instance.id, + "state": vnf_instance.instantiation_state}) + return + + self.vnflcm_driver.instantiate_vnf(context, vnf_instance, + instantiate_vnf) + + vnf_package_vnfd = objects.VnfPackageVnfd.get_by_id( + context, vnf_instance.vnfd_id) + vnf_package = objects.VnfPackage.get_by_id( + context, vnf_package_vnfd.package_uuid, + expected_attrs=['vnfd']) + try: + self._update_package_usage_state(context, vnf_package) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Failed to update usage_state of vnf package %s", + vnf_package.id) + + # Update vnf_lcm_op_occs table and send notification "COMPLETED" + self._send_lcm_op_occ_notification( + context=context, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, + old_vnf_instance=None, + vnf_instance=vnf_instance, + request_obj=instantiate_vnf, + operation_state=fields.LcmOccsOperationState.COMPLETED + ) + + except Exception as ex: + # Update vnf_lcm_op_occs table and send notification "FAILED_TEMP" + self._send_lcm_op_occ_notification( + context=context, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, + old_vnf_instance=None, + vnf_instance=vnf_instance, + request_obj=instantiate_vnf, + operation_state=fields.LcmOccsOperationState.FAILED_TEMP, + error=str(ex) + ) @coordination.synchronized('{vnf_instance[id]}') - def heal(self, context, vnf_instance, heal_vnf_request): - # Check if vnf is in instantiated state. - vnf_instance = objects.VnfInstance.get_by_id(context, - vnf_instance.id) - if vnf_instance.instantiation_state == \ - fields.VnfInstanceState.NOT_INSTANTIATED: - LOG.error("Heal action cannot be performed on vnf %(id)s " - "which is in %(state)s state.", - {"id": vnf_instance.id, - "state": vnf_instance.instantiation_state}) - return - - self.vnflcm_driver.heal_vnf(context, vnf_instance, heal_vnf_request) + def terminate(self, context, vnf_lcm_op_occs_id, + vnf_instance, terminate_vnf_req, vnf_dict): + try: + # Check if vnf is in instantiated state. + vnf_instance = objects.VnfInstance.get_by_id(context, + vnf_instance.id) + if vnf_instance.instantiation_state == \ + fields.VnfInstanceState.NOT_INSTANTIATED: + LOG.error("Terminate action cannot be performed on vnf %(id)s " + "which is in %(state)s state.", + {"id": vnf_instance.id, + "state": vnf_instance.instantiation_state}) + return + + old_vnf_instance = copy.deepcopy(vnf_instance) + + # Update vnf_lcm_op_occs table and send notification "PROCESSING" + self._send_lcm_op_occ_notification( + context=context, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, + old_vnf_instance=old_vnf_instance, + vnf_instance=None, + request_obj=terminate_vnf_req, + operation=fields.LcmOccsOperationType.TERMINATE + ) + + self.vnflcm_driver.terminate_vnf(context, vnf_instance, + terminate_vnf_req, + vnf_lcm_op_occs_id) + + vnf_package_vnfd = \ + objects.VnfPackageVnfd.get_by_id(context, vnf_instance.vnfd_id) + vnf_package = \ + objects.VnfPackage.get_by_id(context, + vnf_package_vnfd.package_uuid, + expected_attrs=['vnfd']) + try: + self._update_package_usage_state(context, vnf_package) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Failed to update usage_state of vnf package %s", + vnf_package.id) + + # Update vnf_lcm_op_occs table and send notification "COMPLETED" + self._send_lcm_op_occ_notification( + context=context, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, + old_vnf_instance=old_vnf_instance, + vnf_instance=None, + request_obj=terminate_vnf_req, + operation=fields.LcmOccsOperationType.TERMINATE, + operation_state=fields.LcmOccsOperationState.COMPLETED + ) + + except Exception as exc: + # Update vnf_lcm_op_occs table and send notification "FAILED_TEMP" + self._send_lcm_op_occ_notification( + context=context, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, + old_vnf_instance=old_vnf_instance, + vnf_instance=None, + request_obj=terminate_vnf_req, + operation=fields.LcmOccsOperationType.TERMINATE, + operation_state=fields.LcmOccsOperationState.FAILED_TEMP, + error=str(exc) + ) + + @coordination.synchronized('{vnf_instance[id]}') + def heal(self, + context, + vnf_instance, + vnf_dict, + heal_vnf_request, + vnf_lcm_op_occs_id): + + try: + evacuate_end_list = [] + + # Check if vnf is in instantiated state. + vnf_instance = objects.VnfInstance.get_by_id(context, + vnf_instance.id) + if vnf_instance.instantiation_state == \ + fields.VnfInstanceState.NOT_INSTANTIATED: + LOG.error("Heal action cannot be performed on vnf %(id)s " + "which is in %(state)s state.", + {"id": vnf_instance.id, + "state": vnf_instance.instantiation_state}) + return + + old_vnf_instance = copy.deepcopy(vnf_instance) + + # Update vnf_lcm_op_occs table and send notification "PROCESSING" + self._send_lcm_op_occ_notification( + context=context, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, + old_vnf_instance=old_vnf_instance, + vnf_instance=vnf_instance, + request_obj=heal_vnf_request, + operation=fields.LcmOccsOperationType.HEAL + ) + + heal_result = \ + self.vnflcm_driver.heal_vnf(context, vnf_instance, vnf_dict, + heal_vnf_request, + vnf_lcm_op_occs_id) + + # update vnf_lcm_op_occs and send notification "COMPLETED" + if heal_result: + evacuate_end_list = heal_result.get('evacuate_end_list') + + self._send_lcm_op_occ_notification( + context=context, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, + old_vnf_instance=old_vnf_instance, + vnf_instance=vnf_instance, + request_obj=heal_vnf_request, + operation=fields.LcmOccsOperationType.HEAL, + operation_state=fields.LcmOccsOperationState.COMPLETED, + evacuate_end_list=evacuate_end_list + ) + except Exception as ex: + + # update vnf_lcm_op_occs and send notification "FAILED_TEMP" + self._send_lcm_op_occ_notification( + context=context, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, + old_vnf_instance=old_vnf_instance, + vnf_instance=vnf_instance, + request_obj=heal_vnf_request, + operation=fields.LcmOccsOperationType.HEAL, + operation_state=fields.LcmOccsOperationState.FAILED_TEMP, + evacuate_end_list=evacuate_end_list, + error=str(ex) + ) def init(args, **kwargs): diff --git a/tacker/conductor/conductorrpc/vnf_lcm_rpc.py b/tacker/conductor/conductorrpc/vnf_lcm_rpc.py index 0ec0855..754acb9 100644 --- a/tacker/conductor/conductorrpc/vnf_lcm_rpc.py +++ b/tacker/conductor/conductorrpc/vnf_lcm_rpc.py @@ -14,7 +14,6 @@ # under the License. import oslo_messaging - from tacker.common import rpc from tacker.common import topics from tacker.objects import base as objects_base @@ -28,7 +27,9 @@ class VNFLcmRPCAPI(object): fanout=False, version='1.0') - def instantiate(self, context, vnf_instance, instantiate_vnf, cast=True): + def instantiate(self, context, vnf_instance, vnf, + instantiate_vnf, vnf_lcm_op_occs_id, + cast=True): serializer = objects_base.TackerObjectSerializer() client = rpc.get_client(self.target, version_cap=None, @@ -37,9 +38,13 @@ class VNFLcmRPCAPI(object): rpc_method = cctxt.cast if cast else cctxt.call return rpc_method(context, 'instantiate', vnf_instance=vnf_instance, - instantiate_vnf=instantiate_vnf) + vnf_dict=vnf, + instantiate_vnf=instantiate_vnf, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id) - def terminate(self, context, vnf_instance, terminate_vnf_req, cast=True): + def terminate(self, context, vnf_instance, vnf, + terminate_vnf_req, vnf_lcm_op_occs_id, + cast=True): serializer = objects_base.TackerObjectSerializer() client = rpc.get_client(self.target, version_cap=None, @@ -47,10 +52,13 @@ class VNFLcmRPCAPI(object): cctxt = client.prepare() rpc_method = cctxt.cast if cast else cctxt.call return rpc_method(context, 'terminate', + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, vnf_instance=vnf_instance, - terminate_vnf_req=terminate_vnf_req) + terminate_vnf_req=terminate_vnf_req, + vnf_dict=vnf) - def heal(self, context, vnf_instance, heal_vnf_request, cast=True): + def heal(self, context, vnf_instance, vnf_dict, heal_vnf_request, + vnf_lcm_op_occs_id, cast=True): serializer = objects_base.TackerObjectSerializer() client = rpc.get_client(self.target, version_cap=None, @@ -59,4 +67,16 @@ class VNFLcmRPCAPI(object): rpc_method = cctxt.cast if cast else cctxt.call return rpc_method(context, 'heal', vnf_instance=vnf_instance, - heal_vnf_request=heal_vnf_request) + vnf_dict=vnf_dict, + heal_vnf_request=heal_vnf_request, + vnf_lcm_op_occs_id=vnf_lcm_op_occs_id) + + def send_notification(self, context, notification, cast=True): + serializer = objects_base.TackerObjectSerializer() + + client = rpc.get_client(self.target, version_cap=None, + serializer=serializer) + cctxt = client.prepare() + rpc_method = cctxt.cast if cast else cctxt.call + return rpc_method(context, 'send_notification', + notification=notification) diff --git a/tacker/objects/__init__.py b/tacker/objects/__init__.py index 55fe047..2a271a0 100644 --- a/tacker/objects/__init__.py +++ b/tacker/objects/__init__.py @@ -34,6 +34,7 @@ def register_all(): __import__('tacker.objects.vim_connection') __import__('tacker.objects.instantiate_vnf_req') __import__('tacker.objects.vnf_resources') + __import__('tacker.objects.vnf_lcm_op_occs') __import__('tacker.objects.terminate_vnf_req') __import__('tacker.objects.vnf_artifact') __import__('tacker.objects.vnf_lcm_subscriptions') diff --git a/tacker/objects/fields.py b/tacker/objects/fields.py index cb223b7..bba05ec 100644 --- a/tacker/objects/fields.py +++ b/tacker/objects/fields.py @@ -184,3 +184,43 @@ class VnfcState(BaseTackerEnum): STOPPED = 'STOPPED' ALL = (STARTED, STOPPED) + + +class LcmOccsOperationState(BaseTackerEnum): + STARTING = 'STARTING' + PROCESSING = 'PROCESSING' + COMPLETED = 'COMPLETED' + FAILED_TEMP = 'FAILED_TEMP' + + ALL = (STARTING, PROCESSING, COMPLETED, FAILED_TEMP) + + +class LcmOccsOperationType(BaseTackerEnum): + INSTANTIATE = 'INSTANTIATE' + TERMINATE = 'TERMINATE' + HEAL = 'HEAL' + + ALL = (INSTANTIATE, TERMINATE, HEAL) + + +class LcmOccsNotificationStatus(BaseTackerEnum): + START = 'START' + RESULT = 'RESULT' + + ALL = (START, RESULT) + + +class ResourceChangeType(BaseTackerEnum): + ADDED = 'ADDED' + REMOVED = 'REMOVED' + MODIFIED = 'MODIFIED' + TEMPORARY = 'TEMPORARY' + + ALL = (ADDED, REMOVED, MODIFIED, TEMPORARY) + + +class LcmOccsNotificationType(BaseTackerEnum): + VNF_OP_OCC_NOTIFICATION = 'VnfLcmOperationOccurrenceNotification' + VNF_ID_CREATION_NOTIFICATION = 'VnfIdentifierCreationNotification' + + ALL = (VNF_OP_OCC_NOTIFICATION) diff --git a/tacker/objects/terminate_vnf_req.py b/tacker/objects/terminate_vnf_req.py index 85612c8..75d1a51 100644 --- a/tacker/objects/terminate_vnf_req.py +++ b/tacker/objects/terminate_vnf_req.py @@ -30,7 +30,9 @@ class TerminateVnfRequest(base.TackerObject, base.TackerPersistentObject): 'termination_type': fields.VnfInstanceTerminationTypeField( nullable=False), 'graceful_termination_timeout': fields.IntegerField(nullable=True, - default=0) + default=0), + 'additional_params': fields.DictOfStringsField(nullable=True, + default={}), } @classmethod diff --git a/tacker/objects/vnf_lcm_op_occs.py b/tacker/objects/vnf_lcm_op_occs.py new file mode 100644 index 0000000..c94427b --- /dev/null +++ b/tacker/objects/vnf_lcm_op_occs.py @@ -0,0 +1,713 @@ +# 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 datetime import datetime + +from oslo_log import log as logging +from oslo_serialization import jsonutils +from oslo_utils import timeutils +from sqlalchemy import exc +from sqlalchemy.orm import joinedload + +from tacker.common import exceptions +from tacker.db import api as db_api +from tacker.db.db_sqlalchemy import api +from tacker.db.db_sqlalchemy import models +from tacker import objects +from tacker.objects import base +from tacker.objects import fields + + +LOG = logging.getLogger(__name__) + + +@db_api.context_manager.writer +def _vnf_lcm_op_occ_create(context, values): + context.session.execute( + models.VnfLcmOpOccs.__table__.insert(None), + values) + + +@db_api.context_manager.writer +def _vnf_lcm_op_occ_update(context, values): + update = {'operation_state': values.operation_state, + 'state_entered_time': values.state_entered_time, + 'error_point': values.error_point, + 'updated_at': datetime.utcnow()} + LOG.debug('values %s', values) + if 'resource_changes' in values: + if values.resource_changes: + update.update({'resource_changes': jsonutils.dumps( + values.resource_changes.to_dict())}) + if 'error' in values: + if values.error: + update.update({'error': jsonutils.dumps(values.error.to_dict())}) + if 'changed_info' in values: + if values.changed_info: + update.update({'changed_info': jsonutils.dumps( + values.changed_info.to_dict())}) + api.model_query(context, models.VnfLcmOpOccs). \ + filter_by(id=values.id). \ + update(update, synchronize_session=False) + + +@db_api.context_manager.reader +def _vnf_lcm_op_occs_get_by_id(context, vnf_lcm_op_occ_id): + + query = api.model_query(context, models.VnfLcmOpOccs, + read_deleted="no", project_only=True). \ + filter_by(id=vnf_lcm_op_occ_id) + + result = query.first() + + if not result: + raise exceptions.NotFound(resource='table', + name='vnf_lcm_op_occs') + + return result + + +@db_api.context_manager.reader +def _vnf_notify_get_by_id(context, vnf_instance_id, columns_to_join=None): + + query = api.model_query(context, models.VnfLcmOpOccs, + read_deleted="no", project_only=True). \ + filter_by(id=vnf_instance_id) + + if columns_to_join: + for column in columns_to_join: + query = query.options(joinedload(column)) + + result = query.first() + + if not result: + raise exceptions.VnfInstanceNotFound(id=vnf_instance_id) + + return result + + +@db_api.context_manager.writer +def _vnf_notify_create(context, values): + + vnf_lcm_op_occs = models.VnfLcmOpOccs() + vnf_lcm_op_occs.update(values) + vnf_lcm_op_occs.save(context.session) + + return _vnf_notify_get_by_id(context, vnf_lcm_op_occs.id, + columns_to_join=None) + + +@db_api.context_manager.writer +def _vnf_notify_update(context, vnf_instance_id, values, + columns_to_join=None): + + vnf_lcm_op_occs = _vnf_notify_get_by_id(context, vnf_instance_id, + columns_to_join=columns_to_join) + values = values.to_dict() + vnf_lcm_op_occs.update(values) + vnf_lcm_op_occs.save(session=context.session) + + return vnf_lcm_op_occs + + +@db_api.context_manager.writer +def _destroy_vnf_notify(context, uuid): + now = timeutils.utcnow() + updated_values = {'deleted': True, + 'deleted_at': now + } + api.model_query(context, models.VnfLcmOpOccs). \ + filter_by(id=uuid). \ + update(updated_values, synchronize_session=False) + + +# decorator to catch DBAccess exception +def _wrap_object_error(method): + + def wrapper(*args, **kwargs): + try: + method(*args, **kwargs) + except exc.SQLAlchemyError: + raise exceptions.DBAccessError + + return wrapper + + +@base.TackerObjectRegistry.register +class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat, + base.TackerPersistentObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.UUIDField(nullable=False), + 'operation_state': fields.StringField(nullable=False), + 'state_entered_time': fields.DateTimeField(nullable=False), + 'start_time': fields.DateTimeField(nullable=False), + 'vnf_instance_id': fields.StringField(nullable=False), + 'operation': fields.StringField(nullable=False), + 'is_automatic_invocation': fields.BooleanField(default=False), + 'operation_params': fields.StringField(nullable=True), + 'is_cancel_pending': fields.BooleanField(default=False), + 'error': fields.ObjectField( + 'ProblemDetails', nullable=True, default=None), + 'resource_changes': fields.ObjectField( + 'ResourceChanges', nullable=True, default=None), + 'changed_info': fields.ObjectField( + 'VnfInfoModifications', nullable=True, default=None), + 'error_point': fields.IntegerField(nullable=True, default=0) + } + + @base.remotable + def create(self): + updates = self.obj_clone() + _vnf_lcm_op_occ_create(self._context, updates) + + @base.remotable + def save(self): + updates = self.obj_clone() + _vnf_lcm_op_occ_update(self._context, updates) + + @staticmethod + def _from_db_object(context, vnf_lcm_op_occ_obj, db_vnf_lcm_op_occ): + + special_fields = ['error', + 'resource_changes', 'changed_info'] + for key in vnf_lcm_op_occ_obj.fields: + if key in special_fields: + continue + setattr(vnf_lcm_op_occ_obj, key, db_vnf_lcm_op_occ.get(key)) + if db_vnf_lcm_op_occ['error']: + error = ProblemDetails.obj_from_primitive( + db_vnf_lcm_op_occ['error'], context) + vnf_lcm_op_occ_obj.error = error + if db_vnf_lcm_op_occ['resource_changes']: + resource_changes = ResourceChanges.obj_from_primitive( + db_vnf_lcm_op_occ['resource_changes'], context) + vnf_lcm_op_occ_obj.resource_changes = resource_changes + if db_vnf_lcm_op_occ['changed_info']: + changed_info = VnfInfoModifications.obj_from_primitive( + db_vnf_lcm_op_occ['changed_info'], context) + vnf_lcm_op_occ_obj.changed_info = changed_info + + vnf_lcm_op_occ_obj._context = context + vnf_lcm_op_occ_obj.obj_reset_changes() + return vnf_lcm_op_occ_obj + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + vnf_lcm_op_occ = super( + VnfLcmOpOcc, cls).obj_from_primitive( + primitive, context) + else: + if 'error' in primitive.keys(): + obj_data = ProblemDetails._from_dict( + primitive.get('error')) + primitive.update({'error': obj_data}) + if 'resource_changes' in primitive.keys(): + obj_data = ResourceChanges._from_dict( + primitive.get('resource_changes')) + primitive.update({'resource_changes': obj_data}) + if 'changed_info' in primitive.keys(): + obj_data = VnfInfoModifications._from_dict( + primitive.get('changed_info')) + primitive.update({'changed_info': obj_data}) + vnf_lcm_op_occ = VnfLcmOpOcc._from_dict(primitive) + + return vnf_lcm_op_occ + + @classmethod + def obj_from_db_obj(cls, context, db_obj): + return cls._from_db_object(context, cls(), db_obj) + + @classmethod + def _from_dict(cls, data_dict): + operation_state = data_dict.get('operation_state') + 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') + operation = data_dict.get('operation') + is_automatic_invocation = data_dict.get('is_automatic_invocation') + operation_params = data_dict.get('operation_params') + is_cancel_pending = data_dict.get('is_cancel_pending') + error = data_dict.get('error') + resource_changes = data_dict.get('resource_changes') + changed_info = data_dict.get('changed_info') + 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, + operation=operation, + is_automatic_invocation=is_automatic_invocation, + operation_params=operation_params, + is_cancel_pending=is_cancel_pending, + error=error, + resource_changes=resource_changes, + changed_info=changed_info, + 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} + if self.error: + data.update({'error': self.error.to_dict()}) + if self.resource_changes: + data.update({'resource_changes': self.resource_changes.to_dict()}) + if self.changed_info: + data.update({'changed_info': self.changed_info.to_dict()}) + + return data + + @base.remotable_classmethod + def get_by_id(cls, context, id): + db_vnf_lcm_op_occs = _vnf_lcm_op_occs_get_by_id(context, id) + return cls._from_db_object(context, cls(), db_vnf_lcm_op_occs) + + +@base.TackerObjectRegistry.register +class ResourceChanges(base.TackerObject, + base.TackerPersistentObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'affected_vnfcs': fields.ListOfObjectsField( + 'AffectedVnfc', nullable=True), + 'affected_virtual_links': fields.ListOfObjectsField( + 'AffectedVirtualLink', nullable=True), + 'affected_virtual_storages': fields.ListOfObjectsField( + 'AffectedVirtualStorage', nullable=True) + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + resource_changes = super( + ResourceChanges, cls).obj_from_primitive( + primitive, context) + else: + rs_dict = jsonutils.loads(primitive) + if rs_dict.get('affected_vnfcs'): + obj_data = [AffectedVnfc._from_dict( + affected_vnfc) for affected_vnfc in rs_dict.get( + 'affected_vnfcs', [])] + rs_dict.update({'affected_vnfcs': obj_data}) + if rs_dict.get('affected_virtual_links'): + obj_data = [AffectedVirtualLink._from_dict( + affected_virtual_link) + for affected_virtual_link in rs_dict.get( + 'affected_virtual_links', [])] + rs_dict.update({'affected_virtual_links': obj_data}) + if rs_dict.get('affected_virtual_storages'): + obj_data = [AffectedVirtualStorage._from_dict( + affected_virtual_storage) + for affected_virtual_storage in rs_dict.get( + 'affected_virtual_storages', [])] + rs_dict.update({'affected_virtual_storages': obj_data}) + resource_changes = ResourceChanges._from_dict(rs_dict) + + return resource_changes + + @classmethod + def _from_dict(cls, data_dict): + affected_vnfcs = data_dict.get('affected_vnfcs') + affected_virtual_links = data_dict.get('affected_virtual_links') + affected_virtual_storages = data_dict.get('affected_virtual_storages') + + obj = cls(affected_vnfcs=affected_vnfcs, + affected_virtual_links=affected_virtual_links, + affected_virtual_storages=affected_virtual_storages + ) + + return obj + + def to_dict(self): + data = {} + if self.affected_vnfcs: + affected_vnfcs_list = [] + for affected_vnfc in self.affected_vnfcs: + affected_vnfcs_list.append(affected_vnfc.to_dict()) + + data.update({'affected_vnfcs': affected_vnfcs_list}) + if self.affected_virtual_links: + affected_virtual_links_list = [] + for affected_virtual_link in self.affected_virtual_links: + affected_virtual_links_list.append( + affected_virtual_link.to_dict()) + + data.update( + {'affected_virtual_links': affected_virtual_links_list}) + if self.affected_virtual_storages: + affected_virtual_storages_list = [] + for affected_virtual_storage in self.affected_virtual_storages: + affected_virtual_storages_list.append( + affected_virtual_storage.to_dict()) + + data.update( + {'affected_virtual_storages': affected_virtual_storages_list}) + return data + + +@base.TackerObjectRegistry.register +class ProblemDetails(base.TackerObject, + base.TackerPersistentObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'title': fields.StringField(nullable=True, default=''), + 'status': fields.IntegerField(nullable=False), + 'detail': fields.StringField(nullable=False) + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + problem_detail = super( + ProblemDetails, cls).obj_from_primitive( + primitive, context) + else: + p_dict = jsonutils.loads(primitive) + problem_detail = ProblemDetails._from_dict(p_dict) + + return problem_detail + + @classmethod + def _from_dict(cls, data_dict): + title = data_dict.get('title') + status = data_dict.get('status') + detail = data_dict.get('detail') + + obj = cls(title=title, + status=status, + detail=detail) + + return obj + + def to_dict(self): + return {'title': self.title, + 'status': self.status, + 'detail': self.detail} + + +@base.TackerObjectRegistry.register +class AffectedVnfc(base.TackerObject, + base.TackerPersistentObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'vdu_id': fields.StringField(nullable=False), + 'change_type': fields.StringField(nullable=False), + 'compute_resource': fields.ObjectField( + 'ResourceHandle', nullable=False), + 'affected_vnfc_cp_ids': + fields.ListOfStringsField(nullable=True, default=[]), + 'added_storage_resource_ids': + fields.ListOfStringsField(nullable=True, default=[]), + 'removed_storage_resource_ids': + fields.ListOfStringsField(nullable=True, default=[]) + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + affected_vnfc = super( + AffectedVnfc, cls).obj_from_primitive( + primitive, context) + else: + if 'compute_resource' in primitive.keys(): + obj_data = ResourceHandle._from_dict( + primitive.get('compute_resource')) + primitive.update({'compute_resource': obj_data}) + affected_vnfc = AffectedVnfc._from_dict(primitive) + + return affected_vnfc + + @classmethod + def _from_dict(cls, data_dict): + id = data_dict.get('id') + vdu_id = data_dict.get('vdu_id') + change_type = data_dict.get('change_type') + compute_resource = ResourceHandle._from_dict( + data_dict.get('compute_resource')) + affected_vnfc_cp_ids = data_dict.get('affected_vnfc_cp_ids') + added_storage_resource_ids = data_dict.get( + 'added_storage_resource_ids') + removed_storage_resource_ids = data_dict.get( + 'removed_storage_resource_ids') + + obj = cls(id=id, + vdu_id=vdu_id, + change_type=change_type, + compute_resource=compute_resource, + affected_vnfc_cp_ids=affected_vnfc_cp_ids, + added_storage_resource_ids=added_storage_resource_ids, + removed_storage_resource_ids=removed_storage_resource_ids + ) + + return obj + + def to_dict(self): + return { + 'id': self.id, + 'vdu_id': self.vdu_id, + 'change_type': self.change_type, + 'compute_resource': self.compute_resource.to_dict(), + 'affected_vnfc_cp_ids': self.affected_vnfc_cp_ids, + 'added_storage_resource_ids': self.added_storage_resource_ids, + 'removed_storage_resource_ids': self.removed_storage_resource_ids} + + +@base.TackerObjectRegistry.register +class AffectedVirtualLink(base.TackerObject, + base.TackerPersistentObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'vnf_virtual_link_desc_id': fields.StringField(nullable=False), + 'change_type': fields.StringField(nullable=False), + 'network_resource': fields.ObjectField( + 'ResourceHandle', nullable=False) + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + affected_virtual_link = super( + AffectedVirtualLink, cls).obj_from_primitive( + primitive, context) + else: + if 'network_resource' in primitive.keys(): + obj_data = ResourceHandle._from_dict( + primitive.get('network_resource')) + primitive.update({'network_resource': obj_data}) + affected_virtual_link = AffectedVirtualLink._from_dict(primitive) + + return affected_virtual_link + + @classmethod + def _from_dict(cls, data_dict): + id = data_dict.get('id') + vnf_virtual_link_desc_id = data_dict.get('vnf_virtual_link_desc_id') + change_type = data_dict.get('change_type') + network_resource = ResourceHandle._from_dict( + data_dict.get('network_resource')) + + obj = cls(id=id, + vnf_virtual_link_desc_id=vnf_virtual_link_desc_id, + change_type=change_type, + network_resource=network_resource + ) + + return obj + + def to_dict(self): + return {'id': self.id, + 'vnf_virtual_link_desc_id': self.vnf_virtual_link_desc_id, + 'change_type': self.change_type, + 'network_resource': self.network_resource.to_dict()} + + +@base.TackerObjectRegistry.register +class AffectedVirtualStorage(base.TackerObject, + base.TackerPersistentObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.StringField(nullable=False), + 'virtual_storage_desc_id': fields.StringField(nullable=False), + 'change_type': fields.StringField(nullable=False), + 'storage_resource': fields.ObjectField( + 'ResourceHandle', nullable=False) + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + affected_virtual_storage = super( + AffectedVirtualStorage, cls).obj_from_primitive( + primitive, context) + else: + if 'storage_resource' in primitive.keys(): + obj_data = ResourceHandle._from_dict( + primitive.get('storage_resource')) + primitive.update({'storage_resource': obj_data}) + affected_virtual_storage = AffectedVirtualStorage._from_dict( + primitive) + + return affected_virtual_storage + + @classmethod + def _from_dict(cls, data_dict): + id = data_dict.get('id') + virtual_storage_desc_id = data_dict.get('virtual_storage_desc_id') + change_type = data_dict.get('change_type') + storage_resource = ResourceHandle._from_dict( + data_dict.get('storage_resource')) + + obj = cls(id=id, + virtual_storage_desc_id=virtual_storage_desc_id, + change_type=change_type, + storage_resource=storage_resource + ) + + return obj + + def to_dict(self): + return {'id': self.id, + 'virtual_storage_desc_id': self.virtual_storage_desc_id, + 'change_type': self.change_type, + 'storage_resource': self.storage_resource.to_dict()} + + +@base.TackerObjectRegistry.register +class VnfInfoModifications(base.TackerObject, + base.TackerPersistentObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'vnf_instance_name': fields.StringField(nullable=True), + 'vnf_instance_description': fields.StringField(nullable=True), + 'vim_connection_info': fields.ListOfObjectsField( + 'VimConnectionInfo', nullable=True, default=[]), + 'vim_connection_info_delete_ids': + fields.ListOfStringsField(nullable=True, default=[]), + 'vnf_pkg_id': fields.StringField(nullable=True, default=None), + 'vnfd_id': fields.StringField(nullable=True), + 'vnf_provider': fields.StringField(nullable=True), + 'vnf_product_name': fields.StringField(nullable=True), + 'vnf_software_version': fields.StringField(nullable=True), + 'vnfd_version': fields.StringField(nullable=True) + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + vnf_info_modifications = super( + VnfInfoModifications, cls).obj_from_primitive( + primitive, context) + else: + if 'vim_connection_info' in primitive.keys(): + obj_data = [objects.VimConnectionInfo._from_dict( + vim_conn) for vim_conn in primitive.get( + 'vim_connection_info', [])] + primitive.update({'vim_connection_info': obj_data}) + vnf_info_modifications = VnfInfoModifications._from_dict(primitive) + + return vnf_info_modifications + + @classmethod + def _from_dict(cls, data_dict): + vnf_instance_name = data_dict.get('vnf_instance_name') + vnf_instance_description = data_dict.get('vnf_instance_description') + vim_connection_info = data_dict.get('vim_connection_info', []) + vim_connection_info_delete_ids = data_dict.get( + 'vim_connection_info_delete_ids') + vnf_pkg_id = data_dict.get('vnf_pkg_id') + vnfd_id = data_dict.get('vnfd_id') + vnf_provider = data_dict.get('vnf_provider') + vnf_product_name = data_dict.get('vnf_product_name') + vnf_software_version = data_dict.get('vnf_software_version') + vnfd_version = data_dict.get('vnfd_version') + + obj = cls( + vnf_instance_name=vnf_instance_name, + vnf_instance_description=vnf_instance_description, + vim_connection_info=vim_connection_info, + vim_connection_info_delete_ids=vim_connection_info_delete_ids, + vnf_pkg_id=vnf_pkg_id, + vnfd_id=vnfd_id, + vnf_provider=vnf_provider, + vnf_product_name=vnf_product_name, + vnf_software_version=vnf_software_version, + vnfd_version=vnfd_version) + + return obj + + def to_dict(self): + return { + 'vnf_instance_name': self.vnf_instance_name, + 'vnf_instance_description': self.vnf_instance_description, + 'vim_connection_info': self.vim_connection_info, + 'vim_connection_info_delete_ids': + self.vim_connection_info_delete_ids, + 'vnf_pkg_id': self.vnf_pkg_id, + 'vnfd_id': self.vnfd_id, + 'vnf_provider': self.vnf_provider, + 'vnf_product_name': self.vnf_product_name, + 'vnf_software_version': self.vnf_software_version, + 'vnfd_version': self.vnfd_version} + + +@base.TackerObjectRegistry.register +class ResourceHandle(base.TackerObject, + base.TackerPersistentObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'vim_connection_id': fields.StringField(nullable=True, + default=None), + 'resource_id': fields.StringField(nullable=False, default=""), + 'vim_level_resource_type': fields.StringField(nullable=True, + default=None) + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + resource_handle = super( + ResourceHandle, cls).obj_from_primitive( + primitive, context) + else: + resource_handle = ResourceHandle._from_dict(primitive) + + return resource_handle + + @classmethod + def _from_dict(cls, data_dict): + LOG.debug("data_dict %s", data_dict) + vim_connection_id = data_dict.get('vim_connection_id') + resource_id = data_dict.get('resource_id', "") + vim_level_resource_type = data_dict.get('vim_level_resource_type') + + obj = cls(vim_connection_id=vim_connection_id, + resource_id=resource_id, + vim_level_resource_type=vim_level_resource_type) + + return obj + + def to_dict(self): + return {'vim_connection_id': self.vim_connection_id, + 'resource_id': self.resource_id, + 'vim_level_resource_type': self.vim_level_resource_type} diff --git a/tacker/plugins/common/constants.py b/tacker/plugins/common/constants.py index cee94ea..f559d85 100644 --- a/tacker/plugins/common/constants.py +++ b/tacker/plugins/common/constants.py @@ -31,6 +31,7 @@ COMMON_PREFIXES = { # Service operation status constants ACTIVE = "ACTIVE" ACK = "ACK" +INACTIVE = "INACTIVE" PENDING_CREATE = "PENDING_CREATE" PENDING_UPDATE = "PENDING_UPDATE" diff --git a/tacker/tests/unit/conductor/fakes.py b/tacker/tests/unit/conductor/fakes.py index 2fc890b..7d713f0 100644 --- a/tacker/tests/unit/conductor/fakes.py +++ b/tacker/tests/unit/conductor/fakes.py @@ -13,14 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime +import iso8601 + import os -from oslo_config import cfg import shutil import tempfile import uuid import yaml import zipfile +from oslo_config import cfg + from tacker.tests import utils from tacker.tests import uuidsentinel @@ -86,6 +90,40 @@ def create_fake_csar_dir(vnf_package_id, temp_dir, return fake_csar +def get_vnf_package_vnfd(): + return { + "id": uuidsentinel.vnfd_id, + "vnf_provider": "test vnf provider", + "vnf_product_name": "Sample VNF", + "vnf_software_version": "1.0", + "vnfd_version": "1.0", + "name": 'Sample VNF Instance', + } + + +def get_lcm_op_occs_data(): + return { + "tenant_id": uuidsentinel.tenant_id, + 'operation_state': 'PROCESSING', + '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), + 'operation': 'MODIFY_INFO', + 'is_automatic_invocation': 0, + 'is_cancel_pending': 0, + } + + +def get_vnf_lcm_subscriptions(): + subscription_id = uuidsentinel.subscription_id + return { + "id": subscription_id.encode(), + "callback_uri": b'http://localhost:9890/' + } + + def get_expected_vnfd_data(zip_file=None): if zip_file: csar_temp_dir = tempfile.mkdtemp() diff --git a/tacker/tests/unit/conductor/test_conductor_server.py b/tacker/tests/unit/conductor/test_conductor_server.py index 413f9b3..aeebb9a 100644 --- a/tacker/tests/unit/conductor/test_conductor_server.py +++ b/tacker/tests/unit/conductor/test_conductor_server.py @@ -19,8 +19,10 @@ import sys from unittest import mock import fixtures + from glance_store import exceptions as store_exceptions from oslo_config import cfg +import requests from six.moves import urllib import six.moves.urllib.error as urlerr import yaml @@ -36,6 +38,7 @@ from tacker import objects from tacker.objects import fields from tacker.tests.unit.conductor import fakes from tacker.tests.unit.db.base import SqlTestCase +from tacker.tests.unit.db import utils as db_utils from tacker.tests.unit.objects import fakes as fake_obj from tacker.tests.unit.vnflcm import fakes as vnflcm_fakes from tacker.tests import utils @@ -62,6 +65,7 @@ class TestConductor(SqlTestCase): self._mock_vnfm_plugin() self.conductor = conductor_server.Conductor('host') self.vnf_package = self._create_vnf_package() + self.instance_uuid = uuidsentinel.instance_id self.temp_dir = self.useFixture(fixtures.TempDir()).path def _mock_vnfm_plugin(self): @@ -84,6 +88,9 @@ class TestConductor(SqlTestCase): vnfpkgm.create() return vnfpkgm + def _create_vnf_package_vnfd(self): + return fakes.get_vnf_package_vnfd() + @mock.patch.object(conductor_server.Conductor, '_onboard_vnf_package') @mock.patch.object(conductor_server, 'revert_upload_vnf_package') @mock.patch.object(csar_utils, 'load_csar_data') @@ -204,10 +211,19 @@ class TestConductor(SqlTestCase): return vnf_pack_vnfd_obj + @mock.patch.object(objects.VnfLcmOpOcc, "save") @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') - def test_instantiate_vnf_instance(self, mock_package_in_use, - mock_get_lock): + @mock.patch.object(objects.VnfLcmOpOcc, "get_by_id") + def test_instantiate_vnf_instance(self, mock_vnf_by_id, + mock_package_in_use, + mock_get_lock, + mock_save): + lcm_op_occs_data = fakes.get_lcm_op_occs_data() + mock_vnf_by_id.return_value = \ + objects.VnfLcmOpOcc(context=self.context, + **lcm_op_occs_data) + vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) @@ -216,17 +232,27 @@ class TestConductor(SqlTestCase): **vnf_instance_data) vnf_instance.create() instantiate_vnf_req = vnflcm_fakes.get_instantiate_vnf_request_obj() + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id self.conductor.instantiate(self.context, vnf_instance, - instantiate_vnf_req) + instantiate_vnf_req, + vnf_lcm_op_occs_id) self.vnflcm_driver.instantiate_vnf.assert_called_once_with( self.context, mock.ANY, instantiate_vnf_req) mock_package_in_use.assert_called_once() + @mock.patch.object(objects.VnfLcmOpOcc, "save") @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') @mock.patch('tacker.conductor.conductor_server.LOG') + @mock.patch.object(objects.VnfLcmOpOcc, "get_by_id") def test_instantiate_vnf_instance_already_instantiated(self, - mock_log, mock_package_in_use, mock_get_lock): + mock_vnf_by_id, mock_log, mock_package_in_use, mock_get_lock, + mock_save): + lcm_op_occs_data = fakes.get_lcm_op_occs_data() + mock_vnf_by_id.return_value = \ + objects.VnfLcmOpOcc(context=self.context, + **lcm_op_occs_data) + vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) @@ -236,8 +262,10 @@ class TestConductor(SqlTestCase): **vnf_instance_data) vnf_instance.create() instantiate_vnf_req = vnflcm_fakes.get_instantiate_vnf_request_obj() + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id self.conductor.instantiate(self.context, vnf_instance, - instantiate_vnf_req) + instantiate_vnf_req, + vnf_lcm_op_occs_id) self.vnflcm_driver.instantiate_vnf.assert_not_called() mock_package_in_use.assert_not_called() expected_log = 'Vnf instance %(id)s is already in %(state)s state.' @@ -245,29 +273,58 @@ class TestConductor(SqlTestCase): {'id': vnf_instance.id, 'state': fields.VnfInstanceState.INSTANTIATED}) + @mock.patch.object(objects.VnfLcmOpOcc, "save") @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') + @mock.patch.object(objects.LccnSubscriptionRequest, + 'vnf_lcm_subscriptions_get') + @mock.patch.object(objects.VnfLcmOpOcc, "get_by_id") def test_instantiate_vnf_instance_with_vnf_package_in_use(self, - mock_vnf_package_in_use, mock_get_lock): + mock_vnf_by_id, + mock_vnf_lcm_subscriptions_get, + mock_vnf_package_in_use, mock_get_lock, mock_save): + lcm_op_occs_data = fakes.get_lcm_op_occs_data() + mock_vnf_by_id.return_value = \ + objects.VnfLcmOpOcc(context=self.context, + **lcm_op_occs_data) + vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) + m_vnf_lcm_subscriptions = \ + [mock.MagicMock(**fakes.get_vnf_lcm_subscriptions())] + mock_vnf_lcm_subscriptions_get.return_value = \ + m_vnf_lcm_subscriptions mock_vnf_package_in_use.return_value = True vnf_instance = objects.VnfInstance(context=self.context, **vnf_instance_data) vnf_instance.create() instantiate_vnf_req = vnflcm_fakes.get_instantiate_vnf_request_obj() + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id self.conductor.instantiate(self.context, vnf_instance, - instantiate_vnf_req) + instantiate_vnf_req, + vnf_lcm_op_occs_id) self.vnflcm_driver.instantiate_vnf.assert_called_once_with( self.context, mock.ANY, instantiate_vnf_req) mock_vnf_package_in_use.assert_called_once() + @mock.patch.object(objects.VnfLcmOpOcc, "save") @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') + @mock.patch.object(objects.LccnSubscriptionRequest, + 'vnf_lcm_subscriptions_get') @mock.patch('tacker.conductor.conductor_server.LOG') + @mock.patch.object(objects.VnfLcmOpOcc, "get_by_id") + @mock.patch('tacker.vnflcm.utils._get_affected_resources') def test_instantiate_vnf_instance_failed_with_exception( - self, mock_log, mock_is_package_in_use, mock_get_lock): + self, mock_res, mock_vnf_by_id, mock_log, + mock_vnf_lcm_subscriptions_get, + mock_is_package_in_use, mock_get_lock, mock_save): + lcm_op_occs_data = fakes.get_lcm_op_occs_data() + mock_vnf_by_id.return_value = \ + objects.VnfLcmOpOcc(context=self.context, + **lcm_op_occs_data) + vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) @@ -275,9 +332,16 @@ class TestConductor(SqlTestCase): **vnf_instance_data) vnf_instance.create() instantiate_vnf_req = vnflcm_fakes.get_instantiate_vnf_request_obj() + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + m_vnf_lcm_subscriptions = \ + [mock.MagicMock(**fakes.get_vnf_lcm_subscriptions())] + mock_vnf_lcm_subscriptions_get.return_value = \ + m_vnf_lcm_subscriptions mock_is_package_in_use.side_effect = Exception + mock_res.return_value = {} self.conductor.instantiate(self.context, vnf_instance, - instantiate_vnf_req) + instantiate_vnf_req, + vnf_lcm_op_occs_id) self.vnflcm_driver.instantiate_vnf.assert_called_once_with( self.context, mock.ANY, instantiate_vnf_req) mock_is_package_in_use.assert_called_once() @@ -285,9 +349,13 @@ class TestConductor(SqlTestCase): mock_log.error.assert_called_once_with(expected_log, vnf_package_vnfd.package_uuid) + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_send_lcm_op_occ_notification') @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') - def test_terminate_vnf_instance(self, mock_package_in_use, mock_get_lock): + def test_terminate_vnf_instance(self, mock_package_in_use, + mock_get_lock, + mock_send_notification): vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) @@ -299,20 +367,27 @@ class TestConductor(SqlTestCase): vnf_instance.create() terminate_vnf_req = objects.TerminateVnfRequest( - termination_type=fields.VnfInstanceTerminationType.GRACEFUL) - - self.conductor.terminate(self.context, vnf_instance, - terminate_vnf_req) + termination_type=fields.VnfInstanceTerminationType.GRACEFUL, + additional_params={"key": "value"}) + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + vnf_dict = db_utils.get_dummy_vnf(instance_id=self.instance_uuid) + self.conductor.terminate(self.context, vnf_lcm_op_occs_id, + vnf_instance, + terminate_vnf_req, vnf_dict) self.vnflcm_driver.terminate_vnf.assert_called_once_with( - self.context, mock.ANY, terminate_vnf_req) + self.context, mock.ANY, terminate_vnf_req, + vnf_lcm_op_occs_id) mock_package_in_use.assert_called_once() + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_send_lcm_op_occ_notification') @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') @mock.patch('tacker.conductor.conductor_server.LOG') def test_terminate_vnf_instance_already_not_instantiated(self, - mock_log, mock_package_in_use, mock_get_lock): + mock_log, mock_package_in_use, mock_get_lock, + mock_send_notification): vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) @@ -324,10 +399,13 @@ class TestConductor(SqlTestCase): vnf_instance.create() terminate_vnf_req = objects.TerminateVnfRequest( - termination_type=fields.VnfInstanceTerminationType.GRACEFUL) - - self.conductor.terminate(self.context, vnf_instance, - terminate_vnf_req) + termination_type=fields.VnfInstanceTerminationType.GRACEFUL, + additional_params={"key": "value"}) + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + vnf_dict = db_utils.get_dummy_vnf(instance_id=self.instance_uuid) + self.conductor.terminate(self.context, vnf_lcm_op_occs_id, + vnf_instance, + terminate_vnf_req, vnf_dict) self.vnflcm_driver.terminate_vnf.assert_not_called() mock_package_in_use.assert_not_called() @@ -337,10 +415,13 @@ class TestConductor(SqlTestCase): {'id': vnf_instance.id, 'state': fields.VnfInstanceState.NOT_INSTANTIATED}) + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_send_lcm_op_occ_notification') @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') def test_terminate_vnf_instance_with_usage_state_not_in_use(self, - mock_vnf_package_is_package_in_use, mock_get_lock): + mock_vnf_package_is_package_in_use, mock_get_lock, + mock_send_notification): vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) @@ -352,19 +433,26 @@ class TestConductor(SqlTestCase): mock_vnf_package_is_package_in_use.return_value = False terminate_vnf_req = objects.TerminateVnfRequest( - termination_type=fields.VnfInstanceTerminationType.GRACEFUL) - - self.conductor.terminate(self.context, vnf_instance, - terminate_vnf_req) + termination_type=fields.VnfInstanceTerminationType.GRACEFUL, + additional_params={"key": "value"}) + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + vnf_dict = db_utils.get_dummy_vnf(instance_id=self.instance_uuid) + self.conductor.terminate(self.context, vnf_lcm_op_occs_id, + vnf_instance, + terminate_vnf_req, vnf_dict) self.vnflcm_driver.terminate_vnf.assert_called_once_with( - self.context, mock.ANY, terminate_vnf_req) + self.context, mock.ANY, terminate_vnf_req, + vnf_lcm_op_occs_id) mock_vnf_package_is_package_in_use.assert_called_once() + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_send_lcm_op_occ_notification') @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') def test_terminate_vnf_instance_with_usage_state_already_in_use(self, - mock_vnf_package_is_package_in_use, mock_get_lock): + mock_vnf_package_is_package_in_use, mock_get_lock, + mock_send_notification): vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) @@ -376,20 +464,27 @@ class TestConductor(SqlTestCase): mock_vnf_package_is_package_in_use.return_value = True terminate_vnf_req = objects.TerminateVnfRequest( - termination_type=fields.VnfInstanceTerminationType.GRACEFUL) - - self.conductor.terminate(self.context, vnf_instance, - terminate_vnf_req) + termination_type=fields.VnfInstanceTerminationType.GRACEFUL, + additional_params={"key": "value"}) + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + vnf_dict = db_utils.get_dummy_vnf(instance_id=self.instance_uuid) + self.conductor.terminate(self.context, vnf_lcm_op_occs_id, + vnf_instance, + terminate_vnf_req, vnf_dict) self.vnflcm_driver.terminate_vnf.assert_called_once_with( - self.context, mock.ANY, terminate_vnf_req) + self.context, mock.ANY, terminate_vnf_req, + vnf_lcm_op_occs_id) mock_vnf_package_is_package_in_use.assert_called_once() + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_send_lcm_op_occ_notification') @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') @mock.patch('tacker.conductor.conductor_server.LOG') def test_terminate_vnf_instance_failed_to_update_usage_state( - self, mock_log, mock_is_package_in_use, mock_get_lock): + self, mock_log, mock_is_package_in_use, mock_get_lock, + mock_send_notification): vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) @@ -399,18 +494,30 @@ class TestConductor(SqlTestCase): **vnf_instance_data) vnf_instance.create() terminate_vnf_req = objects.TerminateVnfRequest( - termination_type=fields.VnfInstanceTerminationType.GRACEFUL) + termination_type=fields.VnfInstanceTerminationType.GRACEFUL, + additional_params={"key": "value"}) mock_is_package_in_use.side_effect = Exception - self.conductor.terminate(self.context, vnf_instance, - terminate_vnf_req) + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + vnf_dict = db_utils.get_dummy_vnf(instance_id=self.instance_uuid) + self.conductor.terminate(self.context, vnf_lcm_op_occs_id, + vnf_instance, + terminate_vnf_req, vnf_dict) self.vnflcm_driver.terminate_vnf.assert_called_once_with( - self.context, mock.ANY, terminate_vnf_req) + self.context, mock.ANY, terminate_vnf_req, + vnf_lcm_op_occs_id) expected_msg = "Failed to update usage_state of vnf package %s" mock_log.error.assert_called_once_with(expected_msg, vnf_package_vnfd.package_uuid) + @mock.patch.object(objects.VnfLcmOpOcc, "save") @mock.patch.object(coordination.Coordinator, 'get_lock') - def test_heal_vnf_instance(self, mock_get_lock): + @mock.patch.object(objects.VnfLcmOpOcc, "get_by_id") + def test_heal_vnf_instance(self, mock_vnf_by_id, mock_get_lock, + mock_save): + lcm_op_occs_data = fakes.get_lcm_op_occs_data() + mock_vnf_by_id.return_value = \ + objects.VnfLcmOpOcc(context=self.context, + **lcm_op_occs_data) vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) @@ -421,9 +528,10 @@ class TestConductor(SqlTestCase): fields.VnfInstanceState.INSTANTIATED vnf_instance.save() heal_vnf_req = objects.HealVnfRequest(cause="healing request") - self.conductor.heal(self.context, vnf_instance, heal_vnf_req) - self.vnflcm_driver.heal_vnf.assert_called_once_with( - self.context, mock.ANY, heal_vnf_req) + vnf_dict = {"fake": "fake_dict"} + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + self.conductor.heal(self.context, vnf_instance, vnf_dict, + heal_vnf_req, vnf_lcm_op_occs_id) @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch('tacker.conductor.conductor_server.LOG') @@ -440,7 +548,10 @@ class TestConductor(SqlTestCase): vnf_instance.create() heal_vnf_req = objects.HealVnfRequest(cause="healing request") - self.conductor.heal(self.context, vnf_instance, heal_vnf_req) + vnf_dict = {"fake": "fake_dict"} + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + self.conductor.heal(self.context, vnf_instance, vnf_dict, + heal_vnf_req, vnf_lcm_op_occs_id) self.vnflcm_driver.heal_vnf.assert_not_called() expected_log = ('Heal action cannot be performed on vnf %(id)s ' @@ -508,3 +619,149 @@ class TestConductor(SqlTestCase): user_name=user_name, password=password) self.assertEqual('CREATED', self.vnf_package.onboarding_state) + + def test_send_notification_not_found_vnfd(self): + notification = {'vnfInstanceId': 'Test'} + + result = self.conductor.send_notification(self.context, notification) + + self.assertEqual(result, -2) + + @mock.patch.object(objects.LccnSubscriptionRequest, + 'vnf_lcm_subscriptions_get') + def test_send_notification_not_found_subscription( + self, + mock_vnf_lcm_subscriptions_get): + + mock_vnf_lcm_subscriptions_get.return_value = None + notification = { + 'vnfInstanceId': 'Test', + 'notificationType': 'VnfLcmOperationOccurrenceNotification'} + + result = self.conductor.send_notification(self.context, notification) + + self.assertEqual(result, -1) + mock_vnf_lcm_subscriptions_get.assert_called() + + @mock.patch.object(objects.LccnSubscriptionRequest, + 'vnf_lcm_subscriptions_get') + @mock.patch('requests.post') + def test_send_notification_vnf_lcm_operation_occurrence( + self, + mock_post, + mock_vnf_lcm_subscriptions_get): + + response = mock.Mock() + response.status_code = 204 + mock_post.return_value = response + + m_vnf_lcm_subscriptions = \ + [mock.MagicMock(**fakes.get_vnf_lcm_subscriptions())] + mock_vnf_lcm_subscriptions_get.return_value = \ + m_vnf_lcm_subscriptions + notification = { + 'vnfInstanceId': 'Test', + 'notificationType': 'VnfLcmOperationOccurrenceNotification', + 'operationTypes': 'SCALE', + 'operationStates': 'RESULT', + '_links': {}} + + result = self.conductor.send_notification(self.context, notification) + + self.assertEqual(result, 0) + mock_vnf_lcm_subscriptions_get.assert_called() + mock_post.assert_called() + + @mock.patch.object(objects.LccnSubscriptionRequest, + 'vnf_lcm_subscriptions_get') + @mock.patch('requests.post') + def test_send_notification_vnf_identifier_creation( + self, + mock_post, + mock_vnf_lcm_subscriptions_get): + + response = mock.Mock() + response.status_code = 204 + mock_post.return_value = response + + m_vnf_lcm_subscriptions = \ + [mock.MagicMock(**fakes.get_vnf_lcm_subscriptions())] + mock_vnf_lcm_subscriptions_get.return_value = \ + m_vnf_lcm_subscriptions + notification = { + 'vnfInstanceId': 'Test', + 'notificationType': 'VnfIdentifierCreationNotification', + 'links': {}} + + result = self.conductor.send_notification(self.context, notification) + + self.assertEqual(result, 0) + mock_vnf_lcm_subscriptions_get.assert_called() + mock_post.assert_called() + + @mock.patch.object(objects.LccnSubscriptionRequest, + 'vnf_lcm_subscriptions_get') + @mock.patch('requests.post') + def test_send_notification_retry_notification( + self, + mock_post, + mock_vnf_lcm_subscriptions_get): + + response = mock.Mock() + response.status_code = 400 + mock_post.return_value = response + + m_vnf_lcm_subscriptions = \ + [mock.MagicMock(**fakes.get_vnf_lcm_subscriptions())] + mock_vnf_lcm_subscriptions_get.return_value = \ + m_vnf_lcm_subscriptions + notification = { + 'vnfInstanceId': 'Test', + 'notificationType': 'VnfIdentifierCreationNotification', + 'links': {}} + + result = self.conductor.send_notification(self.context, notification) + + self.assertEqual(result, 0) + mock_vnf_lcm_subscriptions_get.assert_called() + mock_post.assert_called() + self.assertEqual(mock_post.call_count, 3) + + @mock.patch.object(objects.LccnSubscriptionRequest, + 'vnf_lcm_subscriptions_get') + @mock.patch('requests.post') + def test_send_notification_send_error(self, + mock_post, + mock_vnf_lcm_subscriptions_get): + mock_post.side_effect = \ + requests.exceptions.HTTPError("MockException") + m_vnf_lcm_subscriptions = \ + [mock.MagicMock(**fakes.get_vnf_lcm_subscriptions())] + mock_vnf_lcm_subscriptions_get.return_value = \ + m_vnf_lcm_subscriptions + notification = { + 'vnfInstanceId': 'Test', + 'notificationType': 'VnfIdentifierCreationNotification', + 'links': {}} + + result = self.conductor.send_notification(self.context, notification) + + self.assertEqual(result, 0) + mock_vnf_lcm_subscriptions_get.assert_called() + mock_post.assert_called() + self.assertEqual(mock_post.call_count, 1) + + @mock.patch.object(objects.LccnSubscriptionRequest, + 'vnf_lcm_subscriptions_get') + def test_send_notification_internal_server_error(self, + mock_vnf_lcm_subscriptions_get): + mock_vnf_lcm_subscriptions_get.side_effect = Exception( + "MockException") + notification = { + 'vnfInstanceId': 'Test', + 'notificationType': 'VnfIdentifierCreationNotification', + 'links': {}} + + result = self.conductor.send_notification(self.context, notification) + + self.assertEqual(result, -2) diff --git a/tacker/tests/unit/objects/fakes.py b/tacker/tests/unit/objects/fakes.py index a333ca8..d90cd33 100644 --- a/tacker/tests/unit/objects/fakes.py +++ b/tacker/tests/unit/objects/fakes.py @@ -178,6 +178,21 @@ def get_vnf_instance_data_with_id(vnfd_id): } +def get_lcm_op_occs_data(vnf_instance_id): + return { + "tenant_id": uuidsentinel.tenant_id, + 'operation_state': 'PROCESSING', + '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': vnf_instance_id, + 'operation': 'MODIFY_INFO', + 'is_automatic_invocation': 0, + 'is_cancel_pending': 0, + } + + def fake_vnf_instance_model_dict(**updates): vnf_instance = { 'deleted': False, @@ -401,3 +416,17 @@ def vnf_instance_model_object(vnf_instance): vnf_instance_db_obj = models.VnfInstance() vnf_instance_db_obj.update(instance_dict) return vnf_instance_db_obj + + +def get_changed_info_data(): + return { + "vnf_instance_name": "", + "vnf_instance_description": "", + "vnf_configurable_properties": {"test": "test_value"}, + "vnfc_info_modifications_delete_ids": ["test1"], + "vnfd_id": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnf_provider": "NEC", + "vnf_product_name": "MME", + "vnf_software_version": "1.0", + "vnfd_version": "MME_1.0" + } diff --git a/tacker/tests/unit/objects/test_vnf_lcm_op_occs.py b/tacker/tests/unit/objects/test_vnf_lcm_op_occs.py new file mode 100644 index 0000000..ef0a1d9 --- /dev/null +++ b/tacker/tests/unit/objects/test_vnf_lcm_op_occs.py @@ -0,0 +1,92 @@ +# 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 tacker import context +from tacker import objects +from tacker.tests.unit.db.base import SqlTestCase +from tacker.tests.unit.objects import fakes +from tacker.tests import uuidsentinel + + +class TestVnfLcmOpOcc(SqlTestCase): + + def setUp(self): + super(TestVnfLcmOpOcc, self).setUp() + self.context = context.get_admin_context() + self.vnf_package = self._create_vnf_package() + self.vnf_package_vnfd = self._create_and_upload_vnf_package_vnfd() + self.vnf_instance = self._create_vnf_instance() + self.vnf_lcm_op_occs = self._create_vnf_lcm_op_occs() + + def _create_vnf_package(self): + vnfpkgm = objects.VnfPackage(context=self.context, + **fakes.vnf_package_data) + vnfpkgm.create() + return vnfpkgm + + def _create_and_upload_vnf_package_vnfd(self): + vnf_package = objects.VnfPackage(context=self.context, + **fakes.vnf_package_data) + vnf_package.create() + + vnf_pack_vnfd = fakes.get_vnf_package_vnfd_data( + vnf_package.id, uuidsentinel.vnfd_id) + + vnf_pack_vnfd_obj = objects.VnfPackageVnfd( + context=self.context, **vnf_pack_vnfd) + vnf_pack_vnfd_obj.create() + + return vnf_pack_vnfd_obj + + def _create_vnf_instance(self): + vnf_instance_data = fakes.get_vnf_instance_data( + self.vnf_package_vnfd.vnfd_id) + vnf_instance = objects.VnfInstance(context=self.context, + **vnf_instance_data) + vnf_instance.create() + + return vnf_instance + + def _create_vnf_lcm_op_occs(self): + vnf_lcm_op_occs_data = fakes.get_lcm_op_occs_data(self.vnf_instance.id) + vnf_lcm_op_occs = objects.vnf_lcm_op_occs.VnfLcmOpOcc( + context=self.context, **vnf_lcm_op_occs_data) + vnf_lcm_op_occs.create() + return vnf_lcm_op_occs + + def test_create(self): + vnf_lcm_op_occs_data = fakes.get_lcm_op_occs_data(self.vnf_instance.id) + vnf_lcm_op_occs = objects.vnf_lcm_op_occs.VnfLcmOpOcc( + context=self.context, **vnf_lcm_op_occs_data) + vnf_lcm_op_occs.create() + self.assertTrue(vnf_lcm_op_occs.vnf_instance_id) + + def test_save(self): + vnf_lcm_op_occs_data = fakes.get_lcm_op_occs_data(self.vnf_instance.id) + vnf_lcm_op_occs = objects.vnf_lcm_op_occs.VnfLcmOpOcc( + context=self.context, **vnf_lcm_op_occs_data) + vnf_lcm_op_occs.create() + + problem_obj = objects.vnf_lcm_op_occs.ProblemDetails() + problem_obj.status = '500' + problem_obj.detail = 'test_err' + changed_info = objects.vnf_lcm_op_occs.VnfInfoModifications( + context=self.context, **fakes.get_changed_info_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.save() + + self.assertEqual('FAILED_TEMP', vnf_lcm_op_occs.operation_state) + self.assertEqual(problem_obj, vnf_lcm_op_occs.error) diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index caa66fd..6b890c5 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -15,6 +15,7 @@ from unittest import mock import ddt + from oslo_serialization import jsonutils from six.moves import http_client import urllib @@ -23,11 +24,15 @@ from webob import exc from tacker.api.vnflcm.v1 import controller from tacker.common import exceptions 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.manager import TackerManager from tacker import objects from tacker.objects import fields from tacker.tests import constants from tacker.tests.unit import base +from tacker.tests.unit.db import utils from tacker.tests.unit import fake_request import tacker.tests.unit.nfvo.test_nfvo_plugin as nfvo_plugin from tacker.tests.unit.vnflcm import fakes @@ -72,6 +77,13 @@ class TestController(base.TestCase): 'placement_attr': {'region': 'TestRegionOne'}, 'tenant': 'test' } + self.context = context.get_admin_context() + + with mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, 'get_vnfs', + return_value=[]): + with mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}): + self.controller = controller.VnfLcmController() def tearDown(self): self.mock_manager.stop() @@ -81,6 +93,17 @@ class TestController(base.TestCase): def app(self): return fakes.wsgi_app_v1() + def _get_dummy_vnf(self, vnf_id=None, status=None): + vnf_dict = utils.get_dummy_vnf() + + if status: + vnf_dict['status'] = status + + if vnf_id: + vnf_dict['id'] = vnf_id + + return vnf_dict + @mock.patch.object(objects.VnfInstance, 'save') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.vnf_package.VnfPackage, 'get_by_id') @@ -125,7 +148,7 @@ class TestController(base.TestCase): instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED, **updates) location_header = ('http://localhost/vnflcm/v1/vnf_instances/%s' - % resp.json['id']) + % resp.json['id']) self.assertEqual(expected_vnf, resp.json) self.assertEqual(location_header, resp.headers['location']) @@ -171,12 +194,10 @@ class TestController(base.TestCase): self.assertEqual(http_client.CREATED, resp.status_code) updates = {"vnfInstanceName": "SampleVnf", - "vnfInstanceDescription": "SampleVnf Description"} - expected_vnf = fakes.fake_vnf_instance_response( - instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED, - **updates) + "vnfInstanceDescription": "SampleVnf Description"} + expected_vnf = fakes.fake_vnf_instance_response(**updates) location_header = ('http://localhost/vnflcm/v1/vnf_instances/%s' - % resp.json['id']) + % resp.json['id']) self.assertEqual(expected_vnf, resp.json) self.assertEqual(location_header, resp.headers['location']) @@ -265,12 +286,12 @@ class TestController(base.TestCase): expected_message = ("Invalid input for field/attribute " "{attribute}. Value: {value}. {value} is not " "of type 'string'". - format(value=value, attribute=attribute)) + format(value=value, attribute=attribute)) elif expected_type in ["name_allow_zero_min_length", "description"]: expected_message = ("Invalid input for field/attribute " "{attribute}. " "Value: {value}. {value} is " "not of type 'string'". - format(value=value, attribute=attribute)) + format(value=value, attribute=attribute)) elif expected_type == 'object': expected_message = ("Invalid input for field/attribute " "{attribute}. " "Value: {value}. {value} is " @@ -338,6 +359,12 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.VnfInstance, "save") @@ -347,7 +374,9 @@ class TestController(base.TestCase): def test_instantiate_with_deployment_flavour( self, mock_instantiate, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, mock_save, - mock_vnf_instance_get_by_id, mock_get_vim): + mock_vnf_instance_get_by_id, mock_get_vim, + mock_get_vnf, mock_insta_notfi_process, + mock_get_service_plugins): mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() @@ -355,6 +384,10 @@ class TestController(base.TestCase): fakes.return_vnf_package_vnfd() mock_vnf_package_get_by_id.return_value = \ fakes.return_vnf_package_with_deployment_flavour() + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') body = {"flavourId": "simple"} req = fake_request.HTTPRequest.blank( @@ -368,12 +401,18 @@ class TestController(base.TestCase): self.assertEqual(http_client.ACCEPTED, resp.status_code) mock_instantiate.assert_called_once() + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfPackage, "get_by_id") def test_instantiate_with_non_existing_deployment_flavour( - self, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, - mock_vnf_instance_get_by_id): + self, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, + mock_vnf_instance_get_by_id, mock_get_vnf, + mock_get_service_plugins): mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() @@ -381,6 +420,10 @@ class TestController(base.TestCase): fakes.return_vnf_package_vnfd() mock_vnf_package_get_by_id.return_value = \ fakes.return_vnf_package_with_deployment_flavour() + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') body = {"flavourId": "invalid"} req = fake_request.HTTPRequest.blank( @@ -393,8 +436,15 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.BAD_REQUEST, resp.status_code) self.assertEqual("No flavour with id 'invalid'.", - resp.json['badRequest']['message']) - + resp.json['badRequest']['message']) + mock_get_vnf.assert_called_once() + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.VnfInstance, "save") @@ -404,7 +454,9 @@ class TestController(base.TestCase): def test_instantiate_with_instantiation_level( self, mock_instantiate, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, mock_save, - mock_vnf_instance_get_by_id, mock_get_vim): + mock_vnf_instance_get_by_id, mock_get_vim, + mock_get_vnf, mock_insta_notif_process, + mock_get_service_plugins): mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() @@ -412,6 +464,10 @@ class TestController(base.TestCase): fakes.return_vnf_package_vnfd() mock_vnf_package_get_by_id.return_value = \ fakes.return_vnf_package_with_deployment_flavour() + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') body = {"flavourId": "simple", "instantiationLevelId": "instantiation_level_1"} @@ -425,7 +481,13 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.ACCEPTED, resp.status_code) mock_instantiate.assert_called_once() + mock_get_vnf.assert_called_once() + mock_insta_notif_process.assert_called_once() + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.VnfInstance, "save") @@ -435,7 +497,8 @@ class TestController(base.TestCase): def test_instantiate_with_no_inst_level_in_flavour( self, mock_instantiate, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, mock_save, - mock_vnf_instance_get_by_id, mock_get_vim): + mock_vnf_instance_get_by_id, mock_get_vim, + mock_get_vnf, mock_get_service_plugins): mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() @@ -444,6 +507,10 @@ class TestController(base.TestCase): vnf_package = fakes.return_vnf_package_with_deployment_flavour() vnf_package.vnf_deployment_flavours[0].instantiation_levels = None mock_vnf_package_get_by_id.return_value = vnf_package + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') # No instantiation level in deployment flavour but it's passed in the # request @@ -459,8 +526,14 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.BAD_REQUEST, resp.status_code) self.assertEqual("No instantiation level with id " - "'instantiation_level_1'.", resp.json['badRequest']['message']) + "'instantiation_level_1'.", + resp.json['badRequest']['message']) + mock_get_vnf.assert_called_once() + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfPackage, "get_by_id") @@ -468,7 +541,8 @@ class TestController(base.TestCase): def test_instantiate_with_non_existing_instantiation_level( self, mock_instantiate, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, - mock_vnf_instance_get_by_id): + mock_vnf_instance_get_by_id, mock_get_vnf, + mock_get_service_plugins): mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() @@ -476,6 +550,10 @@ class TestController(base.TestCase): fakes.return_vnf_package_vnfd() mock_vnf_package_get_by_id.return_value = \ fakes.return_vnf_package_with_deployment_flavour() + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') body = {"flavourId": "simple", "instantiationLevelId": "non-existing"} @@ -489,8 +567,15 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.BAD_REQUEST, resp.status_code) self.assertEqual("No instantiation level with id 'non-existing'.", - resp.json['badRequest']['message']) - + resp.json['badRequest']['message']) + mock_get_vnf.assert_called_once() + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.VnfLcmController.' + '_notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.VnfInstance, "save") @@ -500,7 +585,9 @@ class TestController(base.TestCase): def test_instantiate_with_vim_connection( self, mock_instantiate, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, mock_save, - mock_vnf_instance_get_by_id, mock_get_vim): + mock_vnf_instance_get_by_id, mock_get_vim, + mock_get_vnf, mock_insta_notif_process, + mock_get_service_plugins): mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() @@ -508,6 +595,10 @@ class TestController(base.TestCase): fakes.return_vnf_package_vnfd() mock_vnf_package_get_by_id.return_value = \ fakes.return_vnf_package_with_deployment_flavour() + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') body = {"flavourId": "simple", "vimConnectionInfo": [ @@ -526,14 +617,22 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.ACCEPTED, resp.status_code) mock_instantiate.assert_called_once() + mock_get_vnf.assert_called_once() + mock_insta_notif_process.assert_called_once() + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfPackage, "get_by_id") def test_instantiate_with_non_existing_vim( - self, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, - mock_vnf_instance_get_by_id, mock_get_vim): + self, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, + mock_vnf_instance_get_by_id, mock_get_vim, + mock_get_vnf, mock_get_service_plugins): mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() @@ -542,6 +641,10 @@ class TestController(base.TestCase): mock_vnf_package_get_by_id.return_value = \ fakes.return_vnf_package_with_deployment_flavour() mock_get_vim.side_effect = nfvo.VimNotFoundException + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') body = {"flavourId": "simple", "vimConnectionInfo": [ @@ -559,15 +662,23 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.BAD_REQUEST, resp.status_code) self.assertEqual("VimConnection id is not found: %s" % - uuidsentinel.vim_id, resp.json['badRequest']['message']) + uuidsentinel.vim_id, + resp.json['badRequest']['message']) + mock_get_vnf.assert_called_once() + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfPackage, "get_by_id") def test_instantiate_with_non_existing_region_vim( - self, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, - mock_vnf_instance_get_by_id, mock_get_vim): + self, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, + mock_vnf_instance_get_by_id, mock_get_vim, + mock_get_vnf, mock_get_service_plugins): mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() @@ -576,6 +687,10 @@ class TestController(base.TestCase): mock_vnf_package_get_by_id.return_value = \ fakes.return_vnf_package_with_deployment_flavour() mock_get_vim.side_effect = nfvo.VimRegionNotFoundException + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') body = {"flavourId": "simple", "vimConnectionInfo": [ @@ -594,15 +709,23 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.BAD_REQUEST, resp.status_code) self.assertEqual("Region not found for the VimConnection: %s" % - uuidsentinel.vim_id, resp.json['badRequest']['message']) + uuidsentinel.vim_id, + resp.json['badRequest']['message']) + mock_get_vnf.assert_called_once() + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfPackage, "get_by_id") def test_instantiate_with_default_vim_not_configured( - self, mock_vnf_package_get_by_id, mock_vnf_package_vnfd_get_by_id, - mock_vnf_instance_get_by_id, mock_get_vim): + self, mock_vnf_package_get_by_id, + mock_vnf_package_vnfd_get_by_id, + mock_vnf_instance_get_by_id, mock_get_vim, + mock_get_vnf, mock_get_service_plugins): mock_vnf_instance_get_by_id.return_value =\ fakes.return_vnf_instance_model() @@ -611,6 +734,10 @@ class TestController(base.TestCase): mock_vnf_package_get_by_id.return_value = \ fakes.return_vnf_package_with_deployment_flavour() mock_get_vim.side_effect = nfvo.VimDefaultNotDefined + mock_get_vnf.return_value = \ + self._get_dummy_vnf( + vnf_id=mock_vnf_instance_get_by_id.return_value.id, + status='INACTIVE') body = {"flavourId": "simple"} req = fake_request.HTTPRequest.blank( @@ -623,10 +750,16 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.BAD_REQUEST, resp.status_code) self.assertEqual("Default VIM is not defined.", - resp.json['badRequest']['message']) + resp.json['badRequest']['message']) + mock_get_vnf.assert_called_once() + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") - def test_instantiate_incorrect_instantiation_state(self, mock_vnf_by_id): + def test_instantiate_incorrect_instantiation_state(self, mock_vnf_by_id, + mock_get_vnf, mock_get_service_plugins): vnf_instance = fakes.return_vnf_instance_model() vnf_instance.instantiation_state = 'INSTANTIATED' mock_vnf_by_id.return_value = vnf_instance @@ -642,8 +775,13 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.CONFLICT, resp.status_code) + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") - def test_instantiate_incorrect_task_state(self, mock_vnf_by_id): + def test_instantiate_incorrect_task_state(self, mock_vnf_by_id, + mock_get_vnf, mock_get_service_plugins): vnf_instance = fakes.return_vnf_instance_model( task_state=fields.VnfInstanceTaskState.INSTANTIATING) mock_vnf_by_id.return_value = vnf_instance @@ -661,7 +799,7 @@ class TestController(base.TestCase): expected_msg = ("Vnf instance %s in task_state INSTANTIATING. Cannot " "instantiate while the vnf instance is in this state.") self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, - resp.json['conflictingRequest']['message']) + resp.json['conflictingRequest']['message']) @ddt.data({'attribute': 'flavourId', 'value': 123, 'expected_type': 'string'}, @@ -713,7 +851,7 @@ class TestController(base.TestCase): self.assertEqual(http_client.BAD_REQUEST, resp.status_code) self.assertEqual("'flavourId' is a required property", - resp.json['badRequest']['message']) + resp.json['badRequest']['message']) def test_instantiate_invalid_request_parameter(self): body = {"flavourId": "simple"} @@ -749,12 +887,17 @@ class TestController(base.TestCase): self.assertEqual(http_client.NOT_FOUND, resp.status_code) self.assertEqual( - "Can not find requested vnf instance: %s" % constants.INVALID_UUID, + "Can not find requested vnf: %s" % constants.INVALID_UUID, resp.json['itemNotFound']['message']) + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.VnfInstance, "get_by_id") def test_instantiate_with_non_existing_vnf_instance( - self, mock_vnf_by_id): + self, mock_vnf_by_id, mock_get_vnf, + mock_get_service_plugins): mock_vnf_by_id.side_effect = exceptions.VnfInstanceNotFound body = {"flavourId": "simple"} req = fake_request.HTTPRequest.blank( @@ -770,6 +913,7 @@ class TestController(base.TestCase): self.assertEqual("Can not find requested vnf instance: %s" % uuidsentinel.vnf_instance_id, resp.json['itemNotFound']['message']) + mock_get_vnf.assert_called_once() @ddt.data('HEAD', 'PUT', 'DELETE', 'PATCH', 'GET') def test_instantiate_invalid_http_method(self, method): @@ -813,17 +957,21 @@ class TestController(base.TestCase): self.assertEqual(http_client.NOT_FOUND, resp.status_code) self.assertEqual("Can not find requested vnf instance: %s" % - uuidsentinel.vnf_instance_id, - resp.json['itemNotFound']['message']) + uuidsentinel.vnf_instance_id, + resp.json['itemNotFound']['message']) - def test_show_with_invalid_uuid(self): + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + def test_show_with_invalid_uuid(self, + mock_get_service_plugins): req = fake_request.HTTPRequest.blank( '/vnf_instances/%s' % constants.INVALID_UUID) resp = req.get_response(self.app) self.assertEqual(http_client.NOT_FOUND, resp.status_code) self.assertEqual("Can not find requested vnf instance: %s" % - constants.INVALID_UUID, resp.json['itemNotFound']['message']) + constants.INVALID_UUID, + resp.json['itemNotFound']['message']) @ddt.data('PATCH', 'HEAD', 'PUT', 'POST') def test_show_invalid_http_method(self, http_method): @@ -835,6 +983,12 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.METHOD_NOT_ALLOWED, resp.status_code) + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.VnfInstance, "get_by_id") @mock.patch.object(objects.VnfInstance, "save") @mock.patch.object(VNFLcmRPCAPI, "terminate") @@ -842,10 +996,15 @@ class TestController(base.TestCase): {'terminationType': 'GRACEFUL'}, {'terminationType': 'GRACEFUL', 'gracefulTerminationTimeout': 10}) - def test_terminate(self, body, mock_terminate, mock_save, mock_get_by_id): + def test_terminate(self, body, mock_terminate, mock_save, + mock_get_by_id, mock_get_vnf, + mock_notification_process, + mock_get_service_plugins): vnf_instance_obj = fakes.return_vnf_instance( fields.VnfInstanceState.INSTANTIATED) mock_get_by_id.return_value = vnf_instance_obj + mock_get_vnf.return_value = \ + self._get_dummy_vnf(vnf_id=vnf_instance_obj.id, status='ACTIVE') req = fake_request.HTTPRequest.blank( '/vnf_instances/%s/terminate' % uuidsentinel.vnf_instance_id) @@ -856,7 +1015,11 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.ACCEPTED, resp.status_code) mock_terminate.assert_called_once() + mock_get_vnf.assert_called_once() + mock_notification_process.assert_called_once() + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) @ddt.data( {'attribute': 'terminationType', 'value': "TEST", 'expected_type': 'enum'}, @@ -869,9 +1032,10 @@ class TestController(base.TestCase): {'attribute': 'gracefulTerminationTimeout', 'value': "test", 'expected_type': 'integer'} ) - @ddt.unpack def test_terminate_with_invalid_request_body( - self, attribute, value, expected_type): + self, values, mock_get_service_plugins): + attribute = values['attribute'] + value = values['value'] req = fake_request.HTTPRequest.blank( '/vnf_instances/%s/terminate' % uuidsentinel.vnf_instance_id) body = {'terminationType': 'GRACEFUL', @@ -882,7 +1046,8 @@ class TestController(base.TestCase): req.method = 'POST' expected_message = ("Invalid input for field/attribute {attribute}. " - "Value: {value}.".format(value=value, attribute=attribute)) + "Value: {value}.". + format(value=value, attribute=attribute)) exception = self.assertRaises(exceptions.ValidationError, self.controller.terminate, @@ -901,7 +1066,7 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.BAD_REQUEST, resp.status_code) self.assertEqual("'terminationType' is a required property", - resp.json['badRequest']['message']) + resp.json['badRequest']['message']) @ddt.data('GET', 'HEAD', 'PUT', 'DELETE', 'PATCH') def test_terminate_invalid_http_method(self, method): @@ -916,8 +1081,13 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.METHOD_NOT_ALLOWED, resp.status_code) + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") - def test_terminate_non_existing_vnf_instance(self, mock_vnf_by_id): + def test_terminate_non_existing_vnf_instance(self, mock_vnf_by_id, + mock_get_vnf, mock_get_service_plugins): body = {'terminationType': 'GRACEFUL', 'gracefulTerminationTimeout': 10} mock_vnf_by_id.side_effect = exceptions.VnfInstanceNotFound @@ -931,11 +1101,17 @@ class TestController(base.TestCase): self.assertEqual(http_client.NOT_FOUND, resp.status_code) self.assertEqual("Can not find requested vnf instance: %s" % - uuidsentinel.vnf_instance_id, - resp.json['itemNotFound']['message']) + uuidsentinel.vnf_instance_id, + resp.json['itemNotFound']['message']) + mock_get_vnf.assert_called_once() + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") - def test_terminate_incorrect_instantiation_state(self, mock_vnf_by_id): + def test_terminate_incorrect_instantiation_state(self, mock_vnf_by_id, + mock_get_vnf, mock_get_service_plugins): mock_vnf_by_id.return_value = fakes.return_vnf_instance() body = {"terminationType": "FORCEFUL"} req = fake_request.HTTPRequest.blank( @@ -951,10 +1127,16 @@ class TestController(base.TestCase): "NOT_INSTANTIATED. Cannot terminate while the vnf " "instance is in this state.") self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, - resp.json['conflictingRequest']['message']) + resp.json['conflictingRequest']['message']) + mock_get_vnf.assert_called_once() + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.VnfInstance, "get_by_id") - def test_terminate_incorrect_task_state(self, mock_vnf_by_id): + def test_terminate_incorrect_task_state(self, mock_vnf_by_id, + mock_get_vnf, mock_get_service_plugins): vnf_instance = fakes.return_vnf_instance( instantiated_state=fields.VnfInstanceState.INSTANTIATED, task_state=fields.VnfInstanceTaskState.TERMINATING) @@ -973,17 +1155,28 @@ class TestController(base.TestCase): expected_msg = ("Vnf instance %s in task_state TERMINATING. Cannot " "terminate while the vnf instance is in this state.") self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, - resp.json['conflictingRequest']['message']) - + resp.json['conflictingRequest']['message']) + mock_get_vnf.assert_called_once() + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.VnfInstance, "get_by_id") @mock.patch.object(objects.VnfInstance, "save") @mock.patch.object(VNFLcmRPCAPI, "heal") @ddt.data({'cause': 'healing'}, {}) def test_heal(self, body, mock_rpc_heal, mock_save, - mock_vnf_by_id): + mock_vnf_by_id, mock_get_vnf, + mock_heal_notif_process, + mock_get_service_plugins): vnf_instance_obj = fakes.return_vnf_instance( fields.VnfInstanceState.INSTANTIATED) mock_vnf_by_id.return_value = vnf_instance_obj + mock_get_vnf.return_value = \ + self._get_dummy_vnf(vnf_id=vnf_instance_obj.id, status='ACTIVE') req = fake_request.HTTPRequest.blank( '/vnf_instances/%s/heal' % uuidsentinel.vnf_instance_id) @@ -1007,8 +1200,15 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.VnfInstance, "get_by_id") - def test_heal_incorrect_instantiated_state(self, mock_vnf_by_id): + def test_heal_incorrect_instantiated_state(self, mock_vnf_by_id, + mock_get_vnf, mock_notif, mock_get_service_plugins): vnf_instance_obj = fakes.return_vnf_instance( fields.VnfInstanceState.NOT_INSTANTIATED) mock_vnf_by_id.return_value = vnf_instance_obj @@ -1023,13 +1223,20 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.CONFLICT, resp.status_code) expected_msg = ("Vnf instance %s in instantiation_state " - "NOT_INSTANTIATED. Cannot heal while the vnf instance " - "is in this state.") + "NOT_INSTANTIATED. Cannot heal while the vnf instance " + "is in this state.") self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, - resp.json['conflictingRequest']['message']) - + resp.json['conflictingRequest']['message']) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.VnfInstance, "get_by_id") - def test_heal_incorrect_task_state(self, mock_vnf_by_id): + def test_heal_incorrect_task_state(self, mock_vnf_by_id, mock_get_vnf, + mock_notif, mock_get_service_plugins): vnf_instance_obj = fakes.return_vnf_instance( fields.VnfInstanceState.INSTANTIATED, task_state=fields.VnfInstanceTaskState.HEALING) @@ -1045,16 +1252,25 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.CONFLICT, resp.status_code) expected_msg = ("Vnf instance %s in task_state " - "HEALING. Cannot heal while the vnf instance " - "is in this state.") + "HEALING. Cannot heal while the vnf instance " + "is in this state.") self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, - resp.json['conflictingRequest']['message']) - + resp.json['conflictingRequest']['message']) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._notification_process') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._get_vnf') @mock.patch.object(objects.VnfInstance, "get_by_id") - def test_heal_with_invalid_vnfc_id(self, mock_vnf_by_id): + def test_heal_with_invalid_vnfc_id(self, mock_vnf_by_id, + mock_get_vnf, mock_notif, mock_get_service_plugins): vnf_instance_obj = fakes.return_vnf_instance( fields.VnfInstanceState.INSTANTIATED) mock_vnf_by_id.return_value = vnf_instance_obj + mock_get_vnf.return_value = \ + self._get_dummy_vnf(vnf_id=vnf_instance_obj.id, status='ACTIVE') body = {'vnfcInstanceId': [uuidsentinel.vnfc_instance_id]} req = fake_request.HTTPRequest.blank( @@ -1067,7 +1283,8 @@ class TestController(base.TestCase): self.assertEqual(http_client.BAD_REQUEST, resp.status_code) expected_msg = "Vnfc id %s not present in vnf instance %s" self.assertEqual(expected_msg % (uuidsentinel.vnfc_instance_id, - uuidsentinel.vnf_instance_id), resp.json['badRequest']['message']) + uuidsentinel.vnf_instance_id), + resp.json['badRequest']['message']) @ddt.data('HEAD', 'PUT', 'DELETE', 'PATCH', 'GET') def test_heal_invalid_http_method(self, method): @@ -1171,8 +1388,8 @@ class TestController(base.TestCase): self.assertEqual(http_client.NOT_FOUND, resp.status_code) self.assertEqual("Can not find requested vnf instance: %s" % - uuidsentinel.vnf_instance_id, - resp.json['itemNotFound']['message']) + uuidsentinel.vnf_instance_id, + resp.json['itemNotFound']['message']) def test_delete_with_invalid_uuid(self): req = fake_request.HTTPRequest.blank( @@ -1184,8 +1401,8 @@ class TestController(base.TestCase): self.assertEqual(http_client.NOT_FOUND, resp.status_code) self.assertEqual("Can not find requested vnf instance: %s" % - constants.INVALID_UUID, - resp.json['itemNotFound']['message']) + constants.INVALID_UUID, + resp.json['itemNotFound']['message']) @mock.patch.object(objects.VnfInstance, "get_by_id") def test_delete_with_incorrect_instantiation_state(self, mock_vnf_by_id): @@ -1202,10 +1419,10 @@ class TestController(base.TestCase): self.assertEqual(http_client.CONFLICT, resp.status_code) expected_msg = ("Vnf instance %s in instantiation_state " - "INSTANTIATED. Cannot delete while the vnf instance " - "is in this state.") + "INSTANTIATED. Cannot delete while the vnf instance " + "is in this state.") self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, - resp.json['conflictingRequest']['message']) + resp.json['conflictingRequest']['message']) @mock.patch.object(objects.VnfInstance, "get_by_id") def test_delete_with_incorrect_task_state(self, mock_vnf_by_id): @@ -1223,10 +1440,10 @@ class TestController(base.TestCase): self.assertEqual(http_client.CONFLICT, resp.status_code) expected_msg = ("Vnf instance %s in task_state ERROR. " - "Cannot delete while the vnf instance " - "is in this state.") + "Cannot delete while the vnf instance " + "is in this state.") self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, - resp.json['conflictingRequest']['message']) + resp.json['conflictingRequest']['message']) @mock.patch.object(objects.VnfInstanceList, "get_by_filters") @ddt.data( diff --git a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py index e0890cc..f671443 100644 --- a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py +++ b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py @@ -12,13 +12,13 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. + +import fixtures import os import shutil from unittest import mock -import fixtures from oslo_config import cfg - from tacker.common import exceptions from tacker.common import utils from tacker import context @@ -32,6 +32,14 @@ from tacker.vnflcm import vnflcm_driver from tacker.vnfm import vim_client +OPTS_INFRA_DRIVER = [ + cfg.ListOpt( + 'infra_driver', default=['noop', 'openstack', 'kubernetes'], + help=_('Hosting vnf drivers tacker plugin will use')), +] +cfg.CONF.register_opts(OPTS_INFRA_DRIVER, 'tacker') + + class InfraDriverException(Exception): pass @@ -256,7 +264,8 @@ class TestVnflcmDriver(db_base.SqlTestCase): @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfInstance, "save") def test_instantiate_vnf_infra_fails_to_wait_after_instantiate( - self, mock_vnf_instance_save, mock_vnf_package_vnfd, mock_create): + self, mock_vnf_instance_save, mock_vnf_package_vnfd, + mock_create): vnf_package_vnfd = fakes.return_vnf_package_vnfd() vnf_package_id = vnf_package_vnfd.package_uuid mock_vnf_package_vnfd.return_value = vnf_package_vnfd diff --git a/tacker/vnflcm/utils.py b/tacker/vnflcm/utils.py index 220aa80..3dbfa9a 100644 --- a/tacker/vnflcm/utils.py +++ b/tacker/vnflcm/utils.py @@ -76,6 +76,211 @@ def _get_vnfd_dict(context, vnfd_id, flavour_id): return vnfd_dict +def _build_affected_resources(vnf_instance, + change_type=fields.ResourceChangeType.ADDED): + '''build affected resources from vnf_instance instantiated info ''' + + affected_resources = {} + instantiated_vnf_info = vnf_instance.instantiated_vnf_info + if hasattr(instantiated_vnf_info, 'instance_id'): + if instantiated_vnf_info.instance_id: + + affected_resources['affectedVnfcs'] = [] + affected_resources['affectedVirtualLinks'] = [] + affected_resources['affectedVirtualStorages'] = [] + + # build AffectedVnfc + vnfc_resource_info = \ + instantiated_vnf_info.vnfc_resource_info + for vnfc_resource in vnfc_resource_info: + data = {} + data['id'] = vnfc_resource.id + data['vduId'] = vnfc_resource.vdu_id + data['changeType'] = change_type + data['computeResource'] = \ + vnfc_resource.compute_resource.to_dict() + data['metadata'] = vnfc_resource.metadata + affected_resources['affectedVnfcs'].append(data) + + # build AffectedVirtualLink + vnf_virtual_link = \ + instantiated_vnf_info.vnf_virtual_link_resource_info + for vnf_vl_info in vnf_virtual_link: + data = {} + data['id'] = vnf_vl_info.id + data['vnfVirtualLinkDescId'] = \ + vnf_vl_info.vnf_virtual_link_desc_id + data['changeType'] = change_type + data['networkResource'] = \ + vnf_vl_info.network_resource.to_dict() + data['metadata'] = {} + affected_resources['affectedVirtualLinks'].append(data) + + # build affectedVirtualStorages + virtual_storage = \ + instantiated_vnf_info.virtual_storage_resource_info + for vnf_storage_info in virtual_storage: + data = {} + data['id'] = vnf_storage_info.id + data['virtualStorageDescId'] = \ + vnf_storage_info.virtual_storage_desc_id + data['changeType'] = change_type + data['storageResource'] = \ + vnf_storage_info.storage_resource.to_dict() + data['metadata'] = {} + affected_resources['affectedVirtualStorages'].append(data) + + return utils.convert_snakecase_to_camelcase(affected_resources) + + +def _get_affected_resources(old_vnf_instance=None, + new_vnf_instance=None, extra_list=None): + '''get_affected_resources + + returns affected resources in new_vnf_instance not present + in old_vnf_instance. + if extra_list (list of physical resource ids) is present, + included to affected resources + ''' + def _get_affected_cpids(affected_vnfc, vnf_instance): + affected_cpids = [] + instantiated_vnf_info = vnf_instance.instantiated_vnf_info + for vnfc_resource in instantiated_vnf_info.vnfc_resource_info: + if vnfc_resource.id == affected_vnfc['id']: + for vnfc_cp in vnfc_resource.vnfc_cp_info: + if vnfc_cp.cpd_id: + affected_cpids.append(vnfc_cp.cpd_id) + if vnfc_cp.vnf_ext_cp_id: + affected_cpids.append(vnfc_cp.vnf_ext_cp_id) + return affected_cpids + + def _get_added_storageids(affected_vnfc, vnf_instance): + affected_storage_ids = [] + instantiated_vnf_info = vnf_instance.instantiated_vnf_info + for vnfc_resource in instantiated_vnf_info.vnfc_resource_info: + if vnfc_resource.id == affected_vnfc['id']: + for storage_resource_id in vnfc_resource.storage_resource_ids: + virtual_storage = \ + instantiated_vnf_info.virtual_storage_resource_info + for virt_storage_res_info in virtual_storage: + if virt_storage_res_info.id == storage_resource_id: + affected_storage_ids.append( + virt_storage_res_info.virtual_storage_desc_id) + return affected_storage_ids + + def diff_list(old_list, new_list): + diff = [] + for item in new_list: + if item not in old_list: + diff.append(item) + return diff + + affected_resources = {} + affected_resources['affectedVnfcs'] = [] + affected_resources['affectedVirtualLinks'] = [] + affected_resources['affectedVirtualStorages'] = [] + + if not old_vnf_instance: + affected_resources = _build_affected_resources( + new_vnf_instance, fields.ResourceChangeType.ADDED) + # add affected cpids and add added storageids + for affected_vnfc in affected_resources['affectedVnfcs']: + affected_vnfc['affectedVnfcCpIds'] = _get_affected_cpids( + affected_vnfc, new_vnf_instance) + affected_vnfc['addedStorageResourceIds'] = _get_added_storageids( + affected_vnfc, new_vnf_instance) + + elif not new_vnf_instance: + affected_resources = _build_affected_resources(old_vnf_instance, + fields.ResourceChangeType.REMOVED) + # add affected cpids and add remove storageids + for affected_vnfc in affected_resources['affectedVnfcs']: + affected_vnfc['affectedVnfcCpIds'] = _get_affected_cpids( + affected_vnfc, old_vnf_instance) + affected_vnfc['removedStorageResourceIds'] = _get_added_storageids( + affected_vnfc, old_vnf_instance) + elif old_vnf_instance and new_vnf_instance: + old_affected_resources = _build_affected_resources(old_vnf_instance) + new_affected_resources = _build_affected_resources(new_vnf_instance, + fields.ResourceChangeType.MODIFIED) + + # get resource_ids + old_vnfc_resource_ids = [] + for vnfc_resource in old_affected_resources.get('affectedVnfcs', []): + old_vnfc_resource_ids.append( + vnfc_resource['computeResource']['resourceId']) + + # remove extra_list items in old_vnfc_resource_ids + # so that this items will be considered new + if extra_list: + for item in extra_list: + if item in old_vnfc_resource_ids: + index = old_vnfc_resource_ids.index(item) + old_vnfc_resource_ids.pop(index) + + new_vnfc_resource_ids = [] + for vnfc_resource in new_affected_resources.get('affectedVnfcs', []): + resource_id = vnfc_resource['computeResource']['resourceId'] + new_vnfc_resource_ids.append(resource_id) + + old_vnf_vl_resource_ids = [] + for vnf_vl_info in old_affected_resources.get( + 'affectedVirtualLinks', []): + resource_id = vnf_vl_info['networkResource']['resourceId'] + old_vnf_vl_resource_ids.append(resource_id) + + new_vnf_vl_resource_ids = [] + for vnf_vl_info in new_affected_resources.get( + 'affectedVirtualLinks', []): + resource_id = vnf_vl_info['networkResource']['resourceId'] + new_vnf_vl_resource_ids.append(resource_id) + + old_vnf_storage_resource_ids = [] + for vnf_storage_info in old_affected_resources.get( + 'affectedVirtualStorages', []): + resource_id = vnf_storage_info['storageResource']['resourceId'] + old_vnf_storage_resource_ids.append(resource_id) + + new_vnf_storage_resource_ids = [] + for vnf_storage_info in new_affected_resources.get( + 'affectedVirtualStorages', []): + resource_id = vnf_storage_info['storageResource']['resourceId'] + new_vnf_storage_resource_ids.append(resource_id) + + # get difference between resource_ids + vnfc_resource_ids = diff_list(old_vnfc_resource_ids, + new_vnfc_resource_ids) + vnf_vl_resource_ids = diff_list(old_vnf_vl_resource_ids, + new_vnf_vl_resource_ids) + vnf_storage_resource_ids = diff_list(old_vnf_storage_resource_ids, + new_vnf_storage_resource_ids) + + # return new affected resources + for affected_vls in new_affected_resources['affectedVirtualLinks']: + if (affected_vls['networkResource'] + ['resourceId'] in vnf_vl_resource_ids): + affected_resources['affectedVirtualLinks'].append(affected_vls) + + affected_storages = new_affected_resources['affectedVirtualStorages'] + for affected_storage in affected_storages: + if (affected_storage['storageResource'] + ['resourceId'] in vnf_storage_resource_ids): + affected_resources['affectedVirtualStorages'].append( + affected_storage) + + for affected_vnfc in new_affected_resources['affectedVnfcs']: + if (affected_vnfc['computeResource'] + ['resourceId'] in vnfc_resource_ids): + + # update affected affectedVnfcCpIds + affected_vnfc['affectedVnfcCpIds'] = _get_affected_cpids( + affected_vnfc, new_vnf_instance) + + affected_resources['affectedVnfcs'].append(affected_vnfc) + + return affected_resources + + def _get_vnf_package_id(context, vnfd_id): vnf_package = objects.VnfPackageVnfd.get_by_id(context, vnfd_id) return vnf_package.package_uuid diff --git a/tacker/wsgi.py b/tacker/wsgi.py index 0dab677..cc54fb4 100644 --- a/tacker/wsgi.py +++ b/tacker/wsgi.py @@ -28,6 +28,8 @@ import time import eventlet.wsgi # eventlet.patcher.monkey_patch(all=False, socket=True, thread=True) from oslo_config import cfg +import tacker.conf + import oslo_i18n as i18n from oslo_log import log as logging from oslo_serialization import jsonutils @@ -75,7 +77,7 @@ socket_opts = [ "the server securely")), ] -CONF = cfg.CONF +CONF = tacker.conf.CONF CONF.register_opts(socket_opts)