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
This commit is contained in:
Aldinson Esto 2020-08-08 05:05:28 +09:00
parent 35d15a089f
commit 5baeeefedf
21 changed files with 1660 additions and 1 deletions

View File

@ -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.

View File

@ -0,0 +1,8 @@
{
"filter": {
"notificationTypes": [
"VnfLcmOperationOccurrenceNotification"
]
},
"callbackUri": "http://sample1.com/notification"
}

View File

@ -0,0 +1,14 @@
{
"id": "76057f8e65ab37fb82d9382dfc3f3c8b",
"filter": {
"notificationTypes": [
"VnfLcmOperationOccurrenceNotification"
]
},
"callbackUri": "http://sample1.com/notification",
"_links": {
"self": {
"href": "https://sample1.com/vnflcm/v1/subscriptions/76057f8e65ab37fb82d9382dfc3f3c8b"
}
}
}

View File

@ -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"
}
}
}
]

View File

@ -0,0 +1,14 @@
{
"id": "76057f8e65ab37fb82d9382dfc3f3c8b",
"filter": {
"notificationTypes": [
"VnfLcmOperationOccurrenceNotification"
]
},
"callbackUri": "http://sample1.com/notification",
"_links": {
"self": {
"href": "https://sample1.com/vnflcm/v1/subscriptions/76057f8e65ab37fb82d9382dfc3f3c8b"
}
}
}

View File

@ -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 #

View File

@ -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

View File

@ -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,
}

View File

@ -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)

View File

@ -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())

View File

@ -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)

View File

@ -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

View File

@ -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)

47
tacker/conf/vnf_lcm.py Normal file
View File

@ -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}

View File

@ -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)

View File

@ -1 +1 @@
8a7ca803e0d0
c47a733f425a

View File

@ -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'
)

View File

@ -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')

View File

@ -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