commit 5baeeefedf00adb83fdab7c87ea6aa70237566bb Author: Aldinson Esto Date: Sat Aug 8 05:05:28 2020 +0900 Support Subscription for LCM notifications by ETSI Supported for Flow of managing subscriptions as defined in ETSI SOL003. Supported POST/DELETE/GET(List)/GET(Individual) LccnSubscription. * POST /vnflcm/{apiMajorVersion}/subscriptions 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: Ia97dfdc2ea7ed1b11d519ae62d07a37896a80a35 diff --git a/api-ref/source/v1/parameters_vnflcm.yaml b/api-ref/source/v1/parameters_vnflcm.yaml index 020c23f..806c943 100644 --- a/api-ref/source/v1/parameters_vnflcm.yaml +++ b/api-ref/source/v1/parameters_vnflcm.yaml @@ -1,4 +1,11 @@ # variables in header + +subscription_id: + description: | + Identifier of the subscription. + in: path + required: true + type: string vnf_instance_id: description: | Identifier of the VNF instance. @@ -7,6 +14,104 @@ vnf_instance_id: type: string # variables in body +authentication: + description: | + Authentication parameters to configure the use of + Authorization when sending notifications + corresponding to this subscription. + This attribute shall only be present if the subscriber + requires authorization of notifications. + in: body + required: false + type: object +authentication_auth_type: + description: | + Defines the types of Authentication/Authorization which + the API consumer is willing to accept when receiving a + notification. + Permitted values: + + BASIC: In every HTTP request to the + notification endpoint, use HTTP Basic + authentication with the client credentials. + + OAUTH2_CLIENT_CREDENTIALS: In every + HTTP request to the notification endpoint, use + an OAuth 2.0 bearer token, obtained using the + client credentials grant type. + + TLS_CERT: Every HTTP request to the + notification endpoint is sent over a mutually + authenticated TLS session, i.e. not only the + server is authenticated, but also the client is + authenticated during the TLS tunnel setup. + in: body + required: true + type: string +authentication_params_basic: + description: | + Parameters for authentication/authorization using BASIC. + Shall be present if authType is "BASIC" and the + contained information has not been provisioned out of + band. Shall be absent otherwise. + in: body + required: false + type: object +authentication_params_basic_password: + description: | + Password to be used in HTTP Basic authentication. + Shall be present if it has not been provisioned out of band. + in: body + required: false + type: string +authentication_params_basic_user_name: + description: | + Username to be used in HTTP Basic authentication. + Shall be present if it has not been provisioned out of band. + in: body + required: false + type: string +authentication_params_oauth2_client_credentials: + description: | + Parameters for authentication/authorization using + OAUTH2_CLIENT_CREDENTIALS. + Shall be present if authType is + "OAUTH2_CLIENT_CREDENTIALS" and the contained + information has not been provisioned out of band. + Shall be absent otherwise. + in: body + required: false + type: object +authentication_params_oauth2_client_credentials_client_id: + description: | + Client identifier to be used in the access token request + of the OAuth 2.0 client credentials grant type. Shall be + present if it has not been provisioned out of band. + in: body + required: false + type: string +authentication_params_oauth2_client_credentials_client_password: + description: | + Client password to be used in the access token request + of the OAuth 2.0 client credentials grant type. Shall be + present if it has not been provisioned out of band. + in: body + required: false + type: string +authentication_params_oauth2_client_credentials_token_endpoint: + description: | + The token endpoint from which the access token can be + obtained. Shall be present if it has not been provisioned + out of band. + in: body + required: false + type: string +callback_uri: + description: | + The URI of the endpoint to send the notification to. + in: body + required: true + type: string cause: description: | Indicates the reason why a healing procedure is required. @@ -222,6 +327,40 @@ ext_virtual_links_resource_id: in: body required: true type: string +filter: + description: | + Filter settings for this subscription, to define the + subset of all notifications this subscription relates + to. A particular notification is sent to the subscriber + if the filter matches, or if there is no filter. + in: body + required: false + type: object +filter_notification_types: + description: | + Match particular notification types. + Permitted values: + + VnfLcmOperationOccurrenceNotification + + VnfIdentifierCreationNotification + + VnfIdentifierDeletionNotification + in: body + required: false + type: string +filter_operation_types: + description: | + Match particular VNF lifecycle operation types for + the notification of type + VnfLcmOperationOccurrenceNotification. + May be present if the "notificationTypes" attribute + contains the value + "VnfLcmOperationOccurrenceNotification", and + shall be absent otherwise. + in: body + required: false + type: string fixed_addresses: description: | Fixed addresses to assign (from the subnet defined by "subnetId" @@ -408,6 +547,12 @@ subnet_id: in: body required: false type: string +subscription_id_response: + description: | + Identifier of this subscription resource. + in: body + required: true + type: string termination_type: description: | Indicates whether forceful or graceful termination is requested. diff --git a/api-ref/source/v1/samples/vnflcm/create-subscription-request.json b/api-ref/source/v1/samples/vnflcm/create-subscription-request.json new file mode 100644 index 0000000..bb7344b --- /dev/null +++ b/api-ref/source/v1/samples/vnflcm/create-subscription-request.json @@ -0,0 +1,8 @@ +{ + "filter": { + "notificationTypes": [ + "VnfLcmOperationOccurrenceNotification" + ] + }, + "callbackUri": "http://sample1.com/notification" +} \ No newline at end of file diff --git a/api-ref/source/v1/samples/vnflcm/create-subscription-response.json b/api-ref/source/v1/samples/vnflcm/create-subscription-response.json new file mode 100644 index 0000000..6cb6791 --- /dev/null +++ b/api-ref/source/v1/samples/vnflcm/create-subscription-response.json @@ -0,0 +1,14 @@ +{ + "id": "76057f8e65ab37fb82d9382dfc3f3c8b", + "filter": { + "notificationTypes": [ + "VnfLcmOperationOccurrenceNotification" + ] + }, + "callbackUri": "http://sample1.com/notification", + "_links": { + "self": { + "href": "https://sample1.com/vnflcm/v1/subscriptions/76057f8e65ab37fb82d9382dfc3f3c8b" + } + } +} diff --git a/api-ref/source/v1/samples/vnflcm/list-subscription-response.json b/api-ref/source/v1/samples/vnflcm/list-subscription-response.json new file mode 100644 index 0000000..c69efc6 --- /dev/null +++ b/api-ref/source/v1/samples/vnflcm/list-subscription-response.json @@ -0,0 +1,36 @@ +[ + { + "id": "76057f8e65ab37fb82d9382dfc3f3c8b", + "filter": { + "notificationTypes": [ + "VnfLcmOperationOccurrenceNotification" + ] + }, + "callbackUri": "http://sample1.com/notification", + "_links": { + "self": { + "href": "https://sample1.com/vnflcm/v1/subscriptions/76057f8e65ab37fb82d9382dfc3f3c8b" + } + } + }, + { + "id": "4845ac30eab62a0b0b4edc00fbb930ee", + "filter": { + "notificationTypes": [ + "VnfLcmOperationOccurrenceNotification", + "VnfIdentifierCreationNotification", + "VnfIdentifierDeletionNotification" + ], + "notificationTypes": [ + "SCALE", + "HEAL" + ] + }, + "callbackUri": "http://sample2.com/notification", + "_links": { + "self": { + "href": "https://sample2.com/vnflcm/v1/subscriptions/4845ac30eab62a0b0b4edc00fbb930ee" + } + } + } +] \ No newline at end of file diff --git a/api-ref/source/v1/samples/vnflcm/show-subscription-response.json b/api-ref/source/v1/samples/vnflcm/show-subscription-response.json new file mode 100644 index 0000000..b8e6df5 --- /dev/null +++ b/api-ref/source/v1/samples/vnflcm/show-subscription-response.json @@ -0,0 +1,14 @@ +{ + "id": "76057f8e65ab37fb82d9382dfc3f3c8b", + "filter": { + "notificationTypes": [ + "VnfLcmOperationOccurrenceNotification" + ] + }, + "callbackUri": "http://sample1.com/notification", + "_links": { + "self": { + "href": "https://sample1.com/vnflcm/v1/subscriptions/76057f8e65ab37fb82d9382dfc3f3c8b" + } + } +} \ No newline at end of file diff --git a/api-ref/source/v1/status.yaml b/api-ref/source/v1/status.yaml index 6341d06..cb55086 100644 --- a/api-ref/source/v1/status.yaml +++ b/api-ref/source/v1/status.yaml @@ -28,6 +28,11 @@ The response is about a redirection hint. The header of the response usually contains a 'location' value where requesters can check to track the real location of the resource. +303: + default: | + The server is redirecting the user agent to a different resource, as indicated + by a URI in the Location header field, which is intended to provide an indirect + response to the original request. ################# # Error Codes # diff --git a/api-ref/source/v1/vnflcm.inc b/api-ref/source/v1/vnflcm.inc index 418d719..55ae641 100644 --- a/api-ref/source/v1/vnflcm.inc +++ b/api-ref/source/v1/vnflcm.inc @@ -569,3 +569,192 @@ Response Example .. literalinclude:: samples/vnflcm/list-vnf-instance-response.json :language: javascript + +Create a new subscription +========================= + +.. rest_method:: POST /vnflcm/v1/subscriptions + +The POST method creates a new subscription. + +As the result of successfully executing this method, a new "Individual subscription" resource +shall have been created. This method shall not trigger any notification. + +Creation of two "Individual subscription" resources with the same callbackURI and the same filter can result in +performance degradation and will provide duplicates of notifications to the NFVO, and might make sense only in very +rare use cases. Consequently, the VNFM may either allow creating an "Individual subscription" resource if another +Individual subscription resource with the same filter and callbackUri already exists (in which case it shall return the +201 Created response code), or may decide to not create a duplicate "Individual subscription" resource (in which case +it shall return a "303 See Other" response code referencing the existing "Individual subscription" resource with the same +filter and callbackUri). + +Response Codes +-------------- + +.. rest_status_code:: success status.yaml + + - 201 + +.. rest_status_code:: error status.yaml + + - 303 + - 400 + - 401 + - 403 + +Request Parameters +------------------ + +.. rest_parameters:: parameters_vnflcm.yaml + + - filter: filter + - notificationTypes: filter_notification_types + - operationTypes: filter_operation_types + - callbackUri : callback_uri + - authentication: authentication + - authType: authentication_auth_type + - paramsBasic: authentication_params_basic + - userName: authentication_params_basic_user_name + - password: authentication_params_basic_password + - paramsOauth2ClientCredentials: authentication_params_oauth2_client_credentials + - clientId: authentication_params_oauth2_client_credentials_client_id + - clientPassword: authentication_params_oauth2_client_credentials_client_password + - tokenEndpoint: authentication_params_oauth2_client_credentials_token_endpoint + +Request Example +--------------- + +.. literalinclude:: samples/vnflcm/create-subscription-request.json + :language: javascript + +Response Parameters +------------------- + +.. rest_parameters:: parameters_vnflcm.yaml + + - id: subscription_id_response + - filter: filter + - notificationTypes: filter_notification_types + - operationTypes: filter_operation_types + - callbackUri: callback_uri + - _links: vnf_instance_links + +Response Example +---------------- + +.. literalinclude:: samples/vnflcm/create-subscription-response.json + :language: javascript + +Delete a subscription +========================= + +.. rest_method:: DELETE /vnflcm/v1/subscriptions/{subscriptionId} + +The DELETE method terminates an individual subscription. + +As the result of successfully executing this method, the "Individual subscription" resource shall not exist any longer. +This means that no notifications for that subscription shall be sent to the formerly-subscribed API consumer. + +Response Codes +-------------- + +.. rest_status_code:: success status.yaml + + - 204 + +.. rest_status_code:: error status.yaml + + - 401 + - 403 + - 404 + +Request Parameters +------------------ + +.. rest_parameters:: parameters_vnflcm.yaml + + - subscriptionId: subscription_id + +Show subscription +================= + +.. rest_method:: GET /vnflcm/v1/subscriptions/{subscriptionId} + +The GET method retrieves information about a subscription by reading an "Individual subscription" resource. + +Response Codes +-------------- + +.. rest_status_code:: success status.yaml + + - 200 + +.. rest_status_code:: error status.yaml + + - 401 + - 403 + - 404 + +Request Parameters +------------------ + +.. rest_parameters:: parameters_vnflcm.yaml + + - subscriptionId: subscription_id + +Response Parameters +------------------- + +.. rest_parameters:: parameters_vnflcm.yaml + + - id: subscription_id_response + - filter: filter + - notificationTypes: filter_notification_types + - operationTypes: filter_operation_types + - callbackUri: callback_uri + - _links: vnf_instance_links + +Response Example +---------------- + +.. literalinclude:: samples/vnflcm/show-subscription-response.json + :language: javascript + +List subscription +================= + +.. rest_method:: GET /vnflcm/v1/subscriptions + +The GET method queries the list of active subscriptions of the functional block that invokes the method. +It can be used e.g. for resynchronization after error situations. + +Response Codes +-------------- + +.. rest_status_code:: success status.yaml + + - 200 + +.. rest_status_code:: error status.yaml + + - 400 + - 401 + - 403 + +Response Parameters +------------------- + +.. rest_parameters:: parameters_vnflcm.yaml + + - id: subscription_id_response + - filter: filter + - notificationTypes: filter_notification_types + - operationTypes: filter_operation_types + - callbackUri: callback_uri + - _links: vnf_instance_links + +Response Example +---------------- + +.. literalinclude:: samples/vnflcm/list-subscription-response.json + :language: javascript diff --git a/tacker/api/schemas/vnf_lcm.py b/tacker/api/schemas/vnf_lcm.py index 72438e8..6deb2fb 100644 --- a/tacker/api/schemas/vnf_lcm.py +++ b/tacker/api/schemas/vnf_lcm.py @@ -227,3 +227,14 @@ heal = { }, 'additionalProperties': False, } + +register_subscription = { + 'type': 'object', + 'properties': { + 'filter': parameter_types.keyvalue_pairs, + 'callbackUri': {'type': 'string', 'maxLength': 255}, + 'authentication': parameter_types.keyvalue_pairs, + }, + 'required': ['callbackUri'], + 'additionalProperties': False, +} diff --git a/tacker/api/views/vnf_lcm.py b/tacker/api/views/vnf_lcm.py index 27de678..6649da6 100644 --- a/tacker/api/views/vnf_lcm.py +++ b/tacker/api/views/vnf_lcm.py @@ -13,11 +13,20 @@ # License for the specific language governing permissions and limitations # under the License. +import json + +from oslo_log import log as logging + from tacker.api import views as base from tacker.common import utils +import tacker.conf from tacker.objects import fields from tacker.objects import vnf_instance as _vnf_instance +CONF = tacker.conf.CONF + +LOG = logging.getLogger(__name__) + class ViewBuilder(base.BaseViewBuilder): @@ -108,3 +117,113 @@ class ViewBuilder(base.BaseViewBuilder): def index(self, vnf_instances): return [self._get_vnf_instance_info(vnf_instance) for vnf_instance in vnf_instances] + + def _get_subscription_links(self, vnf_lcm_subscription): + if isinstance(vnf_lcm_subscription.id, str): + decode_id = vnf_lcm_subscription.id + else: + decode_id = vnf_lcm_subscription.id.decode() + return { + "_links": { + "self": { + "href": '%(endpoint)s/vnflcm/v1/subscriptions/%(id)s' % + { + "endpoint": CONF.vnf_lcm.endpoint_url, + "id": decode_id}}}} + + def _basic_subscription_info(self, vnf_lcm_subscription, filter=None): + if not filter: + if 'filter' in vnf_lcm_subscription: + filter_dict = json.loads(vnf_lcm_subscription.filter) + return { + 'id': vnf_lcm_subscription.id.decode(), + 'filter': filter_dict, + 'callbackUri': vnf_lcm_subscription.callback_uri.decode(), + } + return { + 'id': vnf_lcm_subscription.id.decode(), + 'callbackUri': vnf_lcm_subscription.callback_uri.decode(), + } + else: + return { + 'id': vnf_lcm_subscription.id, + 'filter': filter, + 'callbackUri': vnf_lcm_subscription.callback_uri, + } + + def _subscription_filter( + self, + subscription_data, + nextpage_opaque_marker, + 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 + wk_counter = 0 + for cnt, line in enumerate(subscription_data): + LOG.debug("cnt %d,line %s" % (cnt, line)) + + if start_num > wk_counter: + wk_counter = wk_counter + 1 + else: + if (CONF.vnf_lcm.subscription_num > len( + lcmsubscription) and nextpage_opaque_marker): + # add lcmsubscription + vnf_subscription_res = self._basic_subscription_info( + line) + links = self._get_subscription_links(line) + vnf_subscription_res.update(links) + lcmsubscription.append(vnf_subscription_res) + if CONF.vnf_lcm.subscription_num == len( + lcmsubscription): + if cnt == len(subscription_data) - 1: + last_flg = True + break + elif not nextpage_opaque_marker: + # add lcmsubscription + vnf_subscription_res = self._basic_subscription_info( + line) + links = self._get_subscription_links(line) + vnf_subscription_res.update(links) + lcmsubscription.append(vnf_subscription_res) + if CONF.vnf_lcm.subscription_num < len( + lcmsubscription): + return 400, False + if cnt == len(subscription_data) - 1: + last_flg = True + + LOG.debug("len(lcmsubscription) %s" % len(lcmsubscription)) + LOG.debug( + "CONF.vnf_lcm.subscription_num %s" % + CONF.vnf_lcm.subscription_num) + + return lcmsubscription, last_flg + + def _get_vnf_lcm_subscription(self, vnf_lcm_subscription, filter=None): + vnf_lcm_subscription_response = self._basic_subscription_info( + vnf_lcm_subscription, filter) + + links = self._get_subscription_links(vnf_lcm_subscription) + vnf_lcm_subscription_response.update(links) + + return vnf_lcm_subscription_response + + def subscription_create(self, vnf_lcm_subscription, filter): + return self._get_vnf_lcm_subscription(vnf_lcm_subscription, filter) + + def subscription_list( + self, + vnf_lcm_subscriptions, + nextpage_opaque_marker, + paging): + return self._subscription_filter( + vnf_lcm_subscriptions, nextpage_opaque_marker, paging) + + def subscription_show(self, vnf_lcm_subscriptions): + return self._get_vnf_lcm_subscription(vnf_lcm_subscriptions) diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index b705a93..21472d4 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -13,25 +13,42 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_log import log as logging +from oslo_serialization import jsonutils from oslo_utils import uuidutils +import ast +import json +import re +import traceback + import six from six.moves import http_client import webob +from urllib import parse + +from tacker._i18n import _ from tacker.api.schemas import vnf_lcm from tacker.api import validation from tacker.api.views import vnf_lcm as vnf_lcm_view from tacker.common import exceptions from tacker.common import utils from tacker.conductor.conductorrpc import vnf_lcm_rpc +import tacker.conf from tacker.extensions import nfvo +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.policies import vnf_lcm as vnf_lcm_policies from tacker.vnfm import vim_client from tacker import wsgi +CONF = tacker.conf.CONF + +LOG = logging.getLogger(__name__) + def check_vnf_state(action, instantiation_state=None, task_state=(None,)): """Decorator to check vnf states are valid for particular action. @@ -70,11 +87,32 @@ def check_vnf_state(action, instantiation_state=None, task_state=(None,)): class VnfLcmController(wsgi.Controller): + notification_type_list = ['VnfLcmOperationOccurrenceNotification', + 'VnfIdentifierCreationNotification', + 'VnfIdentifierDeletionNotification'] + operation_type_list = ['INSTANTIATE', + 'SCALE', + 'SCALE_TO_LEVEL', + 'CHANGE_FLAVOUR', + 'TERMINATE', + 'HEAL', + 'OPERATE', + 'CHANGE_EXT_CONN', + 'MODIFY_INFO'] + operation_state_list = ['STARTING', + 'PROCESSING', + 'COMPLETED', + 'FAILED_TEMP', + 'FAILED', + 'ROLLING_BACK', + 'ROLLED_BACK'] + _view_builder_class = vnf_lcm_view.ViewBuilder def __init__(self): super(VnfLcmController, self).__init__() self.rpc_api = vnf_lcm_rpc.VNFLcmRPCAPI() + self._vnfm_plugin = manager.TackerManager.get_service_plugins()['VNFM'] def _get_vnf_instance_href(self, vnf_instance): return '/vnflcm/v1/vnf_instances/%s' % vnf_instance.id @@ -349,6 +387,193 @@ class VnfLcmController(wsgi.Controller): vnf_instance = self._get_vnf_instance(context, id) self._heal(context, vnf_instance, body) + @wsgi.response(http_client.CREATED) + @validation.schema(vnf_lcm.register_subscription) + def register_subscription(self, request, body): + subscription_request_data = body + if subscription_request_data.get('filter'): + # notificationTypes check + notification_types = subscription_request_data.get( + "filter").get("notificationTypes") + for notification_type in notification_types: + if notification_type not in self.notification_type_list: + msg = ( + _("notificationTypes value mismatch: %s") % + notification_type) + return self._make_problem_detail( + msg, 400, title='Bad Request') + + # operationTypes check + operation_types = subscription_request_data.get( + "filter").get("operationTypes") + for operation_type in operation_types: + if operation_type not in self.operation_type_list: + msg = ( + _("operationTypes value mismatch: %s") % + operation_type) + return self._make_problem_detail( + msg, 400, title='Bad Request') + + subscription_id = uuidutils.generate_uuid() + + vnf_lcm_subscription = subscription_obj.LccnSubscriptionRequest( + context=request.context) + vnf_lcm_subscription.id = subscription_id + vnf_lcm_subscription.callback_uri = subscription_request_data.get( + 'callbackUri') + vnf_lcm_subscription.subscription_authentication = \ + subscription_request_data.get('subscriptionAuthentication') + LOG.debug("filter %s " % subscription_request_data.get('filter')) + LOG.debug( + "filter type %s " % + type( + subscription_request_data.get('filter'))) + filter_uni = subscription_request_data.get('filter') + filter = ast.literal_eval(str(filter_uni).replace("u'", "'")) + + try: + vnf_lcm_subscription = vnf_lcm_subscription.create(filter) + LOG.debug("vnf_lcm_subscription %s" % vnf_lcm_subscription) + except exceptions.SeeOther as e: + if re.search("^303", str(e)): + res = self._make_problem_detail( + "See Other", 303, title='See Other') + link = ( + 'LINK', + CONF.vnf_lcm.endpoint_url + + "/vnflcm/v1/subscriptions/" + + str(e)[ + 3:]) + res.headerlist.append(link) + return res + else: + LOG.error(traceback.format_exc()) + return self._make_problem_detail( + str(e), 500, title='Internal Server Error') + + result = self._view_builder.subscription_create(vnf_lcm_subscription, + filter) + location = result.get('_links', {}).get('self', {}).get('href') + headers = {"location": location} + return wsgi.ResponseObject(result, headers=headers) + + @wsgi.response(http_client.OK) + def subscription_show(self, request, subscriptionId): + try: + vnf_lcm_subscriptions = ( + subscription_obj.LccnSubscriptionRequest. + vnf_lcm_subscriptions_show(request.context, subscriptionId)) + if not vnf_lcm_subscriptions: + msg = ( + _("Can not find requested vnf lcm subscriptions: %s") % + subscriptionId) + return self._make_problem_detail(msg, 404, title='Not Found') + except Exception as e: + return self._make_problem_detail( + str(e), 500, title='Internal Server Error') + + return self._view_builder.subscription_show(vnf_lcm_subscriptions) + + @wsgi.response(http_client.OK) + def subscription_list(self, request): + nextpage_opaque_marker = "" + paging = 1 + + re_url = request.path_url + query_params = request.query_string + if query_params: + query_params = parse.unquote(query_params) + LOG.debug("query_params %s" % query_params) + if query_params: + query_param_list = query_params.split('&') + for query_param in query_param_list: + query_param_key_value = query_param.split('=') + if len(query_param_key_value) != 2: + msg = _("Request query parameter error") + return self._make_problem_detail( + msg, 400, title='Bad Request') + if query_param_key_value[0] == 'nextpage_opaque_marker': + nextpage_opaque_marker = query_param_key_value[1] + if query_param_key_value[0] == 'page': + paging = int(query_param_key_value[1]) + + try: + vnf_lcm_subscriptions = ( + subscription_obj.LccnSubscriptionRequest. + vnf_lcm_subscriptions_list(request.context)) + LOG.debug("vnf_lcm_subscriptions %s" % vnf_lcm_subscriptions) + subscription_data, last = self._view_builder.subscription_list( + vnf_lcm_subscriptions, nextpage_opaque_marker, paging) + LOG.debug("last %s" % last) + except Exception as e: + LOG.error(traceback.format_exc()) + return self._make_problem_detail( + str(e), 500, title='Internal Server Error') + + if subscription_data == 400: + msg = _("Number of records exceeds nextpage_opaque_marker") + return self._make_problem_detail(msg, 400, title='Bad Request') + + # make response + res = webob.Response(content_type='application/json') + res.body = jsonutils.dump_as_bytes(subscription_data) + res.status_int = 200 + if nextpage_opaque_marker: + if not last: + ln = '<%s?page=%s>;rel="next"; title*="next chapter"' % ( + re_url, paging + 1) + # Regarding the setting in http header related to + # nextpage control, RFC8288 and NFV-SOL013 + # specifications have not been confirmed. + # Therefore, it is implemented by setting "page", + # which is a general control method of WebAPI, + # as "URI-Reference" of Link header. + + links = ('Link', ln) + res.headerlist.append(links) + LOG.debug("subscription_list res %s" % res) + + return res + + @wsgi.response(http_client.NO_CONTENT) + def delete_subscription(self, request, subscriptionId): + try: + vnf_lcm_subscription = \ + subscription_obj.LccnSubscriptionRequest.destroy( + request.context, subscriptionId) + if vnf_lcm_subscription == 404: + msg = ( + _("Can not find requested vnf lcm subscription: %s") % + subscriptionId) + return self._make_problem_detail(msg, 404, title='Not Found') + except Exception as e: + return self._make_problem_detail( + str(e), 500, title='Internal Server Error') + + # Generate a response when an error occurs as a problem_detail object + def _make_problem_detail( + self, + detail, + status, + title=None, + type=None, + instance=None): + '''This process returns the problem_detail to the caller''' + LOG.error(detail) + res = webob.Response(content_type='application/problem+json') + problem_details = {} + if type: + problem_details['type'] = type + if title: + problem_details['title'] = title + problem_details['detail'] = detail + problem_details['status'] = status + if instance: + problem_details['instance'] = instance + res.text = json.dumps(problem_details) + res.status_int = status + return res + def create_resource(): return wsgi.Resource(VnfLcmController()) diff --git a/tacker/api/vnflcm/v1/router.py b/tacker/api/vnflcm/v1/router.py index 9e77fd9..4d206bb 100644 --- a/tacker/api/vnflcm/v1/router.py +++ b/tacker/api/vnflcm/v1/router.py @@ -90,3 +90,11 @@ class VnflcmAPIRouter(wsgi.Router): self._setup_route(mapper, "/vnf_instances/{id}/heal", methods, controller, default_resource) + + methods = {"GET": "subscription_list", "POST": "register_subscription"} + self._setup_route(mapper, "/subscriptions", + methods, controller, default_resource) + + methods = {"GET": "subscription_show", "DELETE": "delete_subscription"} + self._setup_route(mapper, "/subscriptions/{subscriptionId}", + methods, controller, default_resource) diff --git a/tacker/common/exceptions.py b/tacker/common/exceptions.py index b20efed..c43bf98 100644 --- a/tacker/common/exceptions.py +++ b/tacker/common/exceptions.py @@ -345,3 +345,7 @@ class LimitExceeded(TackerException): class UserDataUpdateCreateFailed(TackerException): msg_fmt = _("User data for VNF package %(id)s cannot be updated " "or created after %(retries)d retries.") + + +class SeeOther(TackerException): + code = 303 diff --git a/tacker/conf/__init__.py b/tacker/conf/__init__.py index b6b7029..e5afcc6 100644 --- a/tacker/conf/__init__.py +++ b/tacker/conf/__init__.py @@ -18,12 +18,14 @@ from oslo_config import cfg from tacker.conf import conductor from tacker.conf import coordination +from tacker.conf import vnf_lcm from tacker.conf import vnf_package CONF = cfg.CONF CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token') vnf_package.register_opts(CONF) +vnf_lcm.register_opts(CONF) conductor.register_opts(CONF) coordination.register_opts(CONF) glance_store.register_opts(CONF) diff --git a/tacker/conf/vnf_lcm.py b/tacker/conf/vnf_lcm.py new file mode 100644 index 0000000..3fbbad8 --- /dev/null +++ b/tacker/conf/vnf_lcm.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg + + +CONF = cfg.CONF + +OPTS = [ + cfg.StrOpt( + 'endpoint_url', + default='http://localhost:9890/', + help="endpoint_url"), + cfg.IntOpt( + 'subscription_num', + default=100, + help="Number of subscriptions"), + cfg.IntOpt( + 'retry_num', + default=3, + help="Number of retry"), + cfg.IntOpt( + 'retry_wait', + default=10, + help="Retry interval(sec)")] + +vnf_lcm_group = cfg.OptGroup('vnf_lcm', + title='vnf_lcm options', + help="Vnflcm options group") + + +def register_opts(conf): + conf.register_group(vnf_lcm_group) + conf.register_opts(OPTS, group=vnf_lcm_group) + + +def list_opts(): + return {vnf_lcm_group: OPTS} diff --git a/tacker/db/db_sqlalchemy/models.py b/tacker/db/db_sqlalchemy/models.py index d2db8a0..5d127bf 100644 --- a/tacker/db/db_sqlalchemy/models.py +++ b/tacker/db/db_sqlalchemy/models.py @@ -246,3 +246,54 @@ class VnfResource(model_base.BASE, models.SoftDeleteMixin, resource_type = sa.Column(sa.String(255), nullable=False) resource_identifier = sa.Column(sa.String(255), nullable=False) resource_status = sa.Column(sa.String(255), nullable=False) + + +class VnfLcmSubscriptions(model_base.BASE, models.SoftDeleteMixin, + models.TimestampMixin): + """Contains all info about vnf LCM Subscriptions.""" + + __tablename__ = 'vnf_lcm_subscriptions' + id = sa.Column(sa.String(36), nullable=False, primary_key=True) + callback_uri = sa.Column(sa.String(255), nullable=False) + subscription_authentication = sa.Column(sa.JSON, nullable=True) + + +class VnfLcmFilters(model_base.BASE): + """Contains all info about vnf LCM filters.""" + + __tablename__ = 'vnf_lcm_filters' + __maxsize__ = 65536 + id = sa.Column(sa.Integer, nullable=True, primary_key=True) + subscription_uuid = sa.Column(sa.String(36), + sa.ForeignKey('vnf_lcm_subscriptions.id'), + nullable=False) + filter = sa.Column(sa.JSON, nullable=False) + notification_types = sa.Column(sa.VARBINARY(255), nullable=True) + notification_types_len = sa.Column(sa.Integer, nullable=True) + operation_types = sa.Column( + sa.LargeBinary( + length=__maxsize__), + nullable=True) + operation_types_len = sa.Column(sa.Integer, nullable=True) + + +class VnfLcmOpOccs(model_base.BASE, models.SoftDeleteMixin, + models.TimestampMixin): + """VNF LCM OP OCCS Fields""" + + __tablename__ = 'vnf_lcm_op_occs' + id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) + vnf_instance_id = sa.Column(sa.String(36), + sa.ForeignKey('vnf_instances.id'), + nullable=False) + state_entered_time = sa.Column(sa.DateTime(), nullable=False) + start_time = sa.Column(sa.DateTime(), nullable=False) + operation_state = sa.Column(sa.String(length=255), nullable=False) + operation = sa.Column(sa.String(length=255), nullable=False) + is_automatic_invocation = sa.Column(sa.Boolean, nullable=False) + operation_params = sa.Column(sa.JSON(), nullable=True) + is_cancel_pending = sa.Column(sa.Boolean(), nullable=False) + error = sa.Column(sa.JSON(), nullable=True) + resource_changes = sa.Column(sa.JSON(), nullable=True) + changed_info = sa.Column(sa.JSON(), nullable=True) + error_point = sa.Column(sa.Integer, nullable=False) diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index aae979f..5291a3e 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -8a7ca803e0d0 +c47a733f425a \ No newline at end of file diff --git a/tacker/db/migration/alembic_migrations/versions/c47a733f425a_add_vnflcm_subscription.py b/tacker/db/migration/alembic_migrations/versions/c47a733f425a_add_vnflcm_subscription.py new file mode 100644 index 0000000..4d0b982 --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/c47a733f425a_add_vnflcm_subscription.py @@ -0,0 +1,92 @@ +# Copyright 2020 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +# flake8: noqa: E402 + +"""add_vnflcm_subscription + +Revision ID: c47a733f425a +Revises: 8a7ca803e0d0 +Create Date: 2020-08-27 14:18:43.907565 + +""" + +# revision identifiers, used by Alembic. +revision = 'c47a733f425a' +down_revision = '8a7ca803e0d0' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import Boolean + +from tacker.db import types + + +def upgrade(active_plugins=None, options=None): + op.create_table( + 'vnf_lcm_subscriptions', + sa.Column('id', types.Uuid(length=36), nullable=False), + sa.Column('callback_uri', sa.String(length=255), nullable=False), + sa.Column('subscription_authentication', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('deleted', Boolean, default=False), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + + noti_str = "json_unquote(json_extract('filter','$.notificationTypes'))" + sta_str = "json_unquote(json_extract('filter','$.operationStates'))" + op.create_table( + 'vnf_lcm_filters', + sa.Column('id', sa.Integer, autoincrement=True, nullable=False), + sa.Column('subscription_uuid', sa.String(length=36), nullable=False), + sa.Column('filter', sa.JSON(), nullable=False), + sa.Column('notification_types', + sa.LargeBinary(length=65536), + sa.Computed(noti_str)), + sa.Column('notification_types_len', + sa.Integer, + sa.Computed("ifnull(json_length('notification_types'),0)")), + sa.Column('operation_states', + sa.LargeBinary(length=65536), + sa.Computed(sta_str)), + sa.Column('operation_states_len', + sa.Integer, + sa.Computed("ifnull(json_length('operation_states'),0)")), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['subscription_uuid'], + ['vnf_lcm_subscriptions.id'], ), + mysql_engine='InnoDB' + ) + + op.create_table( + 'vnf_lcm_op_occs', + sa.Column('id', types.Uuid(length=36), nullable=False), + sa.Column('operation_state', sa.String(length=255), nullable=False), + sa.Column('state_entered_time', sa.DateTime(), nullable=False), + sa.Column('start_time', sa.DateTime(), nullable=False), + sa.Column('vnf_instance_id', types.Uuid(length=36), nullable=False), + sa.Column('operation', sa.String(length=255), nullable=False), + sa.Column('is_automatic_invocation', sa.Boolean, nullable=False), + sa.Column('operation_params', sa.JSON(), nullable=True), + sa.Column('error', sa.JSON(), nullable=True), + sa.Column('deleted', Boolean, default=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['vnf_instance_id'], + ['vnf_instances.id'], ), + mysql_engine='InnoDB' + ) diff --git a/tacker/objects/__init__.py b/tacker/objects/__init__.py index 3707d69..55fe047 100644 --- a/tacker/objects/__init__.py +++ b/tacker/objects/__init__.py @@ -36,3 +36,4 @@ def register_all(): __import__('tacker.objects.vnf_resources') __import__('tacker.objects.terminate_vnf_req') __import__('tacker.objects.vnf_artifact') + __import__('tacker.objects.vnf_lcm_subscriptions') diff --git a/tacker/objects/vnf_lcm_subscriptions.py b/tacker/objects/vnf_lcm_subscriptions.py new file mode 100644 index 0000000..f18e03d --- /dev/null +++ b/tacker/objects/vnf_lcm_subscriptions.py @@ -0,0 +1,335 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging +from oslo_utils import timeutils +from sqlalchemy.sql import text + +from tacker.common import exceptions +import tacker.conf +from tacker.db import api as db_api +from tacker.db.db_sqlalchemy import api +from tacker.db.db_sqlalchemy import models +from tacker.objects import base +from tacker.objects import fields + +_NO_DATA_SENTINEL = object() + +LOG = logging.getLogger(__name__) +CONF = tacker.conf.CONF + + +def _make_list(value): + if isinstance(value, list): + res = "" + for i in range(len(value)): + t = "\"{}\"".format(value[i]) + if i == 0: + res = str(t) + else: + res = "{0},{1}".format(res, t) + + res = "[{}]".format(res) + else: + res = "[\"{}\"]".format(str(value)) + return res + + +@db_api.context_manager.reader +def _vnf_lcm_subscriptions_get(context, + notification_type, + operation_type=None + ): + + if notification_type == 'VnfLcmOperationOccurrenceNotification': + sql = ( + "select" + " t1.id,t1.callback_uri,t1.subscription_authentication,t2.filter " + " from " + " vnf_lcm_subscriptions t1, " + " (select distinct subscription_uuid,filter from vnf_lcm_filters " + " where " + " (notification_types_len = 0 \ + or JSON_CONTAINS(notification_types, '" + + _make_list(notification_type) + + "')) " + " and " + " (operation_types_len = 0 or JSON_CONTAINS(operation_types, '" + + _make_list(operation_type) + + "')) " + " order by " + " notification_types_len desc," + " operation_types_len desc" + ") t2 " + " where " + " t1.id=t2.subscription_uuid " + " and t1.deleted=0") + else: + sql = ( + "select" + " t1.id,t1.callback_uri,t1.subscription_authentication,t2.filter " + " from " + " vnf_lcm_subscriptions t1, " + " (select distinct subscription_uuid,filter from vnf_lcm_filters " + " where " + " (notification_types_len = 0 or \ + JSON_CONTAINS(notification_types, '" + + _make_list(notification_type) + + "')) " + " order by " + " notification_types_len desc," + " operation_types_len desc" + ") t2 " + " where " + " t1.id=t2.subscription_uuid " + " and t1.deleted=0") + + result_list = [] + result = context.session.execute(sql) + for line in result: + result_list.append(line) + return result_list + + +@db_api.context_manager.reader +def _vnf_lcm_subscriptions_show(context, subscriptionId): + + sql = text( + "select " + "t1.id,t1.callback_uri,t2.filter " + "from vnf_lcm_subscriptions t1, " + "(select distinct subscription_uuid,filter from vnf_lcm_filters) t2 " + "where t1.id = t2.subscription_uuid " + "and deleted = 0 " + "and t1.id = :subsc_id") + try: + result = context.session.execute(sql, {'subsc_id': subscriptionId}) + except exceptions.NotFound: + return '' + except Exception as e: + raise e + return result + + +@db_api.context_manager.reader +def _vnf_lcm_subscriptions_all(context): + + sql = text( + "select " + "t1.id,t1.callback_uri,t2.filter " + "from vnf_lcm_subscriptions t1, " + "(select distinct subscription_uuid,filter from vnf_lcm_filters) t2 " + "where t1.id = t2.subscription_uuid " + "and deleted = 0 ") + result_list = [] + try: + result = context.session.execute(sql) + for line in result: + result_list.append(line) + except Exception as e: + raise e + + return result_list + + +@db_api.context_manager.reader +def _get_by_subscriptionid(context, subscriptionsId): + + sql = text("select id " + "from vnf_lcm_subscriptions " + "where id = :subsc_id " + "and deleted = 0 ") + try: + result = context.session.execute(sql, {'subsc_id': subscriptionsId}) + except exceptions.NotFound: + return '' + except Exception as e: + raise e + + return result + + +@db_api.context_manager.reader +def _vnf_lcm_subscriptions_id_get(context, + callbackUri, + notification_type=None, + operation_type=None + ): + + sql = ("select " + "t1.id " + "from " + "vnf_lcm_subscriptions t1, " + "(select subscription_uuid from vnf_lcm_filters " + "where ") + + if notification_type: + sql = (sql + " JSON_CONTAINS(notification_types, '" + + _make_list(notification_type) + "') ") + else: + sql = sql + " notification_types_len=0 " + sql = sql + "and " + + if operation_type: + sql = sql + " JSON_CONTAINS(operation_types, '" + \ + _make_list(operation_type) + "') " + else: + sql = sql + " operation_types_len=0 " + sql = ( + sql + + ") t2 where t1.id=t2.subscription_uuid and t1.callback_uri= '" + + callbackUri + + "' and t1.deleted=0 ") + LOG.debug("sql[%s]" % sql) + + try: + result = context.session.execute(sql) + return result + except exceptions.NotFound: + return '' + + +def _add_filter_data(context, subscription_id, filter): + with db_api.context_manager.writer.using(context): + + new_entries = [] + new_entries.append({"subscription_uuid": subscription_id, + "filter": filter}) + + context.session.execute( + models.VnfLcmFilters.__table__.insert(None), + new_entries) + + +@db_api.context_manager.writer +def _vnf_lcm_subscriptions_create(context, values, filter): + with db_api.context_manager.writer.using(context): + + new_entries = [] + if 'subscription_authentication' in values: + new_entries.append({"id": values.id, + "callback_uri": values.callback_uri, + "subscription_authentication": + values.subscription_authentication}) + else: + new_entries.append({"id": values.id, + "callback_uri": values.callback_uri}) + + context.session.execute( + models.VnfLcmSubscriptions.__table__.insert(None), + new_entries) + + callbackUri = values.callback_uri + if filter: + notification_type = filter.get('notificationTypes') + operation_type = filter.get('operationTypese') + + vnf_lcm_subscriptions_id = _vnf_lcm_subscriptions_id_get( + context, + callbackUri, + notification_type=notification_type, + operation_type=operation_type) + + if vnf_lcm_subscriptions_id: + raise Exception("303" + vnf_lcm_subscriptions_id.id.decode()) + + _add_filter_data(context, values.id, filter) + + else: + vnf_lcm_subscriptions_id = _vnf_lcm_subscriptions_id_get(context, + callbackUri) + + if vnf_lcm_subscriptions_id: + raise Exception("303" + vnf_lcm_subscriptions_id.id.decode()) + _add_filter_data(context, values.id, {}) + + return values + + +@db_api.context_manager.writer +def _destroy_vnf_lcm_subscription(context, subscriptionId): + now = timeutils.utcnow() + updated_values = {'deleted': 1, + 'deleted_at': now} + try: + api.model_query(context, models.VnfLcmSubscriptions). \ + filter_by(id=subscriptionId). \ + update(updated_values, synchronize_session=False) + except Exception as e: + raise e + + +@base.TackerObjectRegistry.register +class LccnSubscriptionRequest(base.TackerObject, base.TackerPersistentObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.UUIDField(nullable=False), + 'callback_uri': fields.StringField(nullable=False), + 'subscription_authentication': + fields.DictOfStringsField(nullable=True), + 'filter': fields.StringField(nullable=True) + } + + @base.remotable + def create(self, filter): + updates = self.obj_clone() + db_vnf_lcm_subscriptions = _vnf_lcm_subscriptions_create( + self._context, updates, filter) + return db_vnf_lcm_subscriptions + + @base.remotable_classmethod + def vnf_lcm_subscriptions_show(cls, context, subscriptionId): + try: + vnf_lcm_subscriptions = _vnf_lcm_subscriptions_show( + context, subscriptionId) + except Exception as e: + raise e + return vnf_lcm_subscriptions + + @base.remotable_classmethod + def vnf_lcm_subscriptions_list(cls, context): + # get vnf_lcm_subscriptions data + try: + vnf_lcm_subscriptions = _vnf_lcm_subscriptions_all(context) + except Exception as e: + raise e + + return vnf_lcm_subscriptions + + @base.remotable_classmethod + def vnf_lcm_subscriptions_get(cls, context, + notification_type, + operation_type=None): + return _vnf_lcm_subscriptions_get(context, + notification_type, + operation_type) + + @base.remotable_classmethod + def destroy(cls, context, subscriptionId): + try: + get_subscriptionid = _get_by_subscriptionid( + context, subscriptionId) + except Exception as e: + raise e + + if not get_subscriptionid: + return 404 + + try: + _destroy_vnf_lcm_subscription(context, subscriptionId) + except Exception as e: + raise e + + return 204 diff --git a/tacker/tests/unit/objects/fakes.py b/tacker/tests/unit/objects/fakes.py index e35fd51..a333ca8 100644 --- a/tacker/tests/unit/objects/fakes.py +++ b/tacker/tests/unit/objects/fakes.py @@ -50,6 +50,50 @@ artifacts = { 'type': 'tosca.artifacts.nfv.SwImage', 'algorithm': 'sha512', 'hash': uuidsentinel.hash} +filter = { + "usageState": ["NOT_IN_USE"], + "vnfPkgId": ["f04857cb-abdc-405f-8254-01501f3fa059"], + "vnfdId": ["b1bb0ce7-5555-0001-95ed-4840d70a1209"], + "vnfProductsFromProviders": [ + { + "vnfProvider": "xxxxx", + "vnfProducts": [ + { + "vnfProductName": "artifactVNF", + "versions": [ + { + "vnfSoftwareVersion": "1.0", + "vnfdVersions": ["v2.2"] + } + ] + } + ] + }, + { + "vnfProvider": "xxxxx", + "vnfProducts": [ + { + "vnfProductName": "artifactVNF", + "versions": [ + { + "vnfSoftwareVersion": "1.0", + "vnfdVersions": ["v2.2"] + } + ] + } + ] + } + ], + "notificationTypes": ["VnfLcmOperationOccurrenceNotification"], + "operationalState": ["ENABLED"] +} + +subscription_data = { + 'id': "c3e5ea85-8e3d-42df-a636-3b7857cbd7f9", + 'callback_uri': "fake_url", + 'created_at': "2020-06-11 09:39:58" +} + fake_vnf_package_response = copy.deepcopy(vnf_package_data) fake_vnf_package_response.pop('user_data') fake_vnf_package_response.update({'id': uuidsentinel.package_uuid}) diff --git a/tacker/tests/unit/objects/test_vnf_lcm_subscriptions.py b/tacker/tests/unit/objects/test_vnf_lcm_subscriptions.py new file mode 100644 index 0000000..fb990cd --- /dev/null +++ b/tacker/tests/unit/objects/test_vnf_lcm_subscriptions.py @@ -0,0 +1,309 @@ +# 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 unittest import mock + +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 TestVnfd(SqlTestCase): + + def setUp(self): + super(TestVnfd, 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.subscription = self._create_subscription() + + 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 + + @mock.patch.object(objects.vnf_lcm_subscriptions, + '_vnf_lcm_subscriptions_create') + def _create_subscription(self, mock_vnf_lcm_subscriptions_create): + filter = fakes.filter + mock_vnf_lcm_subscriptions_create.return_value = \ + '{\ + "filter": "{"operationStates": ["COMPLETED"],\ + "vnfInstanceNames": ["xxxxxxxxxxxxxxxxxx"],\ + "operationTypes": ["INSTANTIATE"],\ + "vnfdIds": ["405d73c7-e964-4c8b-a914-41478ccd7c42"],\ + "vnfProductsFromProviders": [{\ + "vnfProvider": "x2x", \ + "vnfProducts": [{\ + "vnfProductName": "x2xx", \ + "versions": [{\ + "vnfSoftwareVersion": "xx2XX", \ + "vnfdVersions": ["ss2"]\ + }]\ + }]\ + }, \ + {\ + "vnfProvider": "z2z",\ + "vnfProducts": [{\ + "vnfProductName": "z2zx", \ + "versions": [{\ + "vnfSoftwareVersion": "xx3XX",\ + "vnfdVersions": \ + ["s3sx", "s3sa"]\ + }\ + ]},\ + {\ + "vnfProductName": "zz3ex",\ + "versions": [{\ + "vnfSoftwareVersion": "xxe3eXz",\ + "vnfdVersions": ["ss3xz", "s3esaz"]\ + },\ + {\ + "vnfSoftwareVersion": "xxeeeXw", \ + "vnfdVersions": ["ss3xw", "ss3w"]\ + }]\ + }]\ + }],\ + "notificationTypes": [\ + "VnfLcmOperationOccurrenceNotification"],\ + "vnfInstanceIds": ["fb0b9a12-4b55-47ac-9ca8-5fdd52c4c07f"]}",\ + "callbackUri": "http://localhost/xxx",\ + "_links": {\ + "self": {\ + "href":\ + "http://localhost:9890//vnflcm/v1/subscriptions\ + /530a3c43-043a-4b84-9d65-aa0df49f7ced"\ + }\ + },\ + "id": "530a3c43-043a-4b84-9d65-aa0df49f7ced"\ + }' + + subscription_obj = \ + objects.vnf_lcm_subscriptions.LccnSubscriptionRequest( + context=self.context, **fakes.subscription_data) + subscription_obj.create(filter) + + return subscription_obj + + @mock.patch.object(objects.vnf_lcm_subscriptions, + '_vnf_lcm_subscriptions_create') + def test_create(self, mock_vnf_lcm_subscriptions_create): + filter = fakes.filter + subscription_obj = \ + objects.vnf_lcm_subscriptions.LccnSubscriptionRequest( + context=self.context) + mock_vnf_lcm_subscriptions_create.return_value = \ + '{\ + "filter": "{"operationStates": ["COMPLETED"],\ + "vnfInstanceNames": ["xxxxxxxxxxxxxxxxxx"],\ + "operationTypes": ["INSTANTIATE"],\ + "vnfdIds": ["405d73c7-e964-4c8b-a914-41478ccd7c42"],\ + "vnfProductsFromProviders": [{\ + "vnfProvider": "x2x", \ + "vnfProducts": [{\ + "vnfProductName": "x2xx", \ + "versions": [{\ + "vnfSoftwareVersion": "xx2XX", \ + "vnfdVersions": ["ss2"]\ + }]\ + }]\ + }, \ + {\ + "vnfProvider": "z2z",\ + "vnfProducts": [{\ + "vnfProductName": "z2zx", \ + "versions": [{\ + "vnfSoftwareVersion": "xx3XX",\ + "vnfdVersions": \ + ["s3sx", "s3sa"]\ + }\ + ]},\ + {\ + "vnfProductName": "zz3ex",\ + "versions": [{\ + "vnfSoftwareVersion": "xxe3eXz",\ + "vnfdVersions": ["ss3xz", "s3esaz"]\ + },\ + {\ + "vnfSoftwareVersion": "xxeeeXw", \ + "vnfdVersions": ["ss3xw", "ss3w"]\ + }]\ + }]\ + }],\ + "notificationTypes": [\ + "VnfLcmOperationOccurrenceNotification"],\ + "vnfInstanceIds": ["fb0b9a12-4b55-47ac-9ca8-5fdd52c4c07f"]}",\ + "callbackUri": "http://localhost/xxx",\ + "_links": {\ + "self": {\ + "href":\ + "http://localhost:9890//vnflcm/v1/subscriptions\ + /530a3c43-043a-4b84-9d65-aa0df49f7ced"\ + }\ + },\ + "id": "530a3c43-043a-4b84-9d65-aa0df49f7ced"\ + }' + + result = subscription_obj.create(filter) + self.assertTrue(filter, result) + + @mock.patch.object(objects.vnf_lcm_subscriptions, + '_vnf_lcm_subscriptions_show') + def test_show(self, mock_vnf_lcm_subscriptions_show): + filter = fakes.filter + subscription_obj = \ + objects.vnf_lcm_subscriptions.LccnSubscriptionRequest( + context=self.context) + mock_vnf_lcm_subscriptions_show.return_value = \ + '{\ + "filter": "{"operationStates": ["COMPLETED"],\ + "vnfInstanceNames": ["xxxxxxxxxxxxxxxxxx"],\ + "operationTypes": ["INSTANTIATE"],\ + "vnfdIds": ["405d73c7-e964-4c8b-a914-41478ccd7c42"],\ + "vnfProductsFromProviders": [{\ + "vnfProvider": "x2x", \ + "vnfProducts": [{\ + "vnfProductName": "x2xx", \ + "versions": [{\ + "vnfSoftwareVersion": "xx2XX", \ + "vnfdVersions": ["ss2"]\ + }]\ + }]\ + }, \ + {\ + "vnfProvider": "z2z",\ + "vnfProducts": [{\ + "vnfProductName": "z2zx", \ + "versions": [{\ + "vnfSoftwareVersion": "xx3XX",\ + "vnfdVersions": \ + ["s3sx", "s3sa"]\ + }\ + ]},\ + {\ + "vnfProductName": "zz3ex",\ + "versions": [{\ + "vnfSoftwareVersion": "xxe3eXz",\ + "vnfdVersions": ["ss3xz", "s3esaz"]\ + },\ + {\ + "vnfSoftwareVersion": "xxeeeXw", \ + "vnfdVersions": ["ss3xw", "ss3w"]\ + }]\ + }]\ + }],\ + "notificationTypes": [\ + "VnfLcmOperationOccurrenceNotification"],\ + "vnfInstanceIds": ["fb0b9a12-4b55-47ac-9ca8-5fdd52c4c07f"]}",\ + "callbackUri": "http://localhost/xxx",\ + "_links": {\ + "self": {\ + "href":\ + "http://localhost:9890//vnflcm/v1/subscriptions\ + /530a3c43-043a-4b84-9d65-aa0df49f7ced"\ + }\ + },\ + "id": "530a3c43-043a-4b84-9d65-aa0df49f7ced"\ + }' + + result = subscription_obj.vnf_lcm_subscriptions_show( + self.context, self.subscription.id) + self.assertTrue(filter, result) + + @mock.patch.object(objects.vnf_lcm_subscriptions, + '_vnf_lcm_subscriptions_all') + def test_list(self, mock_vnf_lcm_subscriptions_all): + filter = fakes.filter + subscription_obj = \ + objects.vnf_lcm_subscriptions.LccnSubscriptionRequest( + context=self.context) + mock_vnf_lcm_subscriptions_all.return_value = \ + '{\ + "filter": "{"operationStates": ["COMPLETED"],\ + "vnfInstanceNames": ["xxxxxxxxxxxxxxxxxx"],\ + "operationTypes": ["INSTANTIATE"],\ + "vnfdIds": ["405d73c7-e964-4c8b-a914-41478ccd7c42"],\ + "vnfProductsFromProviders": [{\ + "vnfProvider": "x2x", \ + "vnfProducts": [{\ + "vnfProductName": "x2xx", \ + "versions": [{\ + "vnfSoftwareVersion": "xx2XX", \ + "vnfdVersions": ["ss2"]\ + }]\ + }]\ + }, \ + {\ + "vnfProvider": "z2z",\ + "vnfProducts": [{\ + "vnfProductName": "z2zx", \ + "versions": [{\ + "vnfSoftwareVersion": "xx3XX",\ + "vnfdVersions": \ + ["s3sx", "s3sa"]\ + }\ + ]},\ + {\ + "vnfProductName": "zz3ex",\ + "versions": [{\ + "vnfSoftwareVersion": "xxe3eXz",\ + "vnfdVersions": ["ss3xz", "s3esaz"]\ + },\ + {\ + "vnfSoftwareVersion": "xxeeeXw", \ + "vnfdVersions": ["ss3xw", "ss3w"]\ + }]\ + }]\ + }],\ + "notificationTypes": [\ + "VnfLcmOperationOccurrenceNotification"],\ + "vnfInstanceIds": ["fb0b9a12-4b55-47ac-9ca8-5fdd52c4c07f"]}",\ + "callbackUri": "http://localhost/xxx",\ + "_links": {\ + "self": {\ + "href":\ + "http://localhost:9890//vnflcm/v1/subscriptions\ + /530a3c43-043a-4b84-9d65-aa0df49f7ced"\ + }\ + },\ + "id": "530a3c43-043a-4b84-9d65-aa0df49f7ced"\ + }' + + result = subscription_obj.vnf_lcm_subscriptions_list(self.context) + self.assertTrue(filter, result) + + @mock.patch.object(objects.vnf_lcm_subscriptions, + '_destroy_vnf_lcm_subscription') + @mock.patch.object(objects.vnf_lcm_subscriptions, '_get_by_subscriptionid') + def test_destroy(self, mock_get_by_subscriptionid, + mock_vnf_lcm_subscriptions_destroy): + mock_get_by_subscriptionid.result_value = "OK" + self.subscription.destroy(self.context, self.subscription.id) + mock_vnf_lcm_subscriptions_destroy.assert_called_with( + self.context, self.subscription.id)