Support enhancement for Subscription Post

- Added support for the following attributes:
       *operationStates
       *vnfInstanceSubscriptionFilter

  - The filtering criteria is enhanced by adding the
    above attributes to effectively send_notifications

  - We also updated the following table in DB:
      vnf_lcm_filters

Implements: blueprint support-fundamental-lcm
Spec: https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-fundamental-vnf-lcm-based-on-ETSI-NFV.html
Change-Id: Iebe70c43ef1a5b653f8ec204b1b3a79ca882399f
This commit is contained in:
Aldinson Esto 2021-02-09 05:44:46 +09:00
parent ede9755ab5
commit 8242c132ad
13 changed files with 587 additions and 70 deletions

View File

@ -631,6 +631,18 @@ filter_notification_types:
in: body
required: false
type: string
filter_operation_states:
description: |
Match particular LCM operation state
values as reported in notifications 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
filter_operation_types:
description: |
Match particular VNF lifecycle operation types for
@ -1237,6 +1249,13 @@ vnf_instance_name:
in: body
required: false
type: string
vnf_instance_subscription_filter:
description: |
Filter criteria to select VNF instances
about which to notify.
in: body
required: false
type: object
vnf_instance_vim_connection_info:
description: |
Information about VIM connections to be used for managing the resources

View File

@ -2,7 +2,24 @@
"filter": {
"notificationTypes": [
"VnfLcmOperationOccurrenceNotification"
],
"vnfInstanceSubscriptionFilter": {
"vnfdIds": [],
"vnfProductsFromProviders": {
"vnfProvider": "Vnf Provider 1",
"vnfProducts": [
{
"vnfProductName": "Vnf Product 1",
"versions": [
{
"vnfSoftwareVersion": "v1",
"vnfdVersions": ["vnfd.v1.1"]
}
]
}
]
}
}
},
"callbackUri": "http://sample1.com/notification"
}

View File

