commit 797554a378de942f2770df791a3ed301a93641bc Author: Aldinson Esto Date: Fri Aug 21 21:11:24 2020 +0900 Support VNF update operations based on ETSI NFV Supported ETSI SOL003 Modify VNF function. Added processing for receiving Individual VNF Instances(PATCH). * PATCH /vnflcm/v1/vnf_instances/{vnfInstanceId} Implements: blueprint support-etsi-nfv-specs Spec: https://specs.openstack.org/openstack/tacker-specs/specs/victoria/support-vnf-update-api-based-on-etsi-nfv-sol.html Change-Id: If8c37bcf2fcd9094f3be4d212bdf79c322ae2705 diff --git a/api-ref/source/v1/parameters_vnflcm.yaml b/api-ref/source/v1/parameters_vnflcm.yaml index 8d774c4..5e74bdc 100644 --- a/api-ref/source/v1/parameters_vnflcm.yaml +++ b/api-ref/source/v1/parameters_vnflcm.yaml @@ -1189,6 +1189,50 @@ vnf_instance_metadata: in: body required: false type: object +vnf_instance_modify_request_description: + description: | + New value of the "vnfInstanceDescription" attribute in + "VnfInstance", or "null" to remove the attribute. + in: body + required: false + type: string +vnf_instance_modify_request_metadata: + description: | + Modifications of the "metadata" attribute in + "VnfInstance". If present, these modifications shall be + applied according to the rules of JSON Merge PATCH + in: body + required: false + type: string +vnf_instance_modify_request_name: + description: | + New value of the "vnfInstanceName" attribute in + "VnfInstance", or "null" to remove the attribute. + in: body + required: false + type: string +vnf_instance_modify_request_vim_connection_info: + description: | + New content of certain entries in the + "vimConnectionInfo" attribute array in "VnfInstance", as + defined below this table. + in: body + required: false + type: string +vnf_instance_modify_request_vnf_pkg_id: + description: | + New value of the "vnfPkgId" attribute in "VnfInstance". The + value "null" is not permitted. + in: body + required: false + type: string +vnf_instance_modify_request_vnfd_id: + description: | + New value of the "vnfdId" attribute in "VnfInstance". The + value "null" is not permitted. + in: body + required: false + type: string vnf_instance_name: description: | Name of the VNF instance. diff --git a/api-ref/source/v1/samples/vnflcm/modify-vnf-instance-request.json b/api-ref/source/v1/samples/vnflcm/modify-vnf-instance-request.json new file mode 100644 index 0000000..06d0ff1 --- /dev/null +++ b/api-ref/source/v1/samples/vnflcm/modify-vnf-instance-request.json @@ -0,0 +1,3 @@ +{ + "vnfdId": "093c38b5-a731-4593-a578-d12e42596b3e" +} diff --git a/api-ref/source/v1/vnflcm.inc b/api-ref/source/v1/vnflcm.inc index 2471048..ff6117a 100644 --- a/api-ref/source/v1/vnflcm.inc +++ b/api-ref/source/v1/vnflcm.inc @@ -570,6 +570,66 @@ Response Example .. literalinclude:: samples/vnflcm/list-vnf-instance-response.json :language: javascript +Modify a VNF instance +======================== + +.. rest_method:: POST /vnflcm/v1/vnf_instances/{vnfInstanceId} + +This method modifies an "Individual VNF instance" resource. + +Changes to the VNF configurable properties are applied to the configuration in the VNF instance, and are reflected in +the representation of this resource. Other changes are applied to the VNF instance information managed by the VNFM, +and are reflected in the representation of this resource. + +According to the ETSI NFV SOL document, there is no API request/response +specification for Etag yet, and transactions using Etag are not defined +by standardization. Therefore, the Victoria release does not support +`Error Code: 412 Precondition Failed`. Once a standard specification +for this is established, it will be installed on the tacker. + +Response Codes +-------------- + +.. rest_status_code:: success status.yaml + + - 202 + +.. rest_status_code:: error status.yaml + + - 400 + - 401 + - 403 + - 404 + - 409 + +Request Parameters +------------------ + +.. rest_parameters:: parameters_vnflcm.yaml + + - vnfInstanceId: vnf_instance_id + - vnfInstanceName: vnf_instance_modify_request_name + - vnfInstanceDescription: vnf_instance_modify_request_description + - vnfdId: vnf_instance_modify_request_vnfd_id + - metadata: vnf_instance_modify_request_metadata + - vimConnectionInfo: vnf_instance_modify_request_vim_connection_info + - id: vim_connection_info_id + - vimId: vim_connection_info_vim_id + - vimType: vim_connection_info_vim_type + - interfaceInfo: vim_connection_info_interface_info + - endpoint: vim_connection_info_interface_info_endpoint + - accessInfo: vim_connection_info_access_info + - username: vim_connection_info_access_info_username + - region: vim_connection_info_access_info_region + - password: vim_connection_info_access_info_password + - tenant: vim_connection_info_access_info_tenant + +Request Example +--------------- + +.. literalinclude:: samples/vnflcm/modify-vnf-instance-request.json + :language: javascript + Show VNF LCM operation occurrence ================================= diff --git a/tacker/api/schemas/vnf_lcm.py b/tacker/api/schemas/vnf_lcm.py index d56912b..147a13f 100644 --- a/tacker/api/schemas/vnf_lcm.py +++ b/tacker/api/schemas/vnf_lcm.py @@ -239,3 +239,16 @@ register_subscription = { 'required': ['callbackUri'], 'additionalProperties': False, } + +update = { + 'type': 'object', + 'properties': { + 'vnfdId': parameter_types.uuid, + 'vnfInstanceName': parameter_types.name_allow_zero_min_length, + 'vnfInstanceDescription': parameter_types.description, + 'vnfPkgId': parameter_types.uuid, + 'metadata': parameter_types.keyvalue_pairs, + 'vimConnectionInfo': _vimConnectionInfo, + }, + 'additionalProperties': False, +} diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index c55721a..506c44b 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -695,6 +695,100 @@ class VnfLcmController(wsgi.Controller): return self._view_builder.show_lcm_op_occs(vnf_lcm_op_occs) + @wsgi.response(http_client.OK) + @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND)) + def update(self, request, id, body): + context = request.environ['tacker.context'] + context.can(vnf_lcm_policies.VNFLCM % 'update_vnf') + + # get body + req_body = utils.convert_camelcase_to_snakecase(body) + + # According to the ETSI NFV SOL document, + # there is no API request/response + # specification for Etag yet, + # and transactions using Etag are not defined + # by standardization. Therefore, the Victoria release does not support + # `Error Code: 412 Precondition Failed`. Once a standard specification + # for this is established, it will be installed on the tacker. + + # Confirmation of update target + try: + vnf_data = objects.VNF.vnf_index_list(id, context) + if not vnf_data: + msg = _("Can not find requested vnf data: %s") % id + return self._make_problem_detail(msg, 404, title='Not Found') + except Exception as e: + return self._make_problem_detail( + str(e), 500, 'Internal Server Error') + + if (vnf_data.get("status") != fields.VnfStatus.ACTIVE and + vnf_data.get("status") != fields.VnfStatus.INACTIVE): + msg = _("VNF %(id)s status is %(state)s") + return self._make_problem_detail(msg % {"id": id, + "state": vnf_data.get("status")}, 409, 'Conflict') + + try: + vnf_instance_data = objects.VnfInstanceList.vnf_instance_list( + vnf_data.get('vnfd_id'), context) + if not vnf_instance_data: + msg = _("Can not find requested vnf instance data: %s") \ + % vnf_data.get('vnfd_id') + return self._make_problem_detail(msg, 404, title='Not Found') + except Exception as e: + return self._make_problem_detail( + str(e), 500, 'Internal Server Error') + + if req_body['vnfd_id']: + try: + pkg_obj = objects.VnfPackageVnfd(context=context) + vnfd_pkg = pkg_obj.get_vnf_package_vnfd(req_body['vnfd_id']) + if not vnfd_pkg: + msg = _( + "Can not find requested vnf package vnfd: %s") %\ + req_body['vnfd_id'] + return self._make_problem_detail(msg, 400, 'Bad Request') + except Exception as e: + return self._make_problem_detail( + str(e), 500, 'Internal Server Error') + vnfd_pkg_data = {} + vnfd_pkg_data['vnf_provider'] = vnfd_pkg.get('vnf_provider') + vnfd_pkg_data['vnf_product_name'] = vnfd_pkg.get( + 'vnf_product_name') + vnfd_pkg_data['vnf_software_version'] = vnfd_pkg.get( + 'vnf_software_version') + vnfd_pkg_data['vnfd_version'] = vnfd_pkg.get('vnfd_version') + vnfd_pkg_data['package_uuid'] = vnfd_pkg.get('package_uuid') + + # make op_occs_uuid + op_occs_uuid = uuidutils.generate_uuid() + + # process vnf + if not req_body['vnfd_id']: + vnfd_pkg_data = "" + vnf_lcm_opoccs = { + 'vnf_instance_id': id, + 'id': op_occs_uuid, + 'state_entered_time': vnf_data.get('updated_at'), + 'operationParams': str(body)} + + self.rpc_api.update( + context, + vnf_lcm_opoccs, + req_body, + vnfd_pkg_data, + vnf_data.get('vnfd_id')) + + # make response + res = webob.Response(content_type='application/json') + res.status_int = 202 + loc_url = CONF.vnf_lcm.endpoint_url + \ + '/vnflcm/v1/vnf_lcm_op_occs/' + op_occs_uuid + location = ('Location', loc_url) + res.headerlist.append(location) + + return res + @wsgi.response(http_client.CREATED) @validation.schema(vnf_lcm.register_subscription) def register_subscription(self, request, body): diff --git a/tacker/api/vnflcm/v1/router.py b/tacker/api/vnflcm/v1/router.py index 7e28e02..790d775 100644 --- a/tacker/api/vnflcm/v1/router.py +++ b/tacker/api/vnflcm/v1/router.py @@ -59,7 +59,7 @@ class VnflcmAPIRouter(wsgi.Router): # Allowed methods on # /vnflcm/v1/vnf_instances/{vnfInstanceId} resource - methods = {"DELETE": "delete", "GET": "show"} + methods = {"DELETE": "delete", "GET": "show", "PATCH": "update"} self._setup_route(mapper, "/vnf_instances/{id}", methods, controller, default_resource) diff --git a/tacker/conductor/conductor_server.py b/tacker/conductor/conductor_server.py index f03815e..3603c93 100644 --- a/tacker/conductor/conductor_server.py +++ b/tacker/conductor/conductor_server.py @@ -157,6 +157,79 @@ def revert_upload_vnf_package(function): return decorated_function +@utils.expects_func_args('vnf_lcm_opoccs') +def revert_update_lcm(function): + """Decorator to revert update_lcm on failure.""" + @functools.wraps(function) + def decorated_function(self, context, *args, **kwargs): + try: + return function(self, context, *args, **kwargs) + except Exception as exp: + LOG.error("update vnf_instance failed %s" % exp) + with excutils.save_and_reraise_exception(): + wrapped_func = safe_utils.get_wrapped_function(function) + keyed_args = inspect.getcallargs(wrapped_func, self, context, + *args, **kwargs) + context = keyed_args['context'] + vnf_lcm_opoccs = keyed_args['vnf_lcm_opoccs'] + + try: + # update vnf + vnf_now = timeutils.utcnow() + vnf_obj = objects.vnf.VNF(context=context) + vnf_obj.id = vnf_lcm_opoccs.get('vnf_instance_id') + vnf_obj.status = 'ERROR' + vnf_obj.updated_at = vnf_now + vnf_obj.save() + + e_msg = str(exp) + + # update lcm_op_occs + problem_obj = objects.vnf_lcm_op_occs.ProblemDetails() + problem_obj.status = '500' + problem_obj.detail = e_msg + + lcm_op_obj = objects.vnf_lcm_op_occs.VnfLcmOpOcc( + context=context) + lcm_op_obj.id = vnf_lcm_opoccs.get('id') + lcm_op_obj.operation_state =\ + fields.LcmOccsOperationState.FAILED_TEMP + lcm_op_obj.error = problem_obj + lcm_op_obj.state_entered_time = vnf_now + lcm_op_obj.updated_at = vnf_now + lcm_op_obj.save() + + # Notification + notification = {} + notification['notificationType'] = \ + 'VnfLcmOperationOccurrenceNotification' + notification['notificationStatus'] = 'RESULT' + notification['operationState'] = \ + fields.LcmOccsOperationState.FAILED_TEMP + notification['vnfInstanceId'] = vnf_lcm_opoccs.get( + 'vnf_instance_id') + notification['operation'] = 'MODIFY_INFO' + notification['isAutomaticInvocation'] = 'False' + notification['vnfLcmOpOccId'] = vnf_lcm_opoccs.get('id') + notification['error'] = jsonutils.dumps( + problem_obj.to_dict()) + instance_url = self._get_vnf_instance_href( + vnf_lcm_opoccs.get('vnf_instance_id')) + lcm_url = self._get_vnf_lcm_op_occs_href( + vnf_lcm_opoccs.get('id')) + notification['_links'] = { + 'vnfInstance': { + 'href': instance_url}, + 'vnfLcmOpOcc': { + 'href': lcm_url}} + self.send_notification(context, notification) + + except Exception as msg: + LOG.error("revert_update_lcm failed %s" % str(msg)) + + return decorated_function + + class Conductor(manager.Manager): def __init__(self, host, conf=None): if conf: @@ -180,6 +253,12 @@ class Conductor(manager.Manager): glance_store.initialize_glance_store() self._basic_config_check() + def _get_vnf_instance_href(self, vnf_instance_id): + return '/vnflcm/v1/vnf_instances/%s' % vnf_instance_id + + def _get_vnf_lcm_op_occs_href(self, vnf_lcm_op_occs_id): + return '/vnflcm/v1/vnf_lcm_op_occs/%s' % vnf_lcm_op_occs_id + def _basic_config_check(self): if not os.path.isdir(CONF.vnf_package.vnf_package_csar_path): LOG.error("Config option 'vnf_package_csar_path' is not " @@ -1100,6 +1179,100 @@ class Conductor(manager.Manager): auth_type=auth_type, auth_params=auth_params) + @revert_update_lcm + def update( + self, + context, + vnf_lcm_opoccs, + body_data, + vnfd_pkg_data, + vnfd_id): + # input vnf_lcm_op_occs + now = timeutils.utcnow() + lcm_op_obj = objects.vnf_lcm_op_occs.VnfLcmOpOcc(context=context) + lcm_op_obj.id = vnf_lcm_opoccs.get('id') + lcm_op_obj.operation_state = fields.LcmOccsOperationState.PROCESSING + lcm_op_obj.state_entered_time = vnf_lcm_opoccs.get( + 'state_entered_time') + lcm_op_obj.start_time = now + lcm_op_obj.vnf_instance_id = vnf_lcm_opoccs.get('vnf_instance_id') + lcm_op_obj.operation = fields.InstanceOperation.MODIFY_INFO + lcm_op_obj.is_automatic_invocation = 0 + lcm_op_obj.is_cancel_pending = 0 + lcm_op_obj.operationParams = vnf_lcm_opoccs.get('operationParams') + + try: + lcm_op_obj.create() + except Exception as msg: + raise Exception(str(msg)) + + # Notification + instance_url = self._get_vnf_instance_href( + vnf_lcm_opoccs.get('vnf_instance_id')) + lcm_url = self._get_vnf_lcm_op_occs_href(vnf_lcm_opoccs.get('id')) + + notification_data = { + 'notificationType': + fields.LcmOccsNotificationType.VNF_OP_OCC_NOTIFICATION, + 'notificationStatus': fields.LcmOccsNotificationStatus.START, + 'operationState': fields.LcmOccsOperationState.PROCESSING, + 'vnfInstanceId': vnf_lcm_opoccs.get('vnf_instance_id'), + 'operation': fields.InstanceOperation.MODIFY_INFO, + 'isAutomaticInvocation': False, + 'vnfLcmOpOccId': vnf_lcm_opoccs.get('id'), + '_links': { + 'vnfInstance': { + 'href': instance_url}, + 'vnfLcmOpOcc': { + 'href': lcm_url}}} + + self.send_notification(context, notification_data) + + # update vnf_instances + try: + ins_obj = objects.vnf_instance.VnfInstance(context=context) + result = ins_obj.update( + context, + vnf_lcm_opoccs, + body_data, + vnfd_pkg_data, + vnfd_id) + except Exception as msg: + raise Exception(str(msg)) + + # update lcm_op_occs + changed_info = objects.vnf_lcm_op_occs.VnfInfoModifications() + changed_info.vnf_instance_name = body_data.get('vnf_instance_name') + changed_info.vnf_instance_description = body_data.get( + 'vnf_instance_description') + if body_data.get('vnfd_id'): + changed_info.vnfd_id = body_data.get('vnfd_id') + changed_info.vnf_provider = vnfd_pkg_data.get('vnf_provider') + changed_info.vnf_product_name = vnfd_pkg_data.get( + 'vnf_product_name') + changed_info.vnf_software_version = vnfd_pkg_data.get( + 'vnf_software_version') + changed_info.vnfd_version = vnfd_pkg_data.get('vnfd_version') + + # update vnf_lcm_op_occs + now = timeutils.utcnow() + lcm_op_obj.id = vnf_lcm_opoccs.get('id') + lcm_op_obj.operation_state = fields.LcmOccsOperationState.COMPLETED + lcm_op_obj.state_entered_time = result + lcm_op_obj.updated_at = now + lcm_op_obj.changed_info = changed_info + + try: + lcm_op_obj.save() + except Exception as msg: + raise Exception(str(msg)) + + # Notification + notification_data['notificationStatus'] = 'RESULT' + notification_data['operationState'] = 'COMPLETED' + notification_data['changed_info'] = changed_info.to_dict() + self.send_notification(context, notification_data) + def init(args, **kwargs): CONF(args=args, project='tacker', diff --git a/tacker/conductor/conductorrpc/vnf_lcm_rpc.py b/tacker/conductor/conductorrpc/vnf_lcm_rpc.py index 754acb9..46bd3b5 100644 --- a/tacker/conductor/conductorrpc/vnf_lcm_rpc.py +++ b/tacker/conductor/conductorrpc/vnf_lcm_rpc.py @@ -71,6 +71,20 @@ class VNFLcmRPCAPI(object): heal_vnf_request=heal_vnf_request, vnf_lcm_op_occs_id=vnf_lcm_op_occs_id) + def update(self, context, vnf_lcm_opoccs, body_data, + vnfd_pkg_data, vnfd_id, cast=True): + serializer = objects_base.TackerObjectSerializer() + + client = rpc.get_client(self.target, version_cap=None, + serializer=serializer) + cctxt = client.prepare() + rpc_method = cctxt.cast if cast else cctxt.call + return rpc_method(context, 'update', + vnf_lcm_opoccs=vnf_lcm_opoccs, + body_data=body_data, + vnfd_pkg_data=vnfd_pkg_data, + vnfd_id=vnfd_id) + def send_notification(self, context, notification, cast=True): serializer = objects_base.TackerObjectSerializer() diff --git a/tacker/db/db_sqlalchemy/models.py b/tacker/db/db_sqlalchemy/models.py index aedfad2..5b96d1e 100644 --- a/tacker/db/db_sqlalchemy/models.py +++ b/tacker/db/db_sqlalchemy/models.py @@ -201,7 +201,6 @@ class VnfInstance(model_base.BASE, models.SoftDeleteMixin, task_state = sa.Column(sa.String(255), nullable=True) vim_connection_info = sa.Column(sa.JSON(), nullable=True) tenant_id = sa.Column('tenant_id', sa.String(length=64), nullable=False) - vnf_pkg_id = sa.Column(types.Uuid, nullable=False) vnf_metadata = sa.Column(sa.JSON(), nullable=True) diff --git a/tacker/objects/__init__.py b/tacker/objects/__init__.py index 2b2fd38..f78764d 100644 --- a/tacker/objects/__init__.py +++ b/tacker/objects/__init__.py @@ -25,6 +25,7 @@ def register_all(): # function in order for it to be registered by services that may # need to receive it via RPC. __import__('tacker.objects.heal_vnf_request') + __import__('tacker.objects.vnfd') __import__('tacker.objects.vnf_package') __import__('tacker.objects.vnf_package_vnfd') __import__('tacker.objects.vnf_deployment_flavour') @@ -34,6 +35,7 @@ def register_all(): __import__('tacker.objects.vim_connection') __import__('tacker.objects.instantiate_vnf_req') __import__('tacker.objects.vnf_resources') + __import__('tacker.objects.vnf') __import__('tacker.objects.vnfd') __import__('tacker.objects.vnf_lcm_op_occs') __import__('tacker.objects.terminate_vnf_req') diff --git a/tacker/objects/fields.py b/tacker/objects/fields.py index 6cf1dda..1874c39 100644 --- a/tacker/objects/fields.py +++ b/tacker/objects/fields.py @@ -255,3 +255,14 @@ class LcmOccsNotificationType(BaseTackerEnum): VNF_ID_CREATION_NOTIFICATION = 'VnfIdentifierCreationNotification' ALL = (VNF_OP_OCC_NOTIFICATION) + + +class VnfStatus(BaseTackerEnum): + ACTIVE = 'ACTIVE' + INACTIVE = 'INACTIVE' + + ALL = (ACTIVE, INACTIVE) + + +class InstanceOperation(BaseTackerEnum): + MODIFY_INFO = 'MODIFY_INFO' diff --git a/tacker/objects/vim_connection.py b/tacker/objects/vim_connection.py index adbab05..f803bfb 100644 --- a/tacker/objects/vim_connection.py +++ b/tacker/objects/vim_connection.py @@ -26,6 +26,8 @@ class VimConnectionInfo(base.TackerObject, base.TackerPersistentObject): 'id': fields.StringField(nullable=False), 'vim_id': fields.StringField(nullable=True, default=None), 'vim_type': fields.StringField(nullable=False), + 'interface_info': fields.DictOfNullableStringsField(nullable=True, + default={}), 'access_info': fields.DictOfNullableStringsField(nullable=True, default={}), } @@ -36,7 +38,11 @@ class VimConnectionInfo(base.TackerObject, base.TackerPersistentObject): vim_id = data_dict.get('vim_id') vim_type = data_dict.get('vim_type') access_info = data_dict.get('access_info', {}) - obj = cls(id=id, vim_id=vim_id, vim_type=vim_type, + interface_info = data_dict.get('interface_info', {}) + obj = cls(id=id, + vim_id=vim_id, + vim_type=vim_type, + interface_info=interface_info, access_info=access_info) return obj @@ -55,4 +61,5 @@ class VimConnectionInfo(base.TackerObject, base.TackerPersistentObject): return {'id': self.id, 'vim_id': self.vim_id, 'vim_type': self.vim_type, + 'interface_info': self.interface_info, 'access_info': self.access_info} diff --git a/tacker/objects/vnf.py b/tacker/objects/vnf.py new file mode 100644 index 0000000..2ff0ec7 --- /dev/null +++ b/tacker/objects/vnf.py @@ -0,0 +1,111 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging +from oslo_versionedobjects import base as ovoo_base +from tacker.db import api as db_api +from tacker.db.db_sqlalchemy import api +from tacker.db.vnfm import vnfm_db +from tacker.objects import base +from tacker.objects import fields + +LOG = logging.getLogger(__name__) + + +def _vnf_update(context, values): + update = {'status': values.status, + 'updated_at': values.updated_at} + + api.model_query(context, vnfm_db.VNF). \ + filter_by(id=values.id). \ + update(update, synchronize_session=False) + + +@db_api.context_manager.reader +def _vnf_get(context, id, columns_to_join=None): + vnf_data = api.model_query( + context, + vnfm_db.VNF).filter_by( + id=id).filter_by( + deleted_at='0001-01-01 00:00:00').first() + if vnf_data: + vnf_data = vnf_data.__dict__ + vnf_attribute_data = api.model_query( + context, vnfm_db.VNFAttribute).filter_by( + vnf_id=vnf_data.get('id')).first() + vnf_data['vnf_attribute'] = vnf_attribute_data.__dict__ + vnfd_data = api.model_query( + context, vnfm_db.VNFD).filter_by( + id=vnf_data.get('vnfd_id')).first() + vnf_data['vnfd'] = vnfd_data.__dict__ + vnfd_attribute_data = api.model_query( + context, vnfm_db.VNFDAttribute).filter_by( + vnfd_id=vnf_data.get('vnfd_id')).first() + vnf_data['vnfd_attribute'] = vnfd_attribute_data.__dict__ + else: + vnf_data = "" + + return vnf_data + + +@base.TackerObjectRegistry.register +class VNF(base.TackerObject, base.TackerObjectDictCompat, + base.TackerPersistentObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.UUIDField(nullable=False), + 'tenant_id': fields.UUIDField(nullable=False), + 'name': fields.StringField(nullable=False), + 'vnfd_id': fields.UUIDField(nullable=False), + 'instance_id': fields.StringField(nullable=True), + 'mgmt_ip_address': fields.StringField(nullable=True), + 'status': fields.StringField(nullable=True), + 'description': fields.StringField(nullable=True), + 'placement_attr': fields.StringField(nullable=True), + 'vim_id': fields.StringField(nullable=False), + 'error_reason': fields.StringField(nullable=True), + 'vnf_attribute': fields.ObjectField( + 'VNFAttribute', nullable=True), + 'vnfd': fields.ObjectField('VNFD', nullable=True), + } + + @base.remotable + def save(self): + updates = self.obj_clone() + _vnf_update(self._context, updates) + + @base.remotable_classmethod + def vnf_index_list(cls, id, context): + # get vnf_instance data + expected_attrs = ["vnf_attribute", "vnfd"] + db_vnf = _vnf_get(context, id, columns_to_join=expected_attrs) + return db_vnf + + +@base.TackerObjectRegistry.register +class VnfList(ovoo_base.ObjectListBase, base.TackerObject): + + VERSION = '1.0' + + fields = { + 'objects': fields.ListOfObjectsField('VNF') + } + + @base.remotable_classmethod + def vnf_index_list(cls, id, context): + # get vnf_instance data + expected_attrs = ["vnf_attribute", "vnfd"] + db_vnf = _vnf_get(context, id, columns_to_join=expected_attrs) + return db_vnf diff --git a/tacker/objects/vnf_instance.py b/tacker/objects/vnf_instance.py index 0c09f92..4f05275 100644 --- a/tacker/objects/vnf_instance.py +++ b/tacker/objects/vnf_instance.py @@ -28,10 +28,13 @@ from tacker.common import utils from tacker.db import api as db_api from tacker.db.db_sqlalchemy import api from tacker.db.db_sqlalchemy import models +from tacker.db.vnfm import vnfm_db from tacker import objects from tacker.objects import base from tacker.objects import fields from tacker.objects import vnf_instantiated_info +from tacker.objects import vnf_package as vnf_package_obj +from tacker.objects import vnf_package_vnfd as vnf_package_vnfd LOG = logging.getLogger(__name__) @@ -149,6 +152,151 @@ def _wrap_object_error(method): return wrapper +@db_api.context_manager.reader +def _get_vnf_instance(context, id): + vnf_instance = api.model_query( + context, models.VnfInstance).filter_by( + vnfd_id=id).first() + return vnf_instance + + +@db_api.context_manager.reader +def _vnf_instance_get(context, vnfd_id, columns_to_join=None): + query = api.model_query(context, models.VnfInstance, read_deleted="no", + project_only=True).filter_by(vnfd_id=vnfd_id) + + if columns_to_join: + for column in columns_to_join: + query = query.options(joinedload(column)) + + return query.first() + + +def _merge_vim_connection_info( + pre_vim_connection_info_list, + update_vim_connection_info_list): + + def update_nested_element(pre_data, update_data): + for key, val in update_data.items(): + if not isinstance(val, dict): + pre_data[key] = val + continue + + if key in pre_data: + pre_data[key].update(val) + else: + pre_data.update({key: val}) + + result = [] + clone_pre_list = copy.deepcopy(pre_vim_connection_info_list) + + for update_vim_connection in update_vim_connection_info_list: + pre_data = None + for i in range(0, len(clone_pre_list) - 1): + if clone_pre_list[i].id == update_vim_connection.get('id'): + pre_data = clone_pre_list.pop(i) + + if pre_data is None: + # new elm. + result.append(objects.VimConnectionInfo._from_dict( + update_vim_connection)) + continue + + convert_dict = pre_data.to_dict() + update_nested_element(convert_dict, update_vim_connection) + result.append(objects.VimConnectionInfo._from_dict( + convert_dict)) + + # Reflecting unupdated data + result.extend(clone_pre_list) + + return result + + +@db_api.context_manager.writer +def _update_vnf_instances( + context, + vnf_lcm_opoccs, + body_data, + vnfd_pkg_data, + vnfd_id): + updated_values = {} + updated_values['vnf_instance_name'] = body_data.get('vnf_instance_name') + updated_values['vnf_instance_description'] = body_data.get( + 'vnf_instance_description') + + # get vnf_instances + vnf_instance = _get_vnf_instance(context, vnfd_id) + if body_data.get('metadata'): + vnf_instance.vnf_metadata.update(body_data.get('metadata')) + updated_values['vnf_metadata'] = vnf_instance.vnf_metadata + + if body_data.get('vim_connection_info'): + merge_vim_connection_info = _merge_vim_connection_info( + vnf_instance.vim_connection_info, + body_data.get('vim_connection_info')) + + updated_values['vim_connection_info'] = merge_vim_connection_info + + if body_data.get('vnfd_id'): + updated_values['vnfd_id'] = body_data.get('vnfd_id') + updated_values['vnf_provider'] = vnfd_pkg_data.get('vnf_provider') + updated_values['vnf_product_name'] = vnfd_pkg_data.get( + 'vnf_product_name') + updated_values['vnf_software_version'] = vnfd_pkg_data.get( + 'vnf_software_version') + + api.model_query(context, models.VnfInstance). \ + filter_by(id=vnf_lcm_opoccs.get('vnf_instance_id')). \ + update(updated_values, synchronize_session=False) + + vnf_now = timeutils.utcnow() + if body_data.get('vnfd_id'): + # update vnf + updated_values = {'vnfd_id': body_data.get('vnfd_id'), + 'updated_at': vnf_now + } + api.model_query(context, vnfm_db.VNF).\ + filter_by(id=vnf_lcm_opoccs.get('vnf_instance_id')). \ + update(updated_values, synchronize_session=False) + + # get vnf_packages + id = vnfd_pkg_data.get('package_uuid') + try: + vnf_package = vnf_package_obj.VnfPackage.get_by_id(context, id) + except exceptions.VnfPackageNotFound: + raise exceptions.VnfPackageNotFound(id=id) + + if vnf_package.usage_state == 'NOT_IN_USE': + # update vnf_packages + now = timeutils.utcnow() + updated_values = {'usage_state': 'IN_USE', + 'updated_at': now + } + api.model_query(context, models.VnfPackage).\ + filter_by(id=id). \ + update(updated_values, synchronize_session=False) + + # get vnf_instances + vnf_instance = _get_vnf_instance(context, vnfd_id) + + if not vnf_instance: + # get vnf_package_vnfd + vnfd_data = vnf_package_vnfd.VnfPackageVnfd.get_by_vnfdId( + context, vnfd_id) + + # update vnf_packages + now = timeutils.utcnow() + updated_values = {'usage_state': 'NOT_IN_USE', + 'updated_at': now + } + api.model_query(context, models.VnfPackage).\ + filter_by(id=vnfd_data.package_uuid). \ + update(updated_values, synchronize_session=False) + + return vnf_now + + @base.TackerObjectRegistry.register class VnfInstance(base.TackerObject, base.TackerPersistentObject, base.TackerObjectDictCompat): @@ -171,7 +319,6 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, 'vim_connection_info': fields.ListOfObjectsField( 'VimConnectionInfo', nullable=True, default=[]), 'tenant_id': fields.StringField(nullable=False), - 'vnf_pkg_id': fields.StringField(nullable=False), 'vnf_metadata': fields.DictOfStringsField(nullable=True, default={}), 'instantiated_vnf_info': fields.ObjectField('InstantiatedVnfInfo', nullable=True, default=None) @@ -329,7 +476,6 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, 'vnf_product_name': self.vnf_product_name, 'vnf_software_version': self.vnf_software_version, 'vnfd_version': self.vnfd_version, - 'vnf_pkg_id': self.vnf_pkg_id, 'vnf_metadata': self.vnf_metadata} if (self.instantiation_state == fields.VnfInstanceState.INSTANTIATED @@ -344,6 +490,23 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, return data + @base.remotable + def update( + self, + context, + vnf_lcm_opoccs, + body_data, + vnfd_pkg_data, + vnfd_id): + + # update vnf_instances + return _update_vnf_instances( + context, + vnf_lcm_opoccs, + body_data, + vnfd_pkg_data, + vnfd_id) + @base.remotable_classmethod def get_by_id(cls, context, id): expected_attrs = ["instantiated_vnf_info"] @@ -380,3 +543,19 @@ class VnfInstanceList(ovoo_base.ObjectListBase, base.TackerObject): return _make_vnf_instance_list(context, cls(), db_vnf_instances, expected_attrs) + + @base.remotable_classmethod + def vnf_instance_list(cls, vnfd_id, context): + # get vnf_instance data + expected_attrs = ["instantiated_vnf_info"] + db_vnf_instances = _vnf_instance_get(context, vnfd_id, + columns_to_join=expected_attrs) + + vnf_instance_cls = VnfInstance + vnf_instance_data = "" + vnf_instance_obj = vnf_instance_cls._from_db_object( + context, vnf_instance_cls(context), db_vnf_instances, + expected_attrs=expected_attrs) + vnf_instance_data = vnf_instance_obj + + return vnf_instance_data diff --git a/tacker/objects/vnf_package_vnfd.py b/tacker/objects/vnf_package_vnfd.py index 6ce8b78..c490345 100644 --- a/tacker/objects/vnf_package_vnfd.py +++ b/tacker/objects/vnf_package_vnfd.py @@ -13,6 +13,7 @@ # under the License. from oslo_db import exception as db_exc +from oslo_log import log as logging from oslo_utils import uuidutils from tacker.common import exceptions @@ -23,6 +24,8 @@ from tacker.db.db_sqlalchemy import models from tacker.objects import base from tacker.objects import fields +LOG = logging.getLogger(__name__) + @db_api.context_manager.writer def _vnf_package_vnfd_create(context, values): @@ -40,6 +43,49 @@ def _vnf_package_vnfd_create(context, values): @db_api.context_manager.reader +def _get_vnf_package_vnfd(context, id, package_uuid=None, del_flg=None): + if package_uuid and not del_flg: + query = api.model_query( + context, + models.VnfPackageVnfd).filter_by( + package_uuid=id).filter_by( + deleted=0) + elif package_uuid and del_flg: + query = api.model_query( + context, models.VnfPackageVnfd).filter_by( + package_uuid=id) + else: + query = api.model_query( + context, + models.VnfPackageVnfd).filter_by( + vnfd_id=id).filter_by( + deleted=0) + try: + result = query.all() + result_line = "" + for line in result: + result_line = line + + except Exception: + LOG.error("select vnf_package_vnfd failed") + + if result_line: + return result_line + else: + return None + + +@db_api.context_manager.writer +def _vnf_package_vnfd_delete(context, id): + try: + api.model_query( + context, models.VnfPackageVnfd).filter_by( + package_uuid=id).delete() + except Exception: + LOG.error("delete vnf_package_vnfd failed") + + +@db_api.context_manager.reader def _vnf_package_vnfd_get_by_id(context, vnfd_id): query = api.model_query(context, models.VnfPackageVnfd, @@ -59,6 +105,38 @@ def _vnf_package_vnfd_get_by_id(context, vnfd_id): return result +def _vnf_package_vnfd_get_by_packageId(context, packageId): + + query = api.model_query( + context, + models.VnfPackageVnfd, + read_deleted="no", + project_only=True).filter_by( + package_uuid=packageId) + + result = query.first() + + if not result: + return None + + return result + + +@db_api.context_manager.reader +def _vnf_package_vnfd_get_by_vnfdId(context, vnfdId): + query = api.model_query(context, + models.VnfPackageVnfd, + read_deleted="no", + project_only=True).filter_by(vnfd_id=vnfdId) + + result = query.first() + + if not result: + return None + + return result + + @db_api.context_manager.reader def _get_vnf_package_vnfd_by_vnfid(context, vnfpkgid): @@ -127,10 +205,18 @@ class VnfPackageVnfd(base.TackerObject, base.TackerObjectDictCompat, self._context, updates) self._from_db_object(self._context, self, db_vnf_package_vnfd) + @base.remotable + def get_vnf_package_vnfd(self, id, package_uuid=None, del_flg=None): + return _get_vnf_package_vnfd(self._context, id, package_uuid, del_flg) + @base.remotable_classmethod def get_vnf_package_vnfd_by_vnfid(self, context, vnfid): return _get_vnf_package_vnfd_by_vnfid(context, vnfid) + @base.remotable + def delete(self, id): + _vnf_package_vnfd_delete(self._context, id) + @classmethod def obj_from_db_obj(cls, context, db_obj): return cls._from_db_object(context, cls(), db_obj) @@ -139,3 +225,19 @@ class VnfPackageVnfd(base.TackerObject, base.TackerObjectDictCompat, def get_by_id(cls, context, id): db_vnf_package_vnfd = _vnf_package_vnfd_get_by_id(context, id) return cls._from_db_object(context, cls(), db_vnf_package_vnfd) + + @base.remotable_classmethod + def get_by_vnfdId(cls, context, id): + db_vnf_package_vnfd = _vnf_package_vnfd_get_by_vnfdId( + context, id) + if not db_vnf_package_vnfd: + return db_vnf_package_vnfd + return cls._from_db_object(context, cls(), db_vnf_package_vnfd) + + @base.remotable_classmethod + def get_by_packageId(cls, context, id): + db_vnf_package_vnfd = _vnf_package_vnfd_get_by_packageId( + context, id) + if not db_vnf_package_vnfd: + return db_vnf_package_vnfd + return cls._from_db_object(context, cls(), db_vnf_package_vnfd) diff --git a/tacker/policies/vnf_lcm.py b/tacker/policies/vnf_lcm.py index c77a0c8..4c4b37d 100644 --- a/tacker/policies/vnf_lcm.py +++ b/tacker/policies/vnf_lcm.py @@ -110,6 +110,17 @@ rules = [ } ] ), + policy.DocumentedRuleDefault( + name=VNFLCM % 'update_vnf', + check_str=base.RULE_ADMIN_OR_OWNER, + description="Update an Individual VNF instance.", + operations=[ + { + 'method': 'PATCH', + 'path': '/vnflcm/v1/vnf_instances/{vnfInstanceId}' + } + ] + ), ] diff --git a/tacker/tests/unit/conductor/fakes.py b/tacker/tests/unit/conductor/fakes.py index ff94dc9..242a05b 100644 --- a/tacker/tests/unit/conductor/fakes.py +++ b/tacker/tests/unit/conductor/fakes.py @@ -15,9 +15,11 @@ import datetime import iso8601 - import os import shutil +from tacker import objects +from tacker.objects import fields +from tacker.tests import constants import tempfile import uuid import yaml @@ -25,6 +27,7 @@ import zipfile from oslo_config import cfg +from tacker.db.db_sqlalchemy import models from tacker.tests import utils from tacker.tests import uuidsentinel @@ -150,3 +153,137 @@ def get_expected_vnfd_data(zip_file=None): shutil.rmtree(csar_temp_dir) return file_path_and_data + + +def fake_vnf_package_vnfd_model_dict(**updates): + vnfd = { + 'package_uuid': uuidsentinel.package_uuid, + 'deleted': False, + 'deleted_at': None, + 'updated_at': None, + 'created_at': datetime.datetime(2020, 1, 1, 1, 1, 1, + tzinfo=iso8601.UTC), + 'vnf_product_name': 'Sample VNF', + 'vnf_provider': 'test vnf provider', + 'vnf_software_version': '1.0', + 'vnfd_id': uuidsentinel.vnfd_id, + 'vnfd_version': '1.0', + 'id': constants.UUID, + } + + if updates: + vnfd.update(updates) + + return vnfd + + +def return_vnf_package_vnfd(): + model_obj = models.VnfPackageVnfd() + model_obj.update(fake_vnf_package_vnfd_model_dict()) + return model_obj + + +def _model_non_instantiated_vnf_instance(**updates): + vnf_instance = { + 'created_at': datetime.datetime(2020, 1, 1, 1, 1, 1, + tzinfo=iso8601.UTC), + 'deleted': False, + 'deleted_at': None, + 'id': uuidsentinel.vnf_instance_id, + 'instantiated_vnf_info': None, + 'instantiation_state': fields.VnfInstanceState.NOT_INSTANTIATED, + 'updated_at': None, + 'vim_connection_info': [], + 'vnf_instance_description': 'Vnf instance description', + 'vnf_instance_name': 'Vnf instance name', + 'vnf_product_name': 'Sample VNF', + 'vnf_provider': 'Vnf provider', + 'vnf_software_version': '1.0', + 'tenant_id': uuidsentinel.tenant_id, + 'vnfd_id': uuidsentinel.vnfd_id, + 'vnfd_version': '1.0'} + + if updates: + vnf_instance.update(**updates) + + return vnf_instance + + +def return_vnf_instance( + instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED, + scale_status=None, + **updates): + + if instantiated_state == fields.VnfInstanceState.NOT_INSTANTIATED: + data = _model_non_instantiated_vnf_instance(**updates) + data['instantiation_state'] = instantiated_state + vnf_instance_obj = objects.VnfInstance(**data) + + elif scale_status: + data = _model_non_instantiated_vnf_instance(**updates) + data['instantiation_state'] = instantiated_state + vnf_instance_obj = objects.VnfInstance(**data) + + get_instantiated_vnf_info = { + 'flavour_id': uuidsentinel.flavour_id, + 'vnf_state': 'STARTED', + 'instance_id': uuidsentinel.instance_id + } + instantiated_vnf_info = get_instantiated_vnf_info + + s_status = {"aspect_id": "SP1", "scale_level": 1} + scale_status = objects.ScaleInfo(**s_status) + + instantiated_vnf_info.update( + {"ext_cp_info": [], + 'ext_virtual_link_info': [], + 'ext_managed_virtual_link_info': [], + 'vnfc_resource_info': [], + 'vnf_virtual_link_resource_info': [], + 'virtual_storage_resource_info': [], + "flavour_id": "simple", + "scale_status": [scale_status], + "vnf_instance_id": "171f3af2-a753-468a-b5a7-e3e048160a79", + "additional_params": {"key": "value"}, + 'vnf_state': "STARTED"}) + info_data = objects.InstantiatedVnfInfo(**instantiated_vnf_info) + + vnf_instance_obj.instantiated_vnf_info = info_data + else: + data = _model_non_instantiated_vnf_instance(**updates) + data['instantiation_state'] = instantiated_state + vnf_instance_obj = objects.VnfInstance(**data) + inst_vnf_info = objects.InstantiatedVnfInfo.obj_from_primitive({ + "ext_cp_info": [], + 'ext_virtual_link_info': [], + 'ext_managed_virtual_link_info': [], + 'vnfc_resource_info': [], + 'vnf_virtual_link_resource_info': [], + 'virtual_storage_resource_info': [], + "flavour_id": "simple", + "additional_params": {"key": "value"}, + 'vnf_state': "STARTED"}, None) + + vnf_instance_obj.instantiated_vnf_info = inst_vnf_info + + return vnf_instance_obj + + +def _get_vnf(**updates): + vnf_data = { + 'tenant_id': uuidsentinel.tenant_id, + 'name': "fake_name", + 'vnfd_id': uuidsentinel.vnfd_id, + 'vnf_instance_id': uuidsentinel.instance_id, + 'mgmt_ip_address': "fake_mgmt_ip_address", + 'status': 'ACTIVE', + 'description': 'fake_description', + 'placement_attr': 'fake_placement_attr', + 'vim_id': 'uuidsentinel.vim_id', + 'error_reason': 'fake_error_reason', + } + + if updates: + vnf_data.update(**updates) + + return vnf_data diff --git a/tacker/tests/unit/conductor/test_conductor_server.py b/tacker/tests/unit/conductor/test_conductor_server.py index 234077a..1b4625d 100644 --- a/tacker/tests/unit/conductor/test_conductor_server.py +++ b/tacker/tests/unit/conductor/test_conductor_server.py @@ -14,7 +14,9 @@ # limitations under the License. import base64 +import datetime import fixtures +import iso8601 import json import os import requests @@ -33,6 +35,7 @@ from tacker.common import csar_utils from tacker.common import exceptions from tacker.conductor import conductor_server from tacker import context +from tacker import context as t_context from tacker.glance_store import store as glance_store from tacker import objects from tacker.objects import fields @@ -78,6 +81,9 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): self.vnf_package = self._create_vnf_package() self.instance_uuid = uuidsentinel.instance_id self.temp_dir = self.useFixture(fixtures.TempDir()).path + self.body_data = self._create_body_data() + self.vnf_lcm_opoccs = self._create_vnf_lcm_opoccs() + self.vnfd_pkg_data = self._create_vnfd_pkg_data() def _mock_vnfm_plugin(self): self.vnfm_plugin = mock.Mock(wraps=FakeVNFMPlugin()) @@ -120,6 +126,40 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): return [DummyLcmSubscription(auth_params)] + def _create_body_data(self): + body_data = {} + body_data['vnf_instance_name'] = "new_instance_name" + body_data['vnf_instance_description'] = "new_instance_discription" + body_data['vnfd_id'] = "2c69a161-0000-4b0f-bcf8-391f8fc76600" + body_data['vnf_configurable_properties'] = {"test": "test_value"} + body_data['vnfc_info_modifications_delete_ids'] = ["test1"] + return body_data + + def _create_vnf_lcm_opoccs(self): + vnf_lcm_opoccs = { + 'vnf_instance_id': uuidsentinel.vnf_instance_id, + 'id': uuidsentinel.id, + 'state_entered_time': datetime.datetime( + 1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.UTC), + 'operationParams': { + "key": "value"}} + return vnf_lcm_opoccs + + def _create_vnfd_pkg_data(self): + vnfd_pkg_data = {} + vnfd_pkg_data['vnf_provider'] = fakes.return_vnf_package_vnfd().get( + 'vnf_provider') + vnfd_pkg_data['vnf_product_name'] =\ + fakes.return_vnf_package_vnfd().get('vnf_product_name') + vnfd_pkg_data['vnf_software_version'] =\ + fakes.return_vnf_package_vnfd().get('vnf_software_version') + vnfd_pkg_data['vnfd_version'] = fakes.return_vnf_package_vnfd().get( + 'vnfd_version') + vnfd_pkg_data['package_uuid'] = fakes.return_vnf_package_vnfd().get( + 'package_uuid') + return vnfd_pkg_data + def assert_auth_basic( self, acutual_request, @@ -958,3 +998,24 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): self.assertEqual(result, 99) mock_subscriptions_get.assert_called() + + @mock.patch.object(conductor_server, 'revert_update_lcm') + @mock.patch.object(t_context.get_admin_context().session, "add") + @mock.patch.object(objects.vnf_lcm_op_occs.VnfLcmOpOcc, "save") + @mock.patch.object(objects.VnfInstance, "update") + @mock.patch.object(objects.vnf_lcm_op_occs.VnfLcmOpOcc, "create") + def test_update(self, mock_create, mock_update, mock_save, mock_add, + mock_revert): + mock_create.return_value = "OK" + mock_update.return_value = datetime.datetime( + 1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) + mock_add.return_value = "OK" + mock_save.return_value = "OK" + vnfd_id = "2c69a161-0000-4b0f-bcf8-391f8fc76600" + + self.conductor.update( + self.context, + self.vnf_lcm_opoccs, + self.body_data, + self.vnfd_pkg_data, + vnfd_id) diff --git a/tacker/tests/unit/objects/fakes.py b/tacker/tests/unit/objects/fakes.py index d90cd33..9539d9f 100644 --- a/tacker/tests/unit/objects/fakes.py +++ b/tacker/tests/unit/objects/fakes.py @@ -45,6 +45,14 @@ software_image = { 'metadata': {'key1': 'value1'} } +artifact_data = { + 'file_name': 'test', 'type': 'test', + 'algorithm': 'sha-256', + 'hash': 'b9c3036539fd7a5f87a1bf38eb05fdde8b556a1' + 'a7e664dbeda90ed3cd74b4f9d', + 'metadata': {'key1': 'value1'} +} + artifacts = { 'json_data': 'test data', 'type': 'tosca.artifacts.nfv.SwImage', @@ -94,6 +102,42 @@ subscription_data = { 'created_at': "2020-06-11 09:39:58" } +vnfd_data = { + "tenant_id": uuidsentinel.tenant_id, + 'name': 'test', + 'description': 'test_description', + 'mgmt_driver': 'test_mgmt_driver' +} + +vnfd_attribute = { + 'key': 'test_key', + 'value': 'test_value', +} + +lcm_op_occs_data = { + "tenant_id": uuidsentinel.tenant_id, + 'operation_state': 'PROCESSING', + 'state_entered_time': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.UTC), + 'start_time': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.UTC), + 'operation': 'MODIFY_INFO', + 'is_automatic_invocation': 0, + 'is_cancel_pending': 0, +} + +vim_data = { + 'id': uuidsentinel.vim_id, + 'type': 'test_type', + "tenant_id": uuidsentinel.tenant_id, + 'name': "test_name", + 'description': "test_description", + 'placement_attr': "test_placement_attr", + 'shared': 0, + 'status': "REACHABLE", + 'is_default': 0 +} + fake_vnf_package_response = copy.deepcopy(vnf_package_data) fake_vnf_package_response.pop('user_data') fake_vnf_package_response.update({'id': uuidsentinel.package_uuid}) @@ -430,3 +474,16 @@ def get_changed_info_data(): "vnf_software_version": "1.0", "vnfd_version": "MME_1.0" } + + +def get_vnf(vnfd_id, vim_id): + return { + 'tenant_id': uuidsentinel.tenant_id, + 'name': "test_name", + 'vnfd_id': vnfd_id, + 'mgmt_ip_address': "test_mgmt_ip_address", + 'status': "ACTIVE", + 'description': "test_description", + 'placement_attr': "test_placement_attr", + 'vim_id': vim_id + } diff --git a/tacker/tests/unit/objects/test_vnf.py b/tacker/tests/unit/objects/test_vnf.py new file mode 100644 index 0000000..916ec53 --- /dev/null +++ b/tacker/tests/unit/objects/test_vnf.py @@ -0,0 +1,69 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import mock + +import datetime +import iso8601 +from tacker import context +from tacker.db.nfvo import nfvo_db +from tacker import objects +from tacker.tests.unit.db.base import SqlTestCase +from tacker.tests.unit.objects import fakes +from tacker.tests import uuidsentinel + + +class TestVnf(SqlTestCase): + + def setUp(self): + super(TestVnf, self).setUp() + self.context = context.get_admin_context() + self.vnfd = self._create_vnfd() + self.vims = self._create_vims() + + def _create_vnfd(self): + vnfd_obj = objects.Vnfd(context=self.context, **fakes.vnfd_data) + vnfd_obj.create() + + return vnfd_obj + + def _create_vims(self): + vim_obj = nfvo_db.Vim(**fakes.vim_data) + + return vim_obj + + def test_save(self): + vnf_data = fakes.get_vnf(self.vnfd.id, self.vims.id) + vnf_obj = objects.vnf.VNF(context=self.context, **vnf_data) + + vnf_obj.id = uuidsentinel.instance_id + vnf_obj.status = 'ERROR' + vnf_obj.updated_at = datetime.datetime( + 1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) + vnf_obj.save() + + self.assertEqual('ERROR', vnf_obj.status) + + @mock.patch('tacker.objects.vnf._vnf_get') + def test_vnf_index_list(self, mock_vnf_get): + vnf_data = fakes.get_vnf(self.vnfd.id, self.vims.id) + vnf_obj = objects.vnf.VNF(context=self.context, **vnf_data) + + vnf_obj.id = uuidsentinel.instance_id + vnf_obj.updated_at = datetime.datetime( + 1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC) + vnf_obj.save() + + mock_vnf_get.return_value = vnf_obj + vnf_data_result = vnf_obj.vnf_index_list(vnf_obj.id, self.context) + self.assertEqual('ACTIVE', vnf_data_result.status) diff --git a/tacker/tests/unit/objects/test_vnf_instance.py b/tacker/tests/unit/objects/test_vnf_instance.py index 8608665..d2a2671 100644 --- a/tacker/tests/unit/objects/test_vnf_instance.py +++ b/tacker/tests/unit/objects/test_vnf_instance.py @@ -18,6 +18,8 @@ from unittest import mock from tacker.common import exceptions from tacker import context from tacker.db import api as sqlalchemy_api +from tacker.db.db_sqlalchemy import api +from tacker.db.nfvo import nfvo_db from tacker import objects from tacker.tests.unit.db.base import SqlTestCase from tacker.tests.unit.objects import fakes @@ -26,12 +28,41 @@ from tacker.tests import uuidsentinel get_engine = sqlalchemy_api.get_engine +class FakeApiModelQuery: + + def __init__( + self, + callback_filter_by=None, + callback_update=None, + callback_options=None): + self.callback_filter_by = callback_filter_by + self.callback_update = callback_update + self.callback_options = callback_options + + def options(self, *args, **kwargs): + if self.callback_options: + self.callback_options(*args, **kwargs) + + def filter_by(self, *args, **kwargs): + if self.callback_filter_by: + self.callback_filter_by(*args, **kwargs) + return self + + def update(self, *args, **kwargs): + if self.callback_update: + self.callback_update(*args, **kwargs) + return self + + class TestVnfInstance(SqlTestCase): + maxDiff = None + def setUp(self): super(TestVnfInstance, self).setUp() self.context = context.get_admin_context() self.vnf_package = self._create_and_upload_vnf_package() + self.vims = nfvo_db.Vim(**fakes.vim_data) self.engine = get_engine() self.conn = self.engine.connect() @@ -157,3 +188,156 @@ class TestVnfInstance(SqlTestCase): vnf_instance_obj = objects.VnfInstance(context=self.context) self.assertRaises(exceptions.ObjectActionError, vnf_instance_obj.destroy, self.context) + + @mock.patch('tacker.objects.vnf_instance._get_vnf_instance') + @mock.patch('tacker.objects.vnf_package.VnfPackage.get_by_id') + @mock.patch.object(api, 'model_query') + def test_update_vnf_instances( + self, + mock_model_query, + mock_get_vnf_package, + mock_get_vnf): + + vnf_instance_data = fakes.fake_vnf_instance_model_dict(**{ + "vim_connection_info": [ + objects.VimConnectionInfo._from_dict({ + "id": "testid", + "vim_id": "aaa", + "vim_type": "openstack-1", + "interface_info": {"endpoint": "endpoint"}, + "access_info": {"username": "xxxxx", + "region": "region", + "password": "password", + "tenant": "tenant"}}), + objects.VimConnectionInfo._from_dict({ + "id": "testid3", + "vim_id": "ccc", + "vim_type": "openstack-2", + "interface_info": {"endpoint": "endpoint22"}, + "access_info": {"username": "xxxxx", + "region": "region", + "password": "password"}}), + objects.VimConnectionInfo._from_dict({ + "id": "testid5", + "vim_id": "eee", + "vim_type": "openstack-4"}) + ], + "vnf_metadata": {"testkey": "test_value"}}) + vnf_instance = objects.VnfInstance( + context=self.context, **vnf_instance_data) + mock_get_vnf.return_value = \ + fakes.vnf_instance_model_object(vnf_instance) + + def mock_filter(id=None): + print('### mock_filter ###', id) + + def mock_update(updated_values, synchronize_session=False): + print('### mock_update ###', updated_values) + + if 'vim_connection_info' not in updated_values: + return + + compar_updated_values = {} + compar_updated_values['vnf_instance_name'] = "new_instance_name" + compar_updated_values['vnf_instance_description'] = \ + "new_instance_discription" + compar_updated_values['vnf_metadata'] = { + "testkey": "test_value1", "testkey2": "test_value2"} + compar_updated_values['vim_connection_info'] = [ + objects.VimConnectionInfo._from_dict({ + "id": "testid", + "vim_id": "bbb", + "vim_type": "openstack-1A", + "interface_info": {"endpoint": "endpoint11"}, + "access_info": {"username": "xxxxx1", + "region": "region1", + "password": "password1", + "tenant": "tenant1"}}), + objects.VimConnectionInfo._from_dict({ + "id": "testid3", + "vim_id": "ccc", + "vim_type": "openstack-2", + "interface_info": {"endpoint": "endpoint22"}, + "access_info": {"username": "xxxxx", + "region": "region", + "password": "password2", + "tenant": "tenant2"}}), + objects.VimConnectionInfo._from_dict({ + "id": "testid5", + "vim_id": "eee", + "vim_type": "openstack-4"}), + objects.VimConnectionInfo._from_dict({ + "id": "testid7", + "vim_id": "fff", + "vim_type": "openstack-5A", + "interface_info": {"endpoint": "endpoint55"}, + "access_info": {"username": "xxxxx5", + "region": "region5", + "password": "password5", + "tenant": "tenant5"}}) + ] + compar_updated_values['vnfd_id'] = \ + "2c69a161-0000-4b0f-bcf8-391f8fc76600" + compar_updated_values['vnf_provider'] = \ + self.vnf_package.get('vnf_provider') + compar_updated_values['vnf_product_name'] = \ + self.vnf_package.get('vnf_product_name') + compar_updated_values['vnf_software_version'] = \ + self.vnf_package.get('vnf_software_version') + + expected_vci = sorted(compar_updated_values.pop( + 'vim_connection_info'), key=lambda x: x.id) + actual_vci = sorted( + updated_values.pop('vim_connection_info'), + key=lambda x: x.id) + for e, a in zip(expected_vci, actual_vci): + self.assertDictEqual( + e.to_dict(), + a.to_dict()) + + self.assertDictEqual( + compar_updated_values, + updated_values) + + fake_api_model_query = FakeApiModelQuery( + callback_filter_by=mock_filter, callback_update=mock_update) + mock_model_query.return_value = fake_api_model_query + + vnf_lcm_opoccs = {} + + body = {"vnf_instance_name": "new_instance_name", + "vnf_instance_description": "new_instance_discription", + "vnfd_id": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnf_configurable_properties": {"test": "test_value1"}, + "vnfc_info_modifications_delete_ids": ["test1"], + "metadata": {"testkey": "test_value1", + "testkey2": "test_value2"}, + "vim_connection_info": [ + {"id": "testid", + "vim_id": "bbb", + "vim_type": "openstack-1A", + "interface_info": {"endpoint": "endpoint11"}, + "access_info": {"username": "xxxxx1", + "region": "region1", + "password": "password1", + "tenant": "tenant1"}}, + {"id": "testid3", + "vim_type": "openstack-2", + "access_info": {"password": "password2", + "tenant": "tenant2"}}, + {"id": "testid7", + "vim_id": "fff", + "vim_type": "openstack-5A", + "interface_info": {"endpoint": "endpoint55"}, + "access_info": {"username": "xxxxx5", + "region": "region5", + "password": "password5", + "tenant": "tenant5"}}, + ]} + + vnf_instance.update( + self.context, + vnf_lcm_opoccs, + body, + self.vnf_package, + self.vnf_package.id) diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index 868401d..6a3fdc1 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -120,7 +120,6 @@ def _model_non_instantiated_vnf_instance(**updates): 'tenant_id': uuidsentinel.tenant_id, 'vnfd_id': uuidsentinel.vnfd_id, 'vnfd_version': '1.0', - 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, 'vnf_metadata': {"key": "value"}} if updates: @@ -231,7 +230,6 @@ def _fake_vnf_instance_not_instantiated_response( 'vnfdId': uuidsentinel.vnfd_id, 'vnfdVersion': '1.0', 'vnfSoftwareVersion': '1.0', - 'vnfPkgId': uuidsentinel.vnf_pkg_id, 'id': uuidsentinel.vnf_instance_id, 'metadata': {'key': 'value'} } diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 5714592..7dcd4da 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -244,6 +244,7 @@ class TestController(base.TestCase): @mock.patch.object(objects.VnfPackage, 'get_by_id') @mock.patch('tacker.api.vnflcm.v1.controller.' 'VnfLcmController._create_vnf') + @mock.patch.object(objects.vnf_package.VnfPackage, 'save') @mock.patch.object(TackerManager, 'get_service_plugins', return_value={'VNFM': FakeVNFMPlugin()}) @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') @@ -252,57 +253,14 @@ class TestController(base.TestCase): self, mock_get_by_id, mock_vnf_instance_create, mock_get_service_plugins, + mock_package_save, mock_private_create_vnf, mock_vnf_package_get_by_id, mock_update_package_usage_state, mock_get_vim): - mock_get_by_id.return_value = fakes.return_vnf_package_vnfd() - - updates = {'vnfd_id': uuidsentinel.vnfd_id, - 'vnf_instance_description': None, - 'vnf_instance_name': None, - 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, - 'vnf_metadata': {"key": "value"}} - mock_vnf_instance_create.return_value =\ - fakes.return_vnf_instance_model(**updates) - - req = fake_request.HTTPRequest.blank('/vnf_instances') - body = {'vnfdId': uuidsentinel.vnfd_id, - 'metadata': {"key": "value"}} - req.body = jsonutils.dump_as_bytes(body) - req.headers['Content-Type'] = 'application/json' - req.method = 'POST' - - # Call create API - resp = req.get_response(self.app) - - self.assertEqual(http_client.CREATED, resp.status_code) - - updates = {'vnfInstanceDescription': None, 'vnfInstanceName': None} - expected_vnf = fakes.fake_vnf_instance_response(**updates) - location_header = ('http://localhost/vnflcm/v1/vnf_instances/%s' - % resp.json['id']) - - self.assertEqual(expected_vnf, resp.json) - self.assertEqual(location_header, resp.headers['location']) - - @mock.patch('tacker.api.vnflcm.v1.controller.' - 'VnfLcmController._create_vnf') - @mock.patch.object(objects.VnfInstance, 'save') - @mock.patch.object(vim_client.VimClient, "get_vim") - @mock.patch.object(objects.vnf_package.VnfPackage, 'get_by_id') - @mock.patch.object(objects.vnf_package.VnfPackage, 'save') - @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') - @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') - def test_create_with_name_and_description( - self, mock_get_by_id_package_vnfd, - mock_vnf_instance_create, mock_package_save, - mock_get_by_id_package, mock_get_vim, - mock_save, mock_create_vnf): mock_get_vim.return_value = self.vim_info - mock_get_by_id_package_vnfd.return_value = \ - fakes.return_vnf_package_vnfd() - mock_get_by_id_package.return_value = \ + mock_get_by_id.return_value = fakes.return_vnf_package_vnfd() + mock_vnf_package_get_by_id.return_value = \ fakes.return_vnf_package_with_deployment_flavour() updates = {'vnfd_id': uuidsentinel.vnfd_id, @@ -1199,8 +1157,11 @@ class TestController(base.TestCase): constants.INVALID_UUID, resp.json['itemNotFound']['message']) - @ddt.data('PATCH', 'HEAD', 'PUT', 'POST') - def test_show_invalid_http_method(self, http_method): + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @ddt.data('HEAD', 'PUT', 'POST') + def test_show_invalid_http_method(self, http_method, + mock_get_service_plugins): req = fake_request.HTTPRequest.blank( '/vnf_instances/%s' % constants.UUID) req.headers['Content-Type'] = 'application/json' @@ -1888,3 +1849,187 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.NOT_FOUND, resp.status_code) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + @mock.patch.object(objects.VnfInstanceList, "vnf_instance_list") + @mock.patch.object(objects.VnfPackageVnfd, 'get_vnf_package_vnfd') + @mock.patch.object(VNFLcmRPCAPI, "update") + def test_update_vnf( + self, + mock_update, + mock_vnf_package_vnf_get_vnf_package_vnfd, + mock_vnf_instance_list, + mock_vnf_index_list, + mock_get_service_plugins): + + mock_vnf_index_list.return_value = fakes._get_vnf() + mock_vnf_instance_list.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + mock_vnf_package_vnf_get_vnf_package_vnfd.return_value =\ + fakes.return_vnf_package_vnfd() + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfdId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": { + "test": "test_value" + }, + "vnfcInfoModificationsDeleteIds": ["test1"], + "metadata": {"testkey": "test_value"}, + "vimConnectionInfo": {"id": "testid"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + # Call Instantiate API + resp = req.get_response(self.app) + print(resp) + self.assertEqual(http_client.ACCEPTED, resp.status_code) + mock_update.assert_called_once() + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + def test_update_vnf_none_vnf_data( + self, + mock_vnf_index_list, + mock_get_service_plugins): + + mock_vnf_index_list.return_value = "" + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfdId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": { + "test": "test_value" + }, + "vnfcInfoModificationsDeleteIds": ["test1"], + "metadata": {"testkey": "test_value"}, + "vimConnectionInfo": {"id": "testid"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + msg = _("Can not find requested vnf data: %s") % constants.UUID + res = self._make_problem_detail(msg, 404, title='Not Found') + + resp = req.get_response(self.app) + self.assertEqual(res.text, resp.text) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + def test_update_vnf_status_err( + self, + mock_vnf_index_list, + mock_get_service_plugins): + updates = {'status': 'ERROR'} + mock_vnf_index_list.return_value = fakes._get_vnf(**updates) + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfdId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": { + "test": "test_value" + }, + "vnfcInfoModificationsDeleteIds": ["test1"], + "metadata": {"testkey": "test_value"}, + "vimConnectionInfo": {"id": "testid"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + msg = _("VNF %(id)s status is %(state)s") % { + "id": constants.UUID, "state": "ERROR"} + res = self._make_problem_detail(msg % + {"state": "ERROR"}, 409, 'Conflict') + + resp = req.get_response(self.app) + self.assertEqual(res.text, resp.text) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + @mock.patch.object(objects.VnfInstanceList, "vnf_instance_list") + def test_update_vnf_none_instance_data( + self, + mock_vnf_instance_list, + mock_vnf_index_list, + mock_get_service_plugins): + + mock_vnf_index_list.return_value = fakes._get_vnf() + mock_vnf_instance_list.return_value = "" + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfdId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": { + "test": "test_value" + }, + "vnfcInfoModificationsDeleteIds": ["test1"], + "metadata": {"testkey": "test_value"}, + "vimConnectionInfo": {"id": "testid"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + vnf_data = fakes._get_vnf() + msg = _("Can not find requested vnf instance data: %s") % vnf_data.get( + 'vnfd_id') + res = self._make_problem_detail(msg, 404, title='Not Found') + + resp = req.get_response(self.app) + self.assertEqual(res.text, resp.text) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(objects.VNF, "vnf_index_list") + @mock.patch.object(objects.VnfInstanceList, "vnf_instance_list") + @mock.patch.object(objects.VnfPackageVnfd, 'get_vnf_package_vnfd') + @mock.patch.object(VNFLcmRPCAPI, "update") + def test_update_vnf_none_vnfd_data( + self, + mock_update, + mock_vnf_package_vnf_get_vnf_package_vnfd, + mock_vnf_instance_list, + mock_vnf_index_list, + mock_get_service_plugins): + + mock_vnf_index_list.return_value = fakes._get_vnf() + mock_vnf_instance_list.return_value = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + mock_vnf_package_vnf_get_vnf_package_vnfd.return_value = "" + + body = {"vnfInstanceName": "new_instance_name", + "vnfInstanceDescription": "new_instance_discription", + "vnfdId": "2c69a161-0000-4b0f-bcf8-391f8fc76600", + "vnfConfigurableProperties": { + "test": "test_value" + }, + "vnfcInfoModificationsDeleteIds": ["test1"], + "metadata": {"testkey": "test_value"}, + "vimConnectionInfo": {"id": "testid"}} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s' % constants.UUID) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'PATCH' + + fakes._get_vnf() + msg = _("Can not find requested vnf package vnfd: %s") %\ + body.get('vnfdId') + res = self._make_problem_detail(msg, 400, 'Bad Request') + + resp = req.get_response(self.app) + self.assertEqual(res.text, resp.text)