Add VNF package update API
Added API to update information of VNF package. Implemented below API:- * PATCH /vnf_packages/{vnfPkgId} Co-authored-by: Shubham Potale <shubham.potale@nttdata.com> Change-Id: I6d60e87b48a6703362dcd30975f300f524f8ca7a Implements: bp enhance-vnf-package-support-part1
This commit is contained in:
parent
067d00371b
commit
c74cad521c
api-ref/source/v1
tacker
api
common
db/migration/alembic_migrations/versions
objects
policies
tests
@ -498,6 +498,19 @@ tenant_id_opt:
|
|||||||
in: body
|
in: body
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
update_operational_state:
|
||||||
|
description: |
|
||||||
|
New value of the operational state of the on-boarded instance of the VNF
|
||||||
|
package. Valid values are "ENABLED" and "DISABLED". See note.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
update_user_defined_data:
|
||||||
|
description: |
|
||||||
|
User defined data to be updated. For existing keys, the value is replaced.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: object
|
||||||
updated_at:
|
updated_at:
|
||||||
description: |
|
description: |
|
||||||
The date and time when the resource was updated.
|
The date and time when the resource was updated.
|
||||||
@ -506,6 +519,19 @@ updated_at:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
updated_operational_state:
|
||||||
|
description: |
|
||||||
|
Updated value of the operational state of the on-boarded instance of
|
||||||
|
the VNF package.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
updated_user_defined_data:
|
||||||
|
description: |
|
||||||
|
Updated value of user defined data.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: object
|
||||||
usageState:
|
usageState:
|
||||||
description: |
|
description: |
|
||||||
Usage state of the VNF package.
|
Usage state of the VNF package.
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"operationalState": "DISABLED",
|
||||||
|
"userDefinedData": {
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"operationalState":"DISABLED",
|
||||||
|
"userDefinedData":{
|
||||||
|
"abc":"xyz"
|
||||||
|
}
|
||||||
|
}
|
@ -247,3 +247,61 @@ Request Parameters
|
|||||||
- addressInformation: addressInformation
|
- addressInformation: addressInformation
|
||||||
- userName: userName
|
- userName: userName
|
||||||
- password: password
|
- password: password
|
||||||
|
|
||||||
|
Update VNF Package Information
|
||||||
|
==============================
|
||||||
|
|
||||||
|
.. rest_method:: PATCH /vnfpkgm/v1/vnf_packages/{vnf_package_id}
|
||||||
|
|
||||||
|
Updates the information of a VNF package.
|
||||||
|
|
||||||
|
Response Codes
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. rest_status_code:: success status.yaml
|
||||||
|
|
||||||
|
- 200
|
||||||
|
|
||||||
|
.. rest_status_code:: error status.yaml
|
||||||
|
|
||||||
|
- 400
|
||||||
|
- 401
|
||||||
|
- 403
|
||||||
|
- 404
|
||||||
|
- 409
|
||||||
|
|
||||||
|
Request Parameters
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- vnf_package_id: vnf_package_id_path
|
||||||
|
- operationalState: update_operational_state
|
||||||
|
- userDefinedData: update_user_defined_data
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
At least one of the "operationalState" or "userDefinedData" parameters
|
||||||
|
shall be present. If the VNF package is not on-boarded, the operation is
|
||||||
|
used only to update existing or add additional user defined data using the
|
||||||
|
"userDefinedData" attribute. If user passes existing user defined data
|
||||||
|
with exact same key/values pairs, then it would return 400 error.
|
||||||
|
|
||||||
|
Request Example
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. literalinclude:: samples/vnf_packages/vnf-packages-patch-request.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
Response Parameters
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- operationalState: updated_operational_state
|
||||||
|
- userDefinedData: updated_user_defined_data
|
||||||
|
|
||||||
|
Response Example
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. literalinclude:: samples/vnf_packages/vnf-packages-patch-response.json
|
||||||
|
:language: javascript
|
||||||
|
@ -19,6 +19,7 @@ Schema for vnf packages create API.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from tacker.api.validation import parameter_types
|
from tacker.api.validation import parameter_types
|
||||||
|
from tacker.objects.fields import PackageOperationalStateType
|
||||||
|
|
||||||
create = {
|
create = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@ -48,3 +49,21 @@ upload_from_uri = {
|
|||||||
'required': ['addressInformation'],
|
'required': ['addressInformation'],
|
||||||
'additionalProperties': False,
|
'additionalProperties': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Schema for vnf packages update API.
|
||||||
|
|
||||||
|
"""
|
||||||
|
patch = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'operationalState': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': list(PackageOperationalStateType.ALL),
|
||||||
|
},
|
||||||
|
'userDefinedData': parameter_types.keyvalue_pairs,
|
||||||
|
},
|
||||||
|
'anyOf': [{'required': ['operationalState']},
|
||||||
|
{'required': ['userDefinedData']}],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
@ -92,6 +92,19 @@ class ViewBuilder(object):
|
|||||||
|
|
||||||
return vnf_package_response
|
return vnf_package_response
|
||||||
|
|
||||||
|
def _get_modified_user_data(self, old_user_data, new_user_data):
|
||||||
|
# Checking for the new keys
|
||||||
|
user_data_response = {k: new_user_data[k] for k
|
||||||
|
in set(new_user_data) - set(old_user_data)}
|
||||||
|
|
||||||
|
# Checking for updation in values of existing keys
|
||||||
|
for old_key, old_value in old_user_data.items():
|
||||||
|
if old_key in new_user_data.keys() and \
|
||||||
|
new_user_data[old_key] != old_user_data[old_key]:
|
||||||
|
user_data_response[old_key] = new_user_data[old_key]
|
||||||
|
|
||||||
|
return user_data_response
|
||||||
|
|
||||||
def create(self, request, vnf_package):
|
def create(self, request, vnf_package):
|
||||||
|
|
||||||
return self._get_vnf_package(vnf_package)
|
return self._get_vnf_package(vnf_package)
|
||||||
@ -103,3 +116,14 @@ class ViewBuilder(object):
|
|||||||
def index(self, request, vnf_packages):
|
def index(self, request, vnf_packages):
|
||||||
return {'vnf_packages': [self._get_vnf_package(
|
return {'vnf_packages': [self._get_vnf_package(
|
||||||
vnf_package) for vnf_package in vnf_packages]}
|
vnf_package) for vnf_package in vnf_packages]}
|
||||||
|
|
||||||
|
def patch(self, vnf_package, new_vnf_package):
|
||||||
|
response = {}
|
||||||
|
if vnf_package.operational_state != new_vnf_package.operational_state:
|
||||||
|
response['operationalState'] = new_vnf_package.operational_state
|
||||||
|
if vnf_package.user_data != new_vnf_package.user_data:
|
||||||
|
updated_user_data = self._get_modified_user_data(
|
||||||
|
vnf_package.user_data, new_vnf_package.user_data)
|
||||||
|
response['userDefinedData'] = updated_user_data
|
||||||
|
|
||||||
|
return response
|
||||||
|
@ -48,6 +48,20 @@ class VnfPkgmController(wsgi.Controller):
|
|||||||
self.rpc_api = vnf_pkgm_rpc.VNFPackageRPCAPI()
|
self.rpc_api = vnf_pkgm_rpc.VNFPackageRPCAPI()
|
||||||
glance_store.initialize_glance_store()
|
glance_store.initialize_glance_store()
|
||||||
|
|
||||||
|
def _get_vnf_package(self, id, request):
|
||||||
|
# check if id is of type uuid format
|
||||||
|
if not uuidutils.is_uuid_like(id):
|
||||||
|
msg = _("Can not find requested vnf package: %s") % id
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
vnf_package = vnf_package_obj.VnfPackage.get_by_id(
|
||||||
|
request.context, id)
|
||||||
|
except exceptions.VnfPackageNotFound:
|
||||||
|
msg = _("Can not find requested vnf package: %s") % id
|
||||||
|
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||||
|
return vnf_package
|
||||||
|
|
||||||
@wsgi.response(http_client.CREATED)
|
@wsgi.response(http_client.CREATED)
|
||||||
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN))
|
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN))
|
||||||
@validation.schema(vnf_packages.create)
|
@validation.schema(vnf_packages.create)
|
||||||
@ -108,17 +122,7 @@ class VnfPkgmController(wsgi.Controller):
|
|||||||
context = request.environ['tacker.context']
|
context = request.environ['tacker.context']
|
||||||
context.can(vnf_package_policies.VNFPKGM % 'delete')
|
context.can(vnf_package_policies.VNFPKGM % 'delete')
|
||||||
|
|
||||||
# check if id is of type uuid format
|
vnf_package = self._get_vnf_package(id, request)
|
||||||
if not uuidutils.is_uuid_like(id):
|
|
||||||
msg = _("Can not find requested vnf package: %s") % id
|
|
||||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
vnf_package = vnf_package_obj.VnfPackage.get_by_id(
|
|
||||||
request.context, id)
|
|
||||||
except exceptions.VnfPackageNotFound:
|
|
||||||
msg = _("Can not find requested vnf package: %s") % id
|
|
||||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
|
||||||
|
|
||||||
if (vnf_package.operational_state ==
|
if (vnf_package.operational_state ==
|
||||||
fields.PackageOperationalStateType.ENABLED or
|
fields.PackageOperationalStateType.ENABLED or
|
||||||
@ -143,17 +147,7 @@ class VnfPkgmController(wsgi.Controller):
|
|||||||
context = request.environ['tacker.context']
|
context = request.environ['tacker.context']
|
||||||
context.can(vnf_package_policies.VNFPKGM % 'upload_package_content')
|
context.can(vnf_package_policies.VNFPKGM % 'upload_package_content')
|
||||||
|
|
||||||
# check if id is of type uuid format
|
vnf_package = self._get_vnf_package(id, request)
|
||||||
if not uuidutils.is_uuid_like(id):
|
|
||||||
msg = _("Can not find requested vnf package: %s") % id
|
|
||||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
vnf_package = vnf_package_obj.VnfPackage.get_by_id(
|
|
||||||
request.context, id)
|
|
||||||
except exceptions.VnfPackageNotFound:
|
|
||||||
msg = _("Can not find requested vnf package: %s") % id
|
|
||||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
|
||||||
|
|
||||||
if vnf_package.onboarding_state != \
|
if vnf_package.onboarding_state != \
|
||||||
fields.PackageOnboardingStateType.CREATED:
|
fields.PackageOnboardingStateType.CREATED:
|
||||||
@ -197,10 +191,7 @@ class VnfPkgmController(wsgi.Controller):
|
|||||||
context = request.environ['tacker.context']
|
context = request.environ['tacker.context']
|
||||||
context.can(vnf_package_policies.VNFPKGM % 'upload_from_uri')
|
context.can(vnf_package_policies.VNFPKGM % 'upload_from_uri')
|
||||||
|
|
||||||
# check if id is of type uuid format
|
vnf_package = self._get_vnf_package(id, request)
|
||||||
if not uuidutils.is_uuid_like(id):
|
|
||||||
msg = _("Can not find requested vnf package: %s") % id
|
|
||||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
|
||||||
|
|
||||||
url = body['addressInformation']
|
url = body['addressInformation']
|
||||||
try:
|
try:
|
||||||
@ -213,13 +204,6 @@ class VnfPkgmController(wsgi.Controller):
|
|||||||
if hasattr(data_iter, 'close'):
|
if hasattr(data_iter, 'close'):
|
||||||
data_iter.close()
|
data_iter.close()
|
||||||
|
|
||||||
try:
|
|
||||||
vnf_package = vnf_package_obj.VnfPackage.get_by_id(
|
|
||||||
request.context, id)
|
|
||||||
except exceptions.VnfPackageNotFound:
|
|
||||||
msg = _("Can not find requested vnf package: %s") % id
|
|
||||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
|
||||||
|
|
||||||
if vnf_package.onboarding_state != \
|
if vnf_package.onboarding_state != \
|
||||||
fields.PackageOnboardingStateType.CREATED:
|
fields.PackageOnboardingStateType.CREATED:
|
||||||
msg = _("VNF Package %(id)s onboarding state is not "
|
msg = _("VNF Package %(id)s onboarding state is not "
|
||||||
@ -238,6 +222,59 @@ class VnfPkgmController(wsgi.Controller):
|
|||||||
user_name=body.get('userName'),
|
user_name=body.get('userName'),
|
||||||
password=body.get('password'))
|
password=body.get('password'))
|
||||||
|
|
||||||
|
@wsgi.response(http_client.OK)
|
||||||
|
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN,
|
||||||
|
http_client.NOT_FOUND, http_client.CONFLICT))
|
||||||
|
@validation.schema(vnf_packages.patch)
|
||||||
|
def patch(self, request, id, body):
|
||||||
|
context = request.environ['tacker.context']
|
||||||
|
context.can(vnf_package_policies.VNFPKGM % 'patch')
|
||||||
|
|
||||||
|
old_vnf_package = self._get_vnf_package(id, request)
|
||||||
|
vnf_package = old_vnf_package.obj_clone()
|
||||||
|
|
||||||
|
user_data = body.get('userDefinedData')
|
||||||
|
operational_state = body.get('operationalState')
|
||||||
|
|
||||||
|
if operational_state:
|
||||||
|
if vnf_package.onboarding_state == \
|
||||||
|
fields.PackageOnboardingStateType.ONBOARDED:
|
||||||
|
if vnf_package.operational_state == operational_state:
|
||||||
|
msg = _("VNF Package %(id)s is already in "
|
||||||
|
"%(operationState)s operational state") % {
|
||||||
|
"id": id,
|
||||||
|
"operationState": vnf_package.operational_state}
|
||||||
|
raise webob.exc.HTTPConflict(explanation=msg)
|
||||||
|
else:
|
||||||
|
# update vnf_package operational state,
|
||||||
|
# if vnf_package Onboarding State is ONBOARDED
|
||||||
|
vnf_package.operational_state = operational_state
|
||||||
|
else:
|
||||||
|
if not user_data:
|
||||||
|
msg = _("Updating operational state is not allowed for VNF"
|
||||||
|
" Package %(id)s when onboarding state is not "
|
||||||
|
"%(onboarded)s")
|
||||||
|
raise webob.exc.HTTPBadRequest(
|
||||||
|
explanation=msg % {"id": id, "onboarded": fields.
|
||||||
|
PackageOnboardingStateType.ONBOARDED})
|
||||||
|
# update user data
|
||||||
|
if user_data:
|
||||||
|
for key, value in list(user_data.items()):
|
||||||
|
if vnf_package.user_data.get(key) == value:
|
||||||
|
del user_data[key]
|
||||||
|
|
||||||
|
if not user_data:
|
||||||
|
msg = _("The userDefinedData provided in update request is as"
|
||||||
|
" the existing userDefinedData of vnf package %(id)s."
|
||||||
|
" Nothing to update.")
|
||||||
|
raise webob.exc.HTTPConflict(
|
||||||
|
explanation=msg % {"id": id})
|
||||||
|
vnf_package.user_data = user_data
|
||||||
|
|
||||||
|
vnf_package.save()
|
||||||
|
|
||||||
|
return self._view_builder.patch(old_vnf_package, vnf_package)
|
||||||
|
|
||||||
|
|
||||||
def create_resource():
|
def create_resource():
|
||||||
body_deserializers = {
|
body_deserializers = {
|
||||||
|
@ -58,7 +58,8 @@ class VnfpkgmAPIRouter(wsgi.Router):
|
|||||||
methods, controller, default_resource)
|
methods, controller, default_resource)
|
||||||
|
|
||||||
# Allowed methods on /vnf_packages/{id} resource
|
# Allowed methods on /vnf_packages/{id} resource
|
||||||
methods = {"DELETE": "delete", "GET": "show"}
|
methods = {"DELETE": "delete", "GET": "show",
|
||||||
|
"PATCH": "patch"}
|
||||||
self._setup_route(mapper, "/vnf_packages/{id}",
|
self._setup_route(mapper, "/vnf_packages/{id}",
|
||||||
methods, controller, default_resource)
|
methods, controller, default_resource)
|
||||||
|
|
||||||
|
@ -249,3 +249,8 @@ class LimitExceeded(TackerException):
|
|||||||
self.retry_after = (int(kwargs['retry']) if kwargs.get('retry')
|
self.retry_after = (int(kwargs['retry']) if kwargs.get('retry')
|
||||||
else None)
|
else None)
|
||||||
super(LimitExceeded, self).__init__(*args, **kwargs)
|
super(LimitExceeded, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UserDataUpdateCreateFailed(TackerException):
|
||||||
|
msg_fmt = _("User data for VNF package %(id)s cannot be updated "
|
||||||
|
"or created after %(retries)d retries.")
|
||||||
|
@ -1 +1 @@
|
|||||||
9d425296f2c3
|
abbef484b34c
|
40
tacker/db/migration/alembic_migrations/versions/abbef484b34c_modify_unique_constraint_on_vnf_packages_user_data.py
Normal file
40
tacker/db/migration/alembic_migrations/versions/abbef484b34c_modify_unique_constraint_on_vnf_packages_user_data.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Copyright 2019 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""modify_unique_constraint_on_vnf_packages_user_data
|
||||||
|
|
||||||
|
Revision ID: abbef484b34c
|
||||||
|
Revises: 9d425296f2c3
|
||||||
|
Create Date: 2019-11-18 19:34:26.853715
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'abbef484b34c'
|
||||||
|
down_revision = '9d425296f2c3'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(active_plugins=None, options=None):
|
||||||
|
op.drop_constraint(
|
||||||
|
constraint_name='uniq_vnf_packages_user_data0idid0key0deleted',
|
||||||
|
table_name='vnf_packages_user_data',
|
||||||
|
type_='unique')
|
||||||
|
|
||||||
|
op.create_unique_constraint(
|
||||||
|
constraint_name='uniq_vnf_packages_user_data0package_uuid0key0deleted',
|
||||||
|
table_name='vnf_packages_user_data',
|
||||||
|
columns=['package_uuid', 'key', 'deleted'])
|
@ -12,7 +12,9 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_db import exception as db_exc
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
from oslo_versionedobjects import base as ovoo_base
|
from oslo_versionedobjects import base as ovoo_base
|
||||||
@ -37,17 +39,69 @@ LOG = logging.getLogger(__name__)
|
|||||||
def _add_user_defined_data(context, package_uuid, user_data,
|
def _add_user_defined_data(context, package_uuid, user_data,
|
||||||
max_retries=10):
|
max_retries=10):
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
with db_api.context_manager.writer.using(context):
|
try:
|
||||||
|
with db_api.context_manager.writer.using(context):
|
||||||
|
new_entries = []
|
||||||
|
for key, value in user_data.items():
|
||||||
|
new_entries.append({"key": key,
|
||||||
|
"value": value,
|
||||||
|
"package_uuid": package_uuid})
|
||||||
|
if new_entries:
|
||||||
|
context.session.execute(
|
||||||
|
models.VnfPackageUserData.__table__.insert(None),
|
||||||
|
new_entries)
|
||||||
|
|
||||||
new_entries = []
|
return user_data
|
||||||
for key, value in user_data.items():
|
except db_exc.DBDuplicateEntry:
|
||||||
new_entries.append({"key": key,
|
# a concurrent transaction has been committed,
|
||||||
"value": value,
|
# try again unless this was the last attempt
|
||||||
"package_uuid": package_uuid})
|
with excutils.save_and_reraise_exception() as context:
|
||||||
if new_entries:
|
if attempt < max_retries - 1:
|
||||||
context.session.execute(
|
context.reraise = False
|
||||||
models.VnfPackageUserData.__table__.insert(None),
|
else:
|
||||||
new_entries)
|
raise exceptions.UserDataUpdateCreateFailed(
|
||||||
|
id=package_uuid, retries=max_retries)
|
||||||
|
|
||||||
|
|
||||||
|
def _vnf_package_user_data_get_query(context, package_uuid, model):
|
||||||
|
return api.model_query(context, model, read_deleted="no", project_only=True).\
|
||||||
|
filter_by(package_uuid=package_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@db_api.context_manager.writer
|
||||||
|
def _update_user_defined_data(context, package_uuid, user_data):
|
||||||
|
model = models.VnfPackageUserData
|
||||||
|
user_data = user_data.copy()
|
||||||
|
session = context.session
|
||||||
|
with session.begin(subtransactions=True):
|
||||||
|
# Get existing user_data
|
||||||
|
db_user_data = _vnf_package_user_data_get_query(context, package_uuid,
|
||||||
|
model).all()
|
||||||
|
save = []
|
||||||
|
skip = []
|
||||||
|
# We only want to send changed user_data.
|
||||||
|
for row in db_user_data:
|
||||||
|
if row.key in user_data:
|
||||||
|
value = user_data.pop(row.key)
|
||||||
|
if row.value != value:
|
||||||
|
# ORM objects will not be saved until we do the bulk save
|
||||||
|
row.value = value
|
||||||
|
save.append(row)
|
||||||
|
continue
|
||||||
|
skip.append(row)
|
||||||
|
|
||||||
|
# We also want to save non-existent user_data
|
||||||
|
save.extend(model(key=key, value=value, package_uuid=package_uuid)
|
||||||
|
for key, value in user_data.items())
|
||||||
|
# Do a bulk save
|
||||||
|
if save:
|
||||||
|
session.bulk_save_objects(save, update_changed_only=True)
|
||||||
|
|
||||||
|
# Construct result dictionary with current user_data
|
||||||
|
save.extend(skip)
|
||||||
|
result = {row['key']: row['value'] for row in save}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@db_api.context_manager.reader
|
@db_api.context_manager.reader
|
||||||
@ -111,13 +165,21 @@ def _vnf_package_list_by_filters(context, read_deleted=None, **filters):
|
|||||||
|
|
||||||
|
|
||||||
@db_api.context_manager.writer
|
@db_api.context_manager.writer
|
||||||
def _vnf_package_update(context, package_uuid, values, columns_to_join=None):
|
def _update_vnf_package_except_user_data(context, vnf_package):
|
||||||
|
|
||||||
vnf_package = _vnf_package_get_by_id(context, package_uuid,
|
|
||||||
columns_to_join=columns_to_join)
|
|
||||||
vnf_package.update(values)
|
|
||||||
vnf_package.save(session=context.session)
|
vnf_package.save(session=context.session)
|
||||||
|
|
||||||
|
|
||||||
|
def _vnf_package_update(context, package_uuid, values, columns_to_join=None):
|
||||||
|
user_data = values.pop('user_data', None)
|
||||||
|
if user_data:
|
||||||
|
_update_user_defined_data(context, package_uuid, user_data)
|
||||||
|
|
||||||
|
vnf_package = _vnf_package_get_by_id(
|
||||||
|
context, package_uuid, columns_to_join=columns_to_join)
|
||||||
|
if values:
|
||||||
|
vnf_package.update(values)
|
||||||
|
_update_vnf_package_except_user_data(context, vnf_package)
|
||||||
|
|
||||||
return vnf_package
|
return vnf_package
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,6 +84,17 @@ rules = [
|
|||||||
'upload_from_uri'
|
'upload_from_uri'
|
||||||
}
|
}
|
||||||
]),
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=VNFPKGM % 'patch',
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||||
|
description="update information of vnf package.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'PATCH',
|
||||||
|
'path': '/vnf_packages/{vnf_package_id}'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ class VnfPackageTest(base.BaseTackerTest):
|
|||||||
body = jsonutils.dumps({"userDefinedData": {"foo": "bar"}})
|
body = jsonutils.dumps({"userDefinedData": {"foo": "bar"}})
|
||||||
vnf_package = self._create_vnf_package(body)
|
vnf_package = self._create_vnf_package(body)
|
||||||
file_path = self._get_csar_file_path("sample_vnf_package_csar.zip")
|
file_path = self._get_csar_file_path("sample_vnf_package_csar.zip")
|
||||||
with open(file_path, 'r') as file_object:
|
with open(file_path, 'rb') as file_object:
|
||||||
resp, resp_body = self.http_client.do_request(
|
resp, resp_body = self.http_client.do_request(
|
||||||
'{base_path}/{id}/package_content'.format(
|
'{base_path}/{id}/package_content'.format(
|
||||||
id=vnf_package['id'],
|
id=vnf_package['id'],
|
||||||
@ -132,3 +132,40 @@ class VnfPackageTest(base.BaseTackerTest):
|
|||||||
|
|
||||||
self._delete_vnf_package(vnf_package['id'])
|
self._delete_vnf_package(vnf_package['id'])
|
||||||
self._wait_for_delete(vnf_package['id'])
|
self._wait_for_delete(vnf_package['id'])
|
||||||
|
|
||||||
|
def test_patch_in_onboarded_state(self):
|
||||||
|
user_data = jsonutils.dumps(
|
||||||
|
{"userDefinedData": {"key1": "val1", "key2": "val2",
|
||||||
|
"key3": "val3"}})
|
||||||
|
vnf_package = self._create_vnf_package(user_data)
|
||||||
|
|
||||||
|
update_req_body = jsonutils.dumps(
|
||||||
|
{"operationalState": "DISABLED",
|
||||||
|
"userDefinedData": {"key1": "changed_val1",
|
||||||
|
"key2": "val2", "new_key": "new_val"}})
|
||||||
|
|
||||||
|
expected_result = {"operationalState": "DISABLED",
|
||||||
|
"userDefinedData": {
|
||||||
|
"key1": "changed_val1", "new_key": "new_val"}}
|
||||||
|
|
||||||
|
file_path = self._get_csar_file_path("sample_vnf_package_csar.zip")
|
||||||
|
with open(file_path, 'rb') as file_object:
|
||||||
|
resp, resp_body = self.http_client.do_request(
|
||||||
|
'{base_path}/{id}/package_content'.format(
|
||||||
|
id=vnf_package['id'],
|
||||||
|
base_path=self.base_url),
|
||||||
|
"PUT", body=file_object, content_type='application/zip')
|
||||||
|
|
||||||
|
self.assertEqual(202, resp.status_code)
|
||||||
|
self._wait_for_onboard(vnf_package['id'])
|
||||||
|
|
||||||
|
# Update vnf package which is onboarded
|
||||||
|
resp, resp_body = self.http_client.do_request(
|
||||||
|
'{base_path}/{id}'.format(id=vnf_package['id'],
|
||||||
|
base_path=self.base_url),
|
||||||
|
"PATCH", content_type='application/json', body=update_req_body)
|
||||||
|
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
self.assertEqual(expected_result, resp_body)
|
||||||
|
self._delete_vnf_package(vnf_package['id'])
|
||||||
|
self._wait_for_delete(vnf_package['id'])
|
||||||
|
@ -49,9 +49,10 @@ class TestVnfPackage(SqlTestCase):
|
|||||||
vnf_package_db = models.VnfPackage()
|
vnf_package_db = models.VnfPackage()
|
||||||
vnf_package_db.update(fakes.fake_vnf_package())
|
vnf_package_db.update(fakes.fake_vnf_package())
|
||||||
vnf_package_db.save(self.context.session)
|
vnf_package_db.save(self.context.session)
|
||||||
|
expected_result = {'abc': 'xyz'}
|
||||||
result = vnf_package._add_user_defined_data(
|
result = vnf_package._add_user_defined_data(
|
||||||
self.context, vnf_package_db.id, vnf_package_db.user_data)
|
self.context, vnf_package_db.id, vnf_package_db.user_data)
|
||||||
self.assertEqual(None, result)
|
self.assertEqual(expected_result, result)
|
||||||
|
|
||||||
def test_vnf_package_get_by_id(self):
|
def test_vnf_package_get_by_id(self):
|
||||||
result = vnf_package._vnf_package_get_by_id(
|
result = vnf_package._vnf_package_get_by_id(
|
||||||
@ -75,7 +76,7 @@ class TestVnfPackage(SqlTestCase):
|
|||||||
update = {'user_data': {'test': 'xyz'}}
|
update = {'user_data': {'test': 'xyz'}}
|
||||||
result = vnf_package._vnf_package_update(
|
result = vnf_package._vnf_package_update(
|
||||||
self.context, self.vnf_package.id, update)
|
self.context, self.vnf_package.id, update)
|
||||||
self.assertEqual({'test': 'xyz'}, result.user_data)
|
self.assertEqual({'test': 'xyz', 'abc': 'xyz'}, result.metadetails)
|
||||||
|
|
||||||
def test_destroy_vnf_package(self):
|
def test_destroy_vnf_package(self):
|
||||||
vnf_package._destroy_vnf_package(self.context,
|
vnf_package._destroy_vnf_package(self.context,
|
||||||
@ -92,3 +93,27 @@ class TestVnfPackage(SqlTestCase):
|
|||||||
self.context, vnf_pack_list_obj, response, None)
|
self.context, vnf_pack_list_obj, response, None)
|
||||||
self.assertIsInstance(result, objects.VnfPackagesList)
|
self.assertIsInstance(result, objects.VnfPackagesList)
|
||||||
self.assertTrue(result.objects[0].id)
|
self.assertTrue(result.objects[0].id)
|
||||||
|
|
||||||
|
def test_patch_user_data_existing_key_with_same_value(self):
|
||||||
|
update = {'user_data': {'abc': 'xyz'}}
|
||||||
|
result = vnf_package._vnf_package_update(
|
||||||
|
self.context, self.vnf_package.id, update)
|
||||||
|
self.assertEqual({'abc': 'xyz'}, result.metadetails)
|
||||||
|
|
||||||
|
def test_patch_user_data_existing_key_new_value(self):
|
||||||
|
update = {'user_data': {'abc': 'val1'}}
|
||||||
|
result = vnf_package._vnf_package_update(
|
||||||
|
self.context, self.vnf_package.id, update)
|
||||||
|
self.assertEqual({'abc': 'val1'}, result.metadetails)
|
||||||
|
|
||||||
|
def test_patch_user_data_with_new_key_value(self):
|
||||||
|
update = {'user_data': {'test': '123'}}
|
||||||
|
result = vnf_package._vnf_package_update(
|
||||||
|
self.context, self.vnf_package.id, update)
|
||||||
|
self.assertEqual({'test': '123', 'abc': 'xyz'}, result.metadetails)
|
||||||
|
|
||||||
|
def test_patch_user_data_add_key_value_and_modify_value(self):
|
||||||
|
update = {'user_data': {'test': 'val1', 'abc': 'val2'}}
|
||||||
|
result = vnf_package._vnf_package_update(
|
||||||
|
self.context, self.vnf_package.id, update)
|
||||||
|
self.assertEqual({'test': 'val1', 'abc': 'val2'}, result.metadetails)
|
||||||
|
@ -97,16 +97,43 @@ class InjectContext(wsgi.Middleware):
|
|||||||
return self.application
|
return self.application
|
||||||
|
|
||||||
|
|
||||||
def return_vnf_package():
|
def fake_vnf_package_user_data(**updates):
|
||||||
model_obj = models.VnfPackage()
|
vnf_package_user_data = {
|
||||||
model_obj.update(fake_vnf_package())
|
'key': 'key',
|
||||||
|
'value': 'value',
|
||||||
|
'package_uuid': constants.UUID,
|
||||||
|
'id': constants.UUID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if updates:
|
||||||
|
vnf_package_user_data.update(updates)
|
||||||
|
|
||||||
|
return vnf_package_user_data
|
||||||
|
|
||||||
|
|
||||||
|
def return_vnf_package_user_data(**updates):
|
||||||
|
model_obj = models.VnfPackageUserData()
|
||||||
|
model_obj.update(fake_vnf_package_user_data(**updates))
|
||||||
return model_obj
|
return model_obj
|
||||||
|
|
||||||
|
|
||||||
def return_vnfpkg_obj():
|
def return_vnf_package(**updates):
|
||||||
|
model_obj = models.VnfPackage()
|
||||||
|
if 'user_data' in updates:
|
||||||
|
metadata = []
|
||||||
|
for key, value in updates.pop('user_data').items():
|
||||||
|
vnf_package_user_data = return_vnf_package_user_data(
|
||||||
|
**{'key': key, 'value': value})
|
||||||
|
metadata.extend([vnf_package_user_data])
|
||||||
|
model_obj._metadata = metadata
|
||||||
|
model_obj.update(fake_vnf_package(**updates))
|
||||||
|
return model_obj
|
||||||
|
|
||||||
|
|
||||||
|
def return_vnfpkg_obj(**updates):
|
||||||
vnf_package = vnf_package_obj.VnfPackage._from_db_object(
|
vnf_package = vnf_package_obj.VnfPackage._from_db_object(
|
||||||
context, vnf_package_obj.VnfPackage(),
|
context, vnf_package_obj.VnfPackage(),
|
||||||
return_vnf_package(), expected_attrs=None)
|
return_vnf_package(**updates), expected_attrs=None)
|
||||||
return vnf_package
|
return vnf_package
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,7 +158,6 @@ class TestController(base.TestCase):
|
|||||||
file_path = "tacker/tests/etc/samples/test_data.zip"
|
file_path = "tacker/tests/etc/samples/test_data.zip"
|
||||||
file_obj = open(file_path, "rb")
|
file_obj = open(file_path, "rb")
|
||||||
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj()
|
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj()
|
||||||
mock_vnf_pack_save.return_value = fakes.return_vnfpkg_obj()
|
|
||||||
mock_glance_store.return_value = 'location', 'size', 'checksum',\
|
mock_glance_store.return_value = 'location', 'size', 'checksum',\
|
||||||
'multihash', 'loc_meta'
|
'multihash', 'loc_meta'
|
||||||
req = fake_request.HTTPRequest.blank(
|
req = fake_request.HTTPRequest.blank(
|
||||||
@ -224,7 +223,6 @@ class TestController(base.TestCase):
|
|||||||
mock_url_open):
|
mock_url_open):
|
||||||
body = {"addressInformation": "http://test_data.zip"}
|
body = {"addressInformation": "http://test_data.zip"}
|
||||||
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj()
|
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj()
|
||||||
mock_vnf_pack_save.return_value = fakes.return_vnfpkg_obj()
|
|
||||||
req = fake_request.HTTPRequest.blank(
|
req = fake_request.HTTPRequest.blank(
|
||||||
'/vnf_packages/%s/package_content/upload_from_uri'
|
'/vnf_packages/%s/package_content/upload_from_uri'
|
||||||
% constants.UUID)
|
% constants.UUID)
|
||||||
@ -274,7 +272,10 @@ class TestController(base.TestCase):
|
|||||||
self.controller.upload_vnf_package_from_uri,
|
self.controller.upload_vnf_package_from_uri,
|
||||||
req, constants.UUID, body=body)
|
req, constants.UUID, body=body)
|
||||||
|
|
||||||
def test_upload_vnf_package_from_uri_with_invalid_url(self):
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
def test_upload_vnf_package_from_uri_with_invalid_url(
|
||||||
|
self, mock_vnf_by_id):
|
||||||
|
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj()
|
||||||
body = {"addressInformation": "http://test_data.zip"}
|
body = {"addressInformation": "http://test_data.zip"}
|
||||||
req = fake_request.HTTPRequest.blank(
|
req = fake_request.HTTPRequest.blank(
|
||||||
'/vnf_packages/%s/package_content/upload_from_uri'
|
'/vnf_packages/%s/package_content/upload_from_uri'
|
||||||
@ -282,3 +283,133 @@ class TestController(base.TestCase):
|
|||||||
self.assertRaises(exc.HTTPBadRequest,
|
self.assertRaises(exc.HTTPBadRequest,
|
||||||
self.controller.upload_vnf_package_from_uri,
|
self.controller.upload_vnf_package_from_uri,
|
||||||
req, constants.UUID, body=body)
|
req, constants.UUID, body=body)
|
||||||
|
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "save")
|
||||||
|
def test_patch(self, mock_save, mock_vnf_by_id):
|
||||||
|
update_onboarding_state = {'onboarding_state': 'ONBOARDED'}
|
||||||
|
mock_vnf_by_id.return_value = \
|
||||||
|
fakes.return_vnfpkg_obj(**update_onboarding_state)
|
||||||
|
|
||||||
|
req_body = {"operationalState": "ENABLED",
|
||||||
|
"userDefinedData": {"testKey1": "val01",
|
||||||
|
"testKey2": "val02", "testkey3": "val03"}}
|
||||||
|
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Content-Type'] = 'application/json'
|
||||||
|
req.method = 'PATCH'
|
||||||
|
req.body = jsonutils.dump_as_bytes(req_body)
|
||||||
|
resp = req.get_response(self.app)
|
||||||
|
|
||||||
|
self.assertEqual(http_client.OK, resp.status_code)
|
||||||
|
self.assertEqual(req_body, jsonutils.loads(resp.body))
|
||||||
|
|
||||||
|
def test_patch_with_empty_body(self):
|
||||||
|
body = {}
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Content-Type'] = 'application/json'
|
||||||
|
req.method = 'PATCH'
|
||||||
|
req.body = jsonutils.dump_as_bytes(body)
|
||||||
|
resp = req.get_response(self.app)
|
||||||
|
self.assertEqual(http_client.BAD_REQUEST, resp.status_code)
|
||||||
|
|
||||||
|
def test_patch_with_invalid_operational_state(self):
|
||||||
|
body = {"operationalState": "DISABLE"}
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Content-Type'] = 'application/json'
|
||||||
|
req.method = 'PATCH'
|
||||||
|
req.body = jsonutils.dump_as_bytes(body)
|
||||||
|
resp = req.get_response(self.app)
|
||||||
|
self.assertEqual(http_client.BAD_REQUEST, resp.status_code)
|
||||||
|
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "save")
|
||||||
|
def test_patch_update_existing_user_data(self, mock_save, mock_vnf_by_id):
|
||||||
|
fake_obj = fakes.return_vnfpkg_obj(
|
||||||
|
**{'user_data': {"testKey1": "val01", "testKey2": "val02",
|
||||||
|
"testKey3": "val03"}})
|
||||||
|
mock_vnf_by_id.return_value = fake_obj
|
||||||
|
req_body = {"userDefinedData": {"testKey1": "changed_val01",
|
||||||
|
"testKey2": "changed_val02",
|
||||||
|
"testKey3": "changed_val03"}}
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Content-Type'] = 'application/json'
|
||||||
|
req.method = 'PATCH'
|
||||||
|
req.body = jsonutils.dump_as_bytes(req_body)
|
||||||
|
resp = req.get_response(self.app)
|
||||||
|
self.assertEqual(http_client.OK, resp.status_code)
|
||||||
|
self.assertEqual(req_body, jsonutils.loads(resp.body))
|
||||||
|
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "save")
|
||||||
|
def test_patch_failed_with_same_user_data(self, mock_save,
|
||||||
|
mock_vnf_by_id):
|
||||||
|
body = {"userDefinedData": {"testKey1": "val01",
|
||||||
|
"testKey2": "val02", "testkey3": "val03"}}
|
||||||
|
fake_obj = fakes.return_vnfpkg_obj(
|
||||||
|
**{'user_data': body["userDefinedData"]})
|
||||||
|
mock_vnf_by_id.return_value = fake_obj
|
||||||
|
|
||||||
|
req = fake_request.HTTPRequest.blank('/vnf_packages/%s'
|
||||||
|
% constants.UUID)
|
||||||
|
self.assertRaises(exc.HTTPConflict,
|
||||||
|
self.controller.patch,
|
||||||
|
req, constants.UUID, body=body)
|
||||||
|
|
||||||
|
def test_patch_with_invalid_uuid(self):
|
||||||
|
body = {"operationalState": "ENABLED"}
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s'
|
||||||
|
% constants.INVALID_UUID)
|
||||||
|
exception = self.assertRaises(exc.HTTPNotFound,
|
||||||
|
self.controller.patch,
|
||||||
|
req, constants.INVALID_UUID, body=body)
|
||||||
|
self.assertEqual(
|
||||||
|
"Can not find requested vnf package: %s" % constants.INVALID_UUID,
|
||||||
|
exception.explanation)
|
||||||
|
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
def test_patch_with_non_existing_vnf_package(self, mock_vnf_by_id):
|
||||||
|
body = {"operationalState": "ENABLED"}
|
||||||
|
msg = _("Can not find requested vnf package: %s") % constants.UUID
|
||||||
|
mock_vnf_by_id.side_effect = exc.HTTPNotFound(explanation=msg)
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s' % constants.UUID)
|
||||||
|
exception = self.assertRaises(
|
||||||
|
exc.HTTPNotFound, self.controller.patch,
|
||||||
|
req, constants.UUID, body=body)
|
||||||
|
self.assertEqual(
|
||||||
|
"Can not find requested vnf package: %s" % constants.UUID,
|
||||||
|
exception.explanation)
|
||||||
|
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
def test_patch_failed_with_same_operational_state(self, mock_vnf_by_id):
|
||||||
|
update_operational_state = {'onboarding_state': 'ONBOARDED'}
|
||||||
|
vnf_obj = fakes.return_vnfpkg_obj(**update_operational_state)
|
||||||
|
mock_vnf_by_id.return_value = vnf_obj
|
||||||
|
body = {"operationalState": "DISABLED",
|
||||||
|
"userDefinedData": {"testKey1": "val01",
|
||||||
|
"testKey2": "val02", "testkey3": "val03"}}
|
||||||
|
req = fake_request.HTTPRequest.blank('/vnf_packages/%s'
|
||||||
|
% constants.UUID)
|
||||||
|
self.assertRaises(exc.HTTPConflict,
|
||||||
|
self.controller.patch,
|
||||||
|
req, constants.UUID, body=body)
|
||||||
|
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
def test_patch_not_in_onboarded_state(self, mock_vnf_by_id):
|
||||||
|
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj()
|
||||||
|
body = {"operationalState": "DISABLED"}
|
||||||
|
req = fake_request.HTTPRequest.blank('/vnf_packages/%s'
|
||||||
|
% constants.UUID)
|
||||||
|
self.assertRaises(exc.HTTPBadRequest,
|
||||||
|
self.controller.patch,
|
||||||
|
req, constants.UUID, body=body)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user