@ -1,6 +1,23 @@
{
"id": "76057f8e65ab37fb82d9382dfc3f3c8b",
"filter": {
"vnfInstanceSubscriptionFilter": {
"vnfdIds": [],
"vnfProductsFromProviders": {
"vnfProvider": "Vnf Provider 1",
"vnfProducts": [
{
"vnfProductName": "Vnf Product 1",
"versions": [
{
"vnfSoftwareVersion": "v1",
"vnfdVersions": ["vnfd.v1.1"]
}
]
}
]
}
},
"notificationTypes": [
"VnfLcmOperationOccurrenceNotification"
]

View File

@ -1155,8 +1155,10 @@ Request Parameters
.. rest_parameters:: parameters_vnflcm.yaml
- filter: filter
- vnfInstanceSubscriptionFilter: vnf_instance_subscription_filter
- notificationTypes: filter_notification_types
- operationTypes: filter_operation_types
- operationStates: filter_operation_states
- callbackUri : callback_uri
- authentication: authentication
- authType: authentication_auth_type

View File

@ -184,6 +184,109 @@ _vimConnectionInfo = {
}
}
_versions = {
'type': 'array',
'items': {
'type': 'objects',
'properties': {
'vnfSoftwareVersion': {'type': 'string'},
'vnfdVersions': {
'type': 'array',
'items': {'type': 'string'}
}
},
'required': ['vnfSoftwareVersion']
}
}
_vnf_products = {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'vnfProductName': {'type': 'string'},
'versions': _versions
},
'required': ['vnfProductName']
}
}
_vnf_products_from_providers = {
'type': 'object',
'properties': {
'type': 'object',
'properties': {
'vnfProvider': {'type': 'string'},
'vnfProducts': _vnf_products
}
},
'required': ['vnfProvider']
}
_lifecycle_change_notifications_filter = {
'type': 'object',
'properties': {
'vnfInstanceSubscriptionFilter': {
'type': 'object',
'properties': {
'vnfdIds': {
'type': 'array',
'items': parameter_types.identifier
},
'vnfProductsFromProviders': _vnf_products_from_providers,
'vnfInstanceIds': {
'type': 'array',
'items': parameter_types.identifier
},
'vnfInstanceNames': {
'type': 'array',
'items': {'type': 'string'}
}
}
},
'notificationTypes': {
'type': 'array',
'items': {
'type': 'string',
'enum': [
'VnfLcmOperationOccurrenceNotification',
'VnfIdentifierCreationNotification',
'VnfIdentifierDeletionNotification']
}
},
'operationTypes': {
'type': 'array',
'items': {
'type': 'string',
'enum': [
'INSTANTIATE',
'SCALE',
'SCALE_TO_LEVEL',
'CHANGE_FLAVOUR',
'TERMINATE',
'HEAL',
'OPERATE',
'CHANGE_EXT_CONN',
'MODIFY_INFO']
}
},
'operationStates': {
'type': 'array',
'items': {
'type': 'string',
'enum': [
'STARTING',
'PROCESSING',
'COMPLETED',
'FAILED_TEMP',
'FAILED',
'ROLLING_BACK',
'ROLLED_BACK']
}
}
}
}
create = {
'type': 'object',
'properties': {
@ -241,7 +344,7 @@ heal = {
register_subscription = {
'type': 'object',
'properties': {
'filter': parameter_types.keyvalue_pairs,
'filter': _lifecycle_change_notifications_filter,
'callbackUri': {'type': 'string', 'maxLength': 255},
'authentication': parameter_types.keyvalue_pairs,
},

View File

@ -162,26 +162,6 @@ def check_vnf_status_and_error_point(action, status=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):
@ -904,28 +884,6 @@ class VnfLcmController(wsgi.Controller):
@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()

View File

@ -337,6 +337,17 @@ def chunkiter(fp, chunk_size=65536):
break
# TODO(esto.aln): Consider to move this function to
# convert_camelcase_to_snakecase(). We will consider the correct approach
# to modify the common function so as not to introduce degrade.
def convert_string_to_snakecase(name):
"""Converts a string from camelCase to snake_case."""
name_with_underscores = re.sub(
'(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2',
name_with_underscores).lower()
def convert_camelcase_to_snakecase(request_data):
"""Converts dict keys or list of dict keys from camelCase to snake_case.
@ -347,17 +358,11 @@ def convert_camelcase_to_snakecase(request_data):
:param request_data: dict with keys or list with items, in camelCase.
"""
def convert(name):
name_with_underscores = re.sub(
'(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2',
name_with_underscores).lower()
if isinstance(request_data, dict):
new_dict = {}
for key, property_value in request_data.items():
property_value = convert_camelcase_to_snakecase(property_value)
underscore_joined = convert(key)
underscore_joined = convert_string_to_snakecase(key)
new_dict[underscore_joined] = property_value
return new_dict

View File

@ -287,6 +287,7 @@ class VnfLcmFilters(model_base.BASE):
sa.ForeignKey('vnf_lcm_subscriptions.id'),
nullable=False)
filter = sa.Column(sa.JSON, nullable=False)
vnf_products_from_providers = sa.Column(sa.JSON, nullable=True)
notification_types = sa.Column(sa.VARBINARY(255), nullable=True)
notification_types_len = sa.Column(sa.Integer, nullable=True)
operation_types = sa.Column(

View File

@ -1 +1 @@
3adac34764da
c31f65e0d099

View File

@ -0,0 +1,137 @@
# Copyright 2021 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.
#
"""Add columns to vnf_lcm_filter
Revision ID: c31f65e0d099
Revises: 3adac34764da
Create Date: 2021-02-03 22:53:36.352774
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c31f65e0d099'
down_revision = '3adac34764da'
def upgrade(active_plugins=None, options=None):
sql_text_length = 65535
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnf_products_from_providers', sa.JSON()))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'operation_states', sa.TEXT(length=sql_text_length),
sa.Computed(
"json_unquote(json_extract(`filter`,'$.operationStates'))")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'operation_states_len', sa.Integer,
sa.Computed(
"ifnull(json_length(`operation_states`),0)")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnfd_ids', sa.TEXT(length=sql_text_length),
sa.Computed(
"json_unquote(json_extract(`filter`,'$.vnfdIds'))")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnfd_ids_len', sa.Integer,
sa.Computed(
"ifnull(json_length(`vnfd_ids`),0)")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnf_provider', sa.TEXT(length=sql_text_length),
sa.Computed(
"(ifnull(json_unquote(json_extract("
"`vnf_products_from_providers`,'$.vnfProvider')),''))")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnf_product_name', sa.TEXT(length=sql_text_length),
sa.Computed(
"(ifnull(json_unquote(json_extract("
"`vnf_products_from_providers`,"
"'$.vnfProducts[0].vnfProductName')),''))")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnf_software_version', sa.TEXT(length=sql_text_length),
sa.Computed(
"(ifnull(json_unquote(json_extract("
"`vnf_products_from_providers`,'$.vnfProducts[0]"
".versions[0].vnfSoftwareVersion')),''))")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnfd_versions', sa.TEXT(length=sql_text_length),
sa.Computed(
"json_unquote(json_extract(`vnf_products_from_providers`,"
"'$.vnfProducts[0].versions[0].vnfdVersions'))")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnfd_versions_len', sa.Integer,
sa.Computed(
"ifnull(json_length(`vnfd_versions`),0)")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnf_instance_ids', sa.TEXT(length=sql_text_length),
sa.Computed(
"json_unquote(json_extract(`filter`,'$.vnfInstanceIds'))")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnf_instance_ids_len', sa.Integer,
sa.Computed(
"ifnull(json_length(`vnf_instance_ids`),0)")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnf_instance_names', sa.TEXT(length=sql_text_length),
sa.Computed(
"json_unquote(json_extract(`filter`,'$.vnfInstanceNames'))")))
op.add_column(
'vnf_lcm_filters',
sa.Column(
'vnf_instance_names_len', sa.Integer,
sa.Computed(
"ifnull(json_length(`vnf_instance_names`),0)")))

View File

@ -15,6 +15,7 @@ from oslo_utils import timeutils
from sqlalchemy.sql import text
from tacker.common import exceptions
from tacker.common.utils import convert_string_to_snakecase
import tacker.conf
from tacker.db import api as db_api
from tacker.db.db_sqlalchemy import api
@ -28,6 +29,50 @@ LOG = logging.getLogger(__name__)
CONF = tacker.conf.CONF
VNF_INSTANCE_SUBSCRIPTION_FILTER = [
"vnfdIds", "vnfProvider", "vnfProductName",
"vnfSoftwareVersion", "vnfdVersions", "vnfInstanceIds",
"vnfInstanceNames"
]
VNF_INSTANCE_SUBSCRIPTION_FILTER_LISTS = [
"vnfdIds", "vnfdVersions", "vnfInstanceIds", "vnfInstanceNames"
]
def _get_vnf_subscription_filter_values(vnf_subscription_filter):
vnfd_ids = vnf_subscription_filter.get('vnfdIds', [])
vnf_instance_ids = vnf_subscription_filter.get('vnfInstanceIds', [])
vnf_instance_names = vnf_subscription_filter.get('vnfInstanceNames', [])
vnfd_products_from_providers = vnf_subscription_filter.get(
'vnfProductsFromProviders', {})
vnf_provider = vnfd_products_from_providers.get('vnfProvider', "")
vnf_products = vnfd_products_from_providers.get('vnfProducts', [])
vnf_product_name = ""
vnf_software_version = ""
vnfd_versions = []
if vnf_products:
vnf_product_name = vnf_products[0].get('vnfProductName', "")
versions = vnf_products[0].get('versions', [])
if versions:
vnf_software_version = versions[0].get('vnfSoftwareVersion', "")
vnfd_versions = versions[0].get('vnfdVersions', [])
vnf_subscription_array = [
{'vnfdIds': vnfd_ids},
{'vnfInstanceIds': vnf_instance_ids},
{'vnfInstanceNames': vnf_instance_names},
{'vnfProvider': vnf_provider},
{'vnfProductName': vnf_product_name},
{'vnfSoftwareVersion': vnf_software_version},
{'vnfdVersions': vnfd_versions}]
return vnf_subscription_array
def _make_list(value):
if isinstance(value, list):
res = ""
@ -165,7 +210,9 @@ def _get_by_subscriptionid(context, subscriptionsId):
def _vnf_lcm_subscriptions_id_get(context,
callbackUri,
notification_type=None,
operation_type=None
operation_type=None,
operation_state=None,
vnf_instance_subscription_filter=None
):
sql = ("select "
@ -175,6 +222,35 @@ def _vnf_lcm_subscriptions_id_get(context,
"(select subscription_uuid from vnf_lcm_filters "
"where ")
if vnf_instance_subscription_filter:
included_in_filter = []
column_list = _get_vnf_subscription_filter_values(
vnf_instance_subscription_filter)
for column in column_list:
for key in column:
if key in VNF_INSTANCE_SUBSCRIPTION_FILTER:
value = column[key]
if key in VNF_INSTANCE_SUBSCRIPTION_FILTER_LISTS:
value = _make_list(value)
else:
value = '"{}"'.format(value)
sql = (sql + " JSON_CONTAINS({}, '{}') and ".format(
convert_string_to_snakecase(key),
value
))
included_in_filter.append(key)
not_included_in_filter = list(
set(VNF_INSTANCE_SUBSCRIPTION_FILTER_LISTS) -
set(included_in_filter))
# items not being searched for is excluded by adding
# <name>_len=0 to the sql query
for key in not_included_in_filter:
sql = sql + " {}_len=0 and ".format(
convert_string_to_snakecase(key))
if notification_type:
sql = (sql + " JSON_CONTAINS(notification_types, '" +
_make_list(notification_type) + "') ")
@ -182,6 +258,13 @@ def _vnf_lcm_subscriptions_id_get(context,
sql = sql + " notification_types_len=0 "
sql = sql + "and "
if operation_state:
sql = (sql + " JSON_CONTAINS(operation_states, '" +
_make_list(operation_state) + "') ")
else:
sql = sql + " operation_states_len=0 "
sql = sql + "and "
if operation_type:
sql = sql + " JSON_CONTAINS(operation_types, '" + \
_make_list(operation_type) + "') "
@ -200,14 +283,25 @@ def _vnf_lcm_subscriptions_id_get(context,
return line
except exceptions.NotFound:
return ''
except Exception as exc:
LOG.error("SQL Error: %s" % str(exc))
return ''
def _add_filter_data(context, subscription_id, filter):
with db_api.context_manager.writer.using(context):
vnf_instance_subscription_filter = \
filter.get('vnfInstanceSubscriptionFilter')
vnf_products_from_providers = \
vnf_instance_subscription_filter.get(
'vnfProductsFromProviders')
new_entries = []
new_entries.append({"subscription_uuid": subscription_id,
"filter": filter})
"filter": filter,
"vnf_products_from_providers":
vnf_products_from_providers})
context.session.execute(
models.VnfLcmFilters.__table__.insert(None),
@ -236,12 +330,16 @@ def _vnf_lcm_subscriptions_create(context, values, filter):
if filter:
notification_type = filter.get('notificationTypes')
operation_type = filter.get('operationTypes')
operation_state = filter.get('operationStates')
subscription_filter = filter.get('vnfInstanceSubscriptionFilter')
vnf_lcm_subscriptions_id = _vnf_lcm_subscriptions_id_get(
context,
callbackUri,
notification_type=notification_type,
operation_type=operation_type)
operation_type=operation_type,
operation_state=operation_state,
vnf_instance_subscription_filter=subscription_filter)
if vnf_lcm_subscriptions_id:
raise Exception("303" + vnf_lcm_subscriptions_id)

View File

@ -30,6 +30,23 @@ class Subscription:
"""
return {
"filter": {
"vnfInstanceSubscriptionFilter": {
"vnfdIds": ["b1bb0ce7-ebca-4fa7-95ed-4840d7000000"],
"vnfProductsFromProviders": {
"vnfProvider": "Company",
"vnfProducts": [
{
"vnfProductName": "Sample VNF",
"versions": [
{
"vnfSoftwareVersion": "1.0",
"vnfdVersions": ["1.0"]
}
]
}
]
}
},
"notificationTypes": [
"VnfLcmOperationOccurrenceNotification",
"VnfIdentifierCreationNotification",
@ -42,7 +59,8 @@ class Subscription:
"HEAL",
"MODIFY_INFO",
"CHANGE_EXT_CONN"
]
],
"operationStates": ["STARTING"]
},
"callbackUri": callback_uri
}

View File

@ -24,6 +24,7 @@ import urllib
import webob
from webob import exc
from oslo_config import cfg
from oslo_serialization import jsonutils
from tacker.api.vnflcm.v1 import controller
@ -37,6 +38,7 @@ from tacker.extensions import vnfm
from tacker.manager import TackerManager
from tacker import objects
from tacker.objects import fields
from tacker.objects import vnf_lcm_subscriptions as subscription_obj
from tacker.tests import constants
from tacker.tests.unit import base
from tacker.tests.unit.db import utils
@ -3731,3 +3733,143 @@ class TestController(base.TestCase):
self.assertEqual(
"Can not find requested vnf: %s" % constants.INVALID_UUID,
resp.json['itemNotFound']['message'])
@mock.patch.object(TackerManager, 'get_service_plugins',
return_value={'VNFM': FakeVNFMPlugin()})
@ddt.data('operationTypes', 'operationStates')
def test_register_subscription_operation_mismatch(
self, attribute, mock_get_service_plugins):
body = {
'callbackUri': 'http://sample_callback_uri',
'filter': {
'notificationType': [
'VnfLcmOperationOccurrenceNotification'],
attribute: ['sample_operation']
}
}
req = fake_request.HTTPRequest.blank(
'/subscriptions')
req.body = jsonutils.dump_as_bytes(body)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
resp = req.get_response(self.app)
self.assertEqual(http_client.BAD_REQUEST, resp.status_code)
@mock.patch.object(TackerManager, 'get_service_plugins',
return_value={'VNFM': FakeVNFMPlugin()})
def test_register_subscription_operation_notification_mismatch(
self, mock_get_service_plugins):
body = {
'callbackUri': 'http://sample_callback_uri',
'filter': {
'notificationTypes': ['sample_notification'],
}
}
req = fake_request.HTTPRequest.blank(
'/subscriptions')
req.body = jsonutils.dump_as_bytes(body)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
resp = req.get_response(self.app)
self.assertEqual(http_client.BAD_REQUEST, resp.status_code)
@mock.patch.object(subscription_obj.LccnSubscriptionRequest, 'create')
@mock.patch.object(TackerManager, 'get_service_plugins',
return_value={'VNFM': FakeVNFMPlugin()})
def test_register_subscription_vnf_instance_subscription_filter(
self, mock_get_service_plugins, mock_create):
cfg.CONF.set_override('test_callback_uri', False,
group='vnf_lcm')
body = {
'callbackUri': 'http://sample_callback_uri',
'filter': {
'notificationTypes': ['VnfLcmOperationOccurrenceNotification'],
'vnfInstanceSubscriptionFilter': {
"vnfdIds": [],
"vnfProductsFromProviders": {
"vnfProvider": "Vnf Provider 1",
"vnfProducts": [
{
"vnfProductName": "Vnf Product 1",
"versions": [
{
"vnfSoftwareVersion": "v1",
"vnfdVersions": ["vnfd.v1.1"]
}
]
}
]
}
}
}
}
req = fake_request.HTTPRequest.blank(
'/subscriptions')
req.body = jsonutils.dump_as_bytes(body)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
resp = req.get_response(self.app)
self.assertEqual(http_client.CREATED, resp.status_code)
@mock.patch.object(TackerManager, 'get_service_plugins',
return_value={'VNFM': FakeVNFMPlugin()})
def test_register_subscription_vnf_instance_subscription_filter_error(
self, mock_get_service_plugins):
body = {
'callbackUri': 'http://sample_callback_uri',
'filter': {
'notificationTypes': ['VnfLcmOperationOccurrenceNotification'],
'vnfInstanceSubscriptionFilter': {
"vnfdIds": [],
"vnfProductsFromProviders": {
"vnfProducts": [
{
"vnfProductName": "Vnf Product 1",
"versions": [
{
"vnfSoftwareVersion": "v1",
"vnfdVersions": ["vnfd.v1.1"]
}
]
}
]
}
}
}
}
req = fake_request.HTTPRequest.blank(
'/subscriptions')
req.body = jsonutils.dump_as_bytes(body)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
resp = req.get_response(self.app)
self.assertEqual(http_client.BAD_REQUEST, resp.status_code)
@mock.patch.object(subscription_obj.LccnSubscriptionRequest, 'create')
@mock.patch.object(TackerManager, 'get_service_plugins',
return_value={'VNFM': FakeVNFMPlugin()})
def test_register_subscription(
self, mock_get_service_plugins, mock_save):
cfg.CONF.set_override('test_callback_uri', False,
group='vnf_lcm')
body = {
'callbackUri': 'http://sample_callback_uri'
}
req = fake_request.HTTPRequest.blank(
'/subscriptions')
req.body = jsonutils.dump_as_bytes(body)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
resp = req.get_response(self.app)
self.assertEqual(http_client.CREATED, resp.status_code)