From c74cad521cf31fde8204008abd1b1d4be7d79c4e Mon Sep 17 00:00:00 2001 From: "ajay.parja" Date: Wed, 16 Oct 2019 21:04:41 +0530 Subject: [PATCH] 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 Change-Id: I6d60e87b48a6703362dcd30975f300f524f8ca7a Implements: bp enhance-vnf-package-support-part1 --- api-ref/source/v1/parameters.yaml | 26 ++++ .../vnf-packages-patch-request.json | 7 + .../vnf-packages-patch-response.json | 6 + api-ref/source/v1/vnf_packages.inc | 58 ++++++++ tacker/api/schemas/vnf_packages.py | 19 +++ tacker/api/views/vnf_packages.py | 24 +++ tacker/api/vnfpkgm/v1/controller.py | 103 ++++++++----- tacker/api/vnfpkgm/v1/router.py | 3 +- tacker/common/exceptions.py | 5 + .../alembic_migrations/versions/HEAD | 2 +- ...ue_constraint_on_vnf_packages_user_data.py | 40 +++++ tacker/objects/vnf_package.py | 92 ++++++++++-- tacker/policies/vnf_package.py | 11 ++ .../functional/vnfpkgm/test_vnf_package.py | 39 ++++- tacker/tests/unit/db/test_vnf_package.py | 29 +++- tacker/tests/unit/vnfpkgm/fakes.py | 37 ++++- tacker/tests/unit/vnfpkgm/test_controller.py | 137 +++++++++++++++++- 17 files changed, 577 insertions(+), 61 deletions(-) create mode 100644 api-ref/source/v1/samples/vnf_packages/vnf-packages-patch-request.json create mode 100644 api-ref/source/v1/samples/vnf_packages/vnf-packages-patch-response.json create mode 100644 tacker/db/migration/alembic_migrations/versions/abbef484b34c_modify_unique_constraint_on_vnf_packages_user_data.py diff --git a/api-ref/source/v1/parameters.yaml b/api-ref/source/v1/parameters.yaml index 2b1305f5e..d311b4793 100644 --- a/api-ref/source/v1/parameters.yaml +++ b/api-ref/source/v1/parameters.yaml @@ -498,6 +498,19 @@ tenant_id_opt: in: body required: false 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: description: | The date and time when the resource was updated. @@ -506,6 +519,19 @@ updated_at: in: body required: true 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: description: | Usage state of the VNF package. diff --git a/api-ref/source/v1/samples/vnf_packages/vnf-packages-patch-request.json b/api-ref/source/v1/samples/vnf_packages/vnf-packages-patch-request.json new file mode 100644 index 000000000..2fafdc0e4 --- /dev/null +++ b/api-ref/source/v1/samples/vnf_packages/vnf-packages-patch-request.json @@ -0,0 +1,7 @@ +{ + "operationalState": "DISABLED", + "userDefinedData": { + "key1": "value1", + "key2": "value2" + } +} \ No newline at end of file diff --git a/api-ref/source/v1/samples/vnf_packages/vnf-packages-patch-response.json b/api-ref/source/v1/samples/vnf_packages/vnf-packages-patch-response.json new file mode 100644 index 000000000..2e73edabb --- /dev/null +++ b/api-ref/source/v1/samples/vnf_packages/vnf-packages-patch-response.json @@ -0,0 +1,6 @@ +{ + "operationalState":"DISABLED", + "userDefinedData":{ + "abc":"xyz" + } +} \ No newline at end of file diff --git a/api-ref/source/v1/vnf_packages.inc b/api-ref/source/v1/vnf_packages.inc index 71719119d..75da39385 100644 --- a/api-ref/source/v1/vnf_packages.inc +++ b/api-ref/source/v1/vnf_packages.inc @@ -247,3 +247,61 @@ Request Parameters - addressInformation: addressInformation - userName: userName - 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 diff --git a/tacker/api/schemas/vnf_packages.py b/tacker/api/schemas/vnf_packages.py index 4e966d5be..7bbc07b04 100644 --- a/tacker/api/schemas/vnf_packages.py +++ b/tacker/api/schemas/vnf_packages.py @@ -19,6 +19,7 @@ Schema for vnf packages create API. """ from tacker.api.validation import parameter_types +from tacker.objects.fields import PackageOperationalStateType create = { 'type': 'object', @@ -48,3 +49,21 @@ upload_from_uri = { 'required': ['addressInformation'], '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 +} diff --git a/tacker/api/views/vnf_packages.py b/tacker/api/views/vnf_packages.py index 1ca358ae6..da65c0b0c 100644 --- a/tacker/api/views/vnf_packages.py +++ b/tacker/api/views/vnf_packages.py @@ -92,6 +92,19 @@ class ViewBuilder(object): 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): return self._get_vnf_package(vnf_package) @@ -103,3 +116,14 @@ class ViewBuilder(object): def index(self, request, vnf_packages): return {'vnf_packages': [self._get_vnf_package( 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 diff --git a/tacker/api/vnfpkgm/v1/controller.py b/tacker/api/vnfpkgm/v1/controller.py index fb4882143..4f34f6f62 100644 --- a/tacker/api/vnfpkgm/v1/controller.py +++ b/tacker/api/vnfpkgm/v1/controller.py @@ -48,6 +48,20 @@ class VnfPkgmController(wsgi.Controller): self.rpc_api = vnf_pkgm_rpc.VNFPackageRPCAPI() 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.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN)) @validation.schema(vnf_packages.create) @@ -108,17 +122,7 @@ class VnfPkgmController(wsgi.Controller): context = request.environ['tacker.context'] context.can(vnf_package_policies.VNFPKGM % 'delete') - # 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) + vnf_package = self._get_vnf_package(id, request) if (vnf_package.operational_state == fields.PackageOperationalStateType.ENABLED or @@ -143,17 +147,7 @@ class VnfPkgmController(wsgi.Controller): context = request.environ['tacker.context'] context.can(vnf_package_policies.VNFPKGM % 'upload_package_content') - # 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) + vnf_package = self._get_vnf_package(id, request) if vnf_package.onboarding_state != \ fields.PackageOnboardingStateType.CREATED: @@ -197,10 +191,7 @@ class VnfPkgmController(wsgi.Controller): context = request.environ['tacker.context'] context.can(vnf_package_policies.VNFPKGM % 'upload_from_uri') - # 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) + vnf_package = self._get_vnf_package(id, request) url = body['addressInformation'] try: @@ -213,13 +204,6 @@ class VnfPkgmController(wsgi.Controller): if hasattr(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 != \ fields.PackageOnboardingStateType.CREATED: msg = _("VNF Package %(id)s onboarding state is not " @@ -238,6 +222,59 @@ class VnfPkgmController(wsgi.Controller): user_name=body.get('userName'), 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(): body_deserializers = { diff --git a/tacker/api/vnfpkgm/v1/router.py b/tacker/api/vnfpkgm/v1/router.py index a59a4e550..bdfadd375 100644 --- a/tacker/api/vnfpkgm/v1/router.py +++ b/tacker/api/vnfpkgm/v1/router.py @@ -58,7 +58,8 @@ class VnfpkgmAPIRouter(wsgi.Router): methods, controller, default_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}", methods, controller, default_resource) diff --git a/tacker/common/exceptions.py b/tacker/common/exceptions.py index 94ecbf4c5..796e2b527 100644 --- a/tacker/common/exceptions.py +++ b/tacker/common/exceptions.py @@ -249,3 +249,8 @@ class LimitExceeded(TackerException): self.retry_after = (int(kwargs['retry']) if kwargs.get('retry') else None) 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.") diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index 84b7b9c00..65aa0c6e3 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -9d425296f2c3 \ No newline at end of file +abbef484b34c \ No newline at end of file diff --git a/tacker/db/migration/alembic_migrations/versions/abbef484b34c_modify_unique_constraint_on_vnf_packages_user_data.py b/tacker/db/migration/alembic_migrations/versions/abbef484b34c_modify_unique_constraint_on_vnf_packages_user_data.py new file mode 100644 index 000000000..6a6f3e455 --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/abbef484b34c_modify_unique_constraint_on_vnf_packages_user_data.py @@ -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']) diff --git a/tacker/objects/vnf_package.py b/tacker/objects/vnf_package.py index ea0c4d79e..79826fe97 100644 --- a/tacker/objects/vnf_package.py +++ b/tacker/objects/vnf_package.py @@ -12,7 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_db import exception as db_exc from oslo_log import log as logging +from oslo_utils import excutils from oslo_utils import timeutils from oslo_utils import uuidutils 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, max_retries=10): 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 = [] - 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) + return user_data + except db_exc.DBDuplicateEntry: + # a concurrent transaction has been committed, + # try again unless this was the last attempt + with excutils.save_and_reraise_exception() as context: + if attempt < max_retries - 1: + context.reraise = False + else: + 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 @@ -111,13 +165,21 @@ def _vnf_package_list_by_filters(context, read_deleted=None, **filters): @db_api.context_manager.writer -def _vnf_package_update(context, package_uuid, values, columns_to_join=None): - - vnf_package = _vnf_package_get_by_id(context, package_uuid, - columns_to_join=columns_to_join) - vnf_package.update(values) +def _update_vnf_package_except_user_data(context, vnf_package): 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 diff --git a/tacker/policies/vnf_package.py b/tacker/policies/vnf_package.py index 695b9d438..c93a72ffe 100644 --- a/tacker/policies/vnf_package.py +++ b/tacker/policies/vnf_package.py @@ -84,6 +84,17 @@ rules = [ '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}' + } + ]), + ] diff --git a/tacker/tests/functional/vnfpkgm/test_vnf_package.py b/tacker/tests/functional/vnfpkgm/test_vnf_package.py index d1a0bf51f..c4d2ef050 100644 --- a/tacker/tests/functional/vnfpkgm/test_vnf_package.py +++ b/tacker/tests/functional/vnfpkgm/test_vnf_package.py @@ -119,7 +119,7 @@ class VnfPackageTest(base.BaseTackerTest): body = jsonutils.dumps({"userDefinedData": {"foo": "bar"}}) vnf_package = self._create_vnf_package(body) 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( '{base_path}/{id}/package_content'.format( id=vnf_package['id'], @@ -132,3 +132,40 @@ class VnfPackageTest(base.BaseTackerTest): self._delete_vnf_package(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']) diff --git a/tacker/tests/unit/db/test_vnf_package.py b/tacker/tests/unit/db/test_vnf_package.py index 0fa934df3..6b65bdc33 100644 --- a/tacker/tests/unit/db/test_vnf_package.py +++ b/tacker/tests/unit/db/test_vnf_package.py @@ -49,9 +49,10 @@ class TestVnfPackage(SqlTestCase): vnf_package_db = models.VnfPackage() vnf_package_db.update(fakes.fake_vnf_package()) vnf_package_db.save(self.context.session) + expected_result = {'abc': 'xyz'} result = vnf_package._add_user_defined_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): result = vnf_package._vnf_package_get_by_id( @@ -75,7 +76,7 @@ class TestVnfPackage(SqlTestCase): update = {'user_data': {'test': 'xyz'}} result = vnf_package._vnf_package_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): vnf_package._destroy_vnf_package(self.context, @@ -92,3 +93,27 @@ class TestVnfPackage(SqlTestCase): self.context, vnf_pack_list_obj, response, None) self.assertIsInstance(result, objects.VnfPackagesList) 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) diff --git a/tacker/tests/unit/vnfpkgm/fakes.py b/tacker/tests/unit/vnfpkgm/fakes.py index 3d26da810..7e99ca66e 100644 --- a/tacker/tests/unit/vnfpkgm/fakes.py +++ b/tacker/tests/unit/vnfpkgm/fakes.py @@ -97,16 +97,43 @@ class InjectContext(wsgi.Middleware): return self.application -def return_vnf_package(): - model_obj = models.VnfPackage() - model_obj.update(fake_vnf_package()) +def fake_vnf_package_user_data(**updates): + vnf_package_user_data = { + '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 -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( context, vnf_package_obj.VnfPackage(), - return_vnf_package(), expected_attrs=None) + return_vnf_package(**updates), expected_attrs=None) return vnf_package diff --git a/tacker/tests/unit/vnfpkgm/test_controller.py b/tacker/tests/unit/vnfpkgm/test_controller.py index 02d60dfc3..20a02ee7a 100644 --- a/tacker/tests/unit/vnfpkgm/test_controller.py +++ b/tacker/tests/unit/vnfpkgm/test_controller.py @@ -158,7 +158,6 @@ class TestController(base.TestCase): file_path = "tacker/tests/etc/samples/test_data.zip" file_obj = open(file_path, "rb") 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',\ 'multihash', 'loc_meta' req = fake_request.HTTPRequest.blank( @@ -224,7 +223,6 @@ class TestController(base.TestCase): mock_url_open): body = {"addressInformation": "http://test_data.zip"} 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( '/vnf_packages/%s/package_content/upload_from_uri' % constants.UUID) @@ -274,7 +272,10 @@ class TestController(base.TestCase): self.controller.upload_vnf_package_from_uri, 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"} req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/package_content/upload_from_uri' @@ -282,3 +283,133 @@ class TestController(base.TestCase): self.assertRaises(exc.HTTPBadRequest, self.controller.upload_vnf_package_from_uri, 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)