diff --git a/tacker/api/views/__init__.py b/tacker/api/views/__init__.py index e69de29bb..0fb9d85cc 100644 --- a/tacker/api/views/__init__.py +++ b/tacker/api/views/__init__.py @@ -0,0 +1,92 @@ +# Copyright (C) 2020 NTT DATA +# All Rights Reserved. +# +# 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 tacker.api.common import attribute_filter +from tacker.common import exceptions as exception + + +class BaseViewBuilder(object): + + @classmethod + def validate_filter(cls, filters=None): + if not filters: + return + + return attribute_filter.parse_filter_rule(filters, + target=cls.FLATTEN_ATTRIBUTES) + + @classmethod + def validate_attribute_fields(cls, all_fields=None, fields=None, + exclude_fields=None, exclude_default=None): + + if all_fields and (fields or exclude_fields or exclude_default): + msg = ("Invalid query parameter combination: 'all_fields' " + "cannot be combined with 'fields' or 'exclude_fields' " + "or 'exclude_default'") + raise exception.ValidationError(msg) + + if fields and (all_fields or exclude_fields): + msg = ("Invalid query parameter combination: 'fields' " + "cannot be combined with 'all_fields' or 'exclude_fields' ") + raise exception.ValidationError(msg) + + if exclude_fields and (all_fields or fields or exclude_default): + msg = ("Invalid query parameter combination: 'exclude_fields' " + "cannot be combined with 'all_fields' or 'fields' " + "or 'exclude_default'") + raise exception.ValidationError(msg) + + if exclude_default and (all_fields or exclude_fields): + msg = ("Invalid query parameter combination: 'exclude_default' " + "cannot be combined with 'all_fields' or 'exclude_fields' ") + raise exception.ValidationError(msg) + + def _validate_complex_attributes(query_parameter, fields): + msg = ("Invalid query parameter '%(query_parameter)s'. " + "Value: %(field)s") + for field in fields: + if field in cls.COMPLEX_ATTRIBUTES: + continue + elif '*' in field: + # Field should never contain '*' as it's reserved for + # special purpose for handling key-value pairs. + raise exception.ValidationError(msg % + {"query_parameter": query_parameter, + "field": field}) + elif field not in cls.FLATTEN_COMPLEX_ATTRIBUTES: + # Special case for field with key-value pairs. + # In this particular case, key will act as an attribute + # in structure so you need to treat it differently than + # other fields. All key-value pair field will be post-fix + # with '*' in FLATTEN_COMPLEX_ATTRIBUTES. Request + # with field which contains '*' will be treated as an + # error. + special_field = False + for attribute in cls.FLATTEN_COMPLEX_ATTRIBUTES: + if '*' in attribute and field.startswith( + attribute.split('*')[0]): + special_field = True + + if not special_field: + raise exception.ValidationError(msg % + {"query_parameter": query_parameter, + "field": field}) + + if fields: + _validate_complex_attributes("fields", fields.split(',')) + elif exclude_fields: + _validate_complex_attributes("exclude_fields", + exclude_fields.split(",")) diff --git a/tacker/api/views/vnf_packages.py b/tacker/api/views/vnf_packages.py index da65c0b0c..5ac66bc0d 100644 --- a/tacker/api/views/vnf_packages.py +++ b/tacker/api/views/vnf_packages.py @@ -13,10 +13,16 @@ # License for the specific language governing permissions and limitations # under the License. -from tacker.objects import fields +from tacker.api import views as base +from tacker.objects import vnf_package as _vnf_package -class ViewBuilder(object): +class ViewBuilder(base.BaseViewBuilder): + + FLATTEN_ATTRIBUTES = _vnf_package.VnfPackage.FLATTEN_ATTRIBUTES + COMPLEX_ATTRIBUTES = _vnf_package.VnfPackage.COMPLEX_ATTRIBUTES + FLATTEN_COMPLEX_ATTRIBUTES = [key for key in FLATTEN_ATTRIBUTES.keys() + if '/' in key] def _get_links(self, vnf_package): return { @@ -32,64 +38,13 @@ class ViewBuilder(object): } } - def _get_software_images(self, vnf_deployment_flavours): - software_images = list() - for vnf_deployment_flavour in vnf_deployment_flavours: - for sw_image in vnf_deployment_flavour.software_images: - software_images.append({ - "id": sw_image.software_image_id, - "name": sw_image.name, - "provider": "provider", - "version": sw_image.version, - "checksum": { - "algorithm": sw_image.algorithm, - "hash": sw_image.hash - }, - "containerFormat": sw_image.container_format, - "diskFormat": sw_image.disk_format, - "minDisk": sw_image.min_disk, - "minRam": sw_image.min_ram, - "size": sw_image.size, - "imagePath": sw_image.image_path, - "userMetadata": sw_image.metadata - }) - - return {'softwareImages': software_images} - - def _get_vnfd(self, vnf_package): - vnfd = vnf_package.vnfd - return { - 'vnfdId': vnfd.vnfd_id, - 'vnfProvider': vnfd.vnf_provider, - 'vnfProductName': vnfd.vnf_product_name, - 'vnfSoftwareVersion': vnfd.vnf_software_version, - 'vnfdVersion': vnfd.vnfd_version - } - - def _basic_vnf_package_info(self, vnf_package): - return { - 'id': vnf_package.id, - 'onboardingState': vnf_package.onboarding_state, - 'operationalState': vnf_package.operational_state, - 'usageState': vnf_package.usage_state, - 'userDefinedData': vnf_package.user_data, - } - - def _get_vnf_package(self, vnf_package): - vnf_package_response = self._basic_vnf_package_info(vnf_package) + def _get_vnf_package(self, vnf_package, include_fields=None): + vnf_package_response = vnf_package.to_dict( + include_fields=include_fields) links = self._get_links(vnf_package) vnf_package_response.update(links) - if (vnf_package.onboarding_state == - fields.PackageOnboardingStateType.ONBOARDED): - # add software images - vnf_deployment_flavours = vnf_package.vnf_deployment_flavours - vnf_package_response.update(self._get_software_images( - vnf_deployment_flavours)) - - vnf_package_response.update(self._get_vnfd(vnf_package)) - return vnf_package_response def _get_modified_user_data(self, old_user_data, new_user_data): @@ -106,17 +61,11 @@ class ViewBuilder(object): return user_data_response def create(self, request, vnf_package): - return self._get_vnf_package(vnf_package) def show(self, request, vnf_package): - return self._get_vnf_package(vnf_package) - 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: @@ -127,3 +76,47 @@ class ViewBuilder(object): response['userDefinedData'] = updated_user_data return response + + def index(self, request, vnf_packages, all_fields=True, + exclude_fields=None, fields=None, exclude_default=False): + + # Find out which fields are to be returned in the response. + if all_fields: + include_fields = set(self.FLATTEN_ATTRIBUTES.keys()) + if exclude_default and fields or fields: + # Note(tpatil): If fields contains any of the complex attributes + # base name, it should include all attributes from that complex + # attribute. For example, if softwareImages is passed in the + # fields, it should include all attributes matching + # softwareImages/*. + fields = set(fields.split(',')) + attributes = set(self.COMPLEX_ATTRIBUTES).intersection(fields) + for attribute in attributes: + add_fields = set([key for key in self.FLATTEN_ATTRIBUTES. + keys() if key.startswith(attribute)]) + fields = fields.union(add_fields) + + include_fields = set(_vnf_package.VnfPackage.simple_attributes + + _vnf_package.VnfPackage.simple_instantiated_attributes). \ + union(fields) + elif exclude_default: + include_fields = set(_vnf_package.VnfPackage.simple_attributes + + _vnf_package.VnfPackage.simple_instantiated_attributes) + elif exclude_fields: + # Note(tpatil): If exclude_fields contains any of the complex + # attributes base name, it should exclude all attributes from + # that complex attribute. For example, if softwareImages is passed + # in the excluded_fields, it should exclude all attributes + # matching softwareImages/*. + exclude_fields = set(exclude_fields.split(',')) + exclude_additional_attributes = set( + self.COMPLEX_ATTRIBUTES).intersection(exclude_fields) + for attribute in exclude_additional_attributes: + fields = set([key for key in self.FLATTEN_ATTRIBUTES.keys() + if key.startswith(attribute)]) + exclude_fields = exclude_fields.union(fields) + + include_fields = set(self.FLATTEN_ATTRIBUTES.keys()) - \ + exclude_fields + return {'vnf_packages': [self._get_vnf_package(vnf_package, + include_fields=include_fields)for vnf_package in vnf_packages]} diff --git a/tacker/api/vnfpkgm/v1/controller.py b/tacker/api/vnfpkgm/v1/controller.py index da4efae0d..51d2cb004 100644 --- a/tacker/api/vnfpkgm/v1/controller.py +++ b/tacker/api/vnfpkgm/v1/controller.py @@ -114,11 +114,37 @@ class VnfPkgmController(wsgi.Controller): context = request.environ['tacker.context'] context.can(vnf_package_policies.VNFPKGM % 'index') - vnf_packages = vnf_package_obj.VnfPackagesList.get_all( - request.context, - expected_attrs=["vnf_deployment_flavours", "vnfd"]) + search_opts = {} + search_opts.update(request.GET) - return self._view_builder.index(request, vnf_packages) + def _key_exists(key, validate_value=True): + try: + request.GET[key] + except KeyError: + return False + + return True + + all_fields = _key_exists('all_fields') + exclude_default = _key_exists('exclude_default') + fields = request.GET.get('fields') + exclude_fields = request.GET.get('exclude_fields') + filters = request.GET.get('filter') + if not (all_fields or fields or exclude_fields): + exclude_default = True + + self._view_builder.validate_attribute_fields(all_fields=all_fields, + fields=fields, exclude_fields=exclude_fields, + exclude_default=exclude_default) + + filters = self._view_builder.validate_filter(filters) + + vnf_packages = vnf_package_obj.VnfPackagesList.get_by_filters( + request.context, read_deleted='no', filters=filters) + + return self._view_builder.index(request, vnf_packages, + all_fields=all_fields, exclude_fields=exclude_fields, + fields=fields, exclude_default=exclude_default) @wsgi.response(http_client.NO_CONTENT) @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND, diff --git a/tacker/common/utils.py b/tacker/common/utils.py index ccdf34ef0..4a2a57300 100644 --- a/tacker/common/utils.py +++ b/tacker/common/utils.py @@ -19,6 +19,7 @@ """Utilities and helper functions.""" import functools +from functools import reduce import inspect import logging as std_logging import math @@ -402,6 +403,22 @@ def is_url(url): return False +def flatten_dict(data, prefix=''): + ret = {} + for key, val in data.items(): + key = prefix + key + if isinstance(val, dict): + ret.update(flatten_dict(val, key + '/')) + else: + ret[key] = val + return ret + + +def deepgetattr(obj, attr): + """Recurses through an attribute chain to get the ultimate value.""" + return reduce(getattr, attr.split('.'), obj) + + class CooperativeReader(object): """An eventlet thread friendly class for reading in image data. diff --git a/tacker/objects/vnf_deployment_flavour.py b/tacker/objects/vnf_deployment_flavour.py index b6766fd7a..2fe60b142 100644 --- a/tacker/objects/vnf_deployment_flavour.py +++ b/tacker/objects/vnf_deployment_flavour.py @@ -239,6 +239,16 @@ class VnfDeploymentFlavour(base.TackerObject, base.TackerPersistentObject): objects.VnfSoftwareImage, db_sw_images) self.obj_reset_changes(['software_images']) + def to_dict(self, include_fields=None): + software_images = list() + for software_image in self.software_images: + sw_image_dict = software_image.to_dict( + include_fields=include_fields) + if sw_image_dict: + software_images.append(sw_image_dict) + + return software_images + @base.TackerObjectRegistry.register class VnfDeploymentFlavoursList(ovoo_base.ObjectListBase, base.TackerObject): @@ -248,3 +258,12 @@ class VnfDeploymentFlavoursList(ovoo_base.ObjectListBase, base.TackerObject): fields = { 'objects': fields.ListOfObjectsField('VnfDeploymentFlavour') } + + def to_dict(self, include_fields=None): + software_images = list() + for deployment_flavour in self.objects: + images = deployment_flavour.to_dict(include_fields=include_fields) + if images: + software_images.extend(images) + + return software_images diff --git a/tacker/objects/vnf_package.py b/tacker/objects/vnf_package.py index 06b00f1f7..153bb0a65 100644 --- a/tacker/objects/vnf_package.py +++ b/tacker/objects/vnf_package.py @@ -25,12 +25,14 @@ from sqlalchemy_filters import apply_filters from tacker._i18n import _ from tacker.common import exceptions +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 import objects from tacker.objects import base from tacker.objects import fields +from tacker.objects import vnf_software_image _NO_DATA_SENTINEL = object() @@ -256,14 +258,62 @@ def _make_vnf_packages_list(context, vnf_package_list, db_vnf_package_list, class VnfPackage(base.TackerObject, base.TackerPersistentObject, base.TackerObjectDictCompat): + # Key corresponds to the name of the parameter as defined + # in type VnfPkgInfo of SOL003 document and value will contain tuple + # of following values:- + # 1. Parameter that is mapped to version object + # 2. Data type of the field as per the data types supported by + # attribute-based filtering + # 3. DB model that's mapped to the version object. + # 4. Valid values for a given data type if any. This value is set + # especially for 'enum' data type. + ALL_ATTRIBUTES = { + 'id': ('id', "string", 'VnfPackage'), + 'onboardingState': ('onboarding_state', "enum", 'VnfPackage', + fields.PackageOnboardingStateTypeField().valid_values), + 'operationalState': ('operational_state', 'enum', 'VnfPackage', + fields.PackageOperationalStateTypeField().valid_values), + 'usageState': ('usage_state', 'enum', 'VnfPackage', + fields.PackageUsageStateTypeField().valid_values), + 'vnfProvider': ('vnfd.vnf_provider', 'string', 'VnfPackageVnfd'), + 'vnfProductName': ('vnfd.vnf_product_name', 'string', + 'VnfPackageVnfd'), + 'vnfdId': ('vnfd.vnfd_id', 'string', 'VnfPackageVnfd'), + 'vnfSoftwareVersion': ('vnfd.vnf_software_version', 'string', + 'VnfPackageVnfd'), + 'vnfdVersion': ('vnfd.vnfd_version', 'string', 'VnfPackageVnfd'), + 'userDefinedData/*': ('user_data', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfPackageUserData"}), + "checksum": { + 'algorithm': ('algorithm', 'string', 'VnfPackage'), + 'hash': ('hash', 'string', 'VnfPackage'), + } + } + + ALL_ATTRIBUTES.update(vnf_software_image.VnfSoftwareImage.ALL_ATTRIBUTES) + + FLATTEN_ATTRIBUTES = utils.flatten_dict(ALL_ATTRIBUTES.copy()) + + simple_attributes = ['id', 'onboardingState', 'operationalState', + 'usageState'] + simple_instantiated_attributes = ['vnfProvider', 'vnfProductName', + 'vnfdId', 'vnfSoftwareVersion', 'vnfdVersion'] + + COMPLEX_ATTRIBUTES = ["checksum", "userDefinedData"] + COMPLEX_ATTRIBUTES.extend( + vnf_software_image.VnfSoftwareImage.COMPLEX_ATTRIBUTES) + # Version 1.0: Initial version VERSION = '1.0' fields = { 'id': fields.UUIDField(nullable=False), - 'onboarding_state': fields.StringField(nullable=False), - 'operational_state': fields.StringField(nullable=False), - 'usage_state': fields.StringField(nullable=False), + 'onboarding_state': fields.PackageOnboardingStateTypeField( + nullable=False), + 'operational_state': fields.PackageOperationalStateTypeField( + nullable=False), + 'usage_state': fields.PackageUsageStateTypeField(nullable=False), 'user_data': fields.DictOfStringsField(), 'tenant_id': fields.StringField(nullable=False), 'algorithm': fields.StringField(nullable=True), @@ -277,8 +327,7 @@ class VnfPackage(base.TackerObject, base.TackerPersistentObject, @staticmethod def _from_db_object(context, vnf_package, db_vnf_package, expected_attrs=None): - if expected_attrs is None: - expected_attrs = [] + expected_attrs = expected_attrs or [] vnf_package._context = context @@ -303,7 +352,8 @@ class VnfPackage(base.TackerObject, base.TackerPersistentObject, expected_attrs=None): """Method to help with migration of extra attributes to objects.""" - expected_attrs = expected_attrs or [] + if expected_attrs is None: + expected_attrs = [] if 'vnf_deployment_flavours' in expected_attrs: vnf_package._load_vnf_deployment_flavours( @@ -460,6 +510,92 @@ class VnfPackage(base.TackerObject, base.TackerPersistentObject, else: return False + def _get_vnfd(self, include_fields=None): + response = dict() + to_fields = set(self.simple_instantiated_attributes).intersection( + include_fields) + for field in to_fields: + response[field] = utils.deepgetattr(self, + self.FLATTEN_ATTRIBUTES[field][0]) + return response + + def _get_checksum(self, include_fields=None): + response = dict() + to_fields = set([key for key in self.FLATTEN_ATTRIBUTES.keys() + if key.startswith('checksum')]) + to_fields = to_fields.intersection(include_fields) + for field in to_fields: + display_field = field.split("/")[-1] + response[display_field] = getattr(self, + self.FLATTEN_ATTRIBUTES[field][0]) + return {'checksum': response} if response else None + + def _get_user_defined_data(self, include_fields=None): + # Need special handling for field containing key-value pair. + # If user requests userDefined/key1 and if userDefineData contains + # key1=value1, key2-value2, it should return only keys that are + # requested in include_fields. If user requests only userDefinedData, + # then in that case,it should return all key/value pairs. In case, + # if any of the requested key is not present, then it should + # siliently ignore it. + key = 'userDefinedData' + if key in include_fields or 'userDefinedData/*' in include_fields: + return {key: self.user_data} + else: + # Check if user has requested specified keys from + # userDefinedData. + data_resp = dict() + key_list = [] + special_key = 'userDefinedData/' + for field in include_fields: + if field.startswith(special_key): + key_list.append(field[len(special_key):]) + + for key_req in key_list: + if key_req in self.user_data: + data_resp[key_req] = self.user_data[key_req] + + if data_resp: + return {key: data_resp} + + def _basic_vnf_package_info(self, include_fields=None): + response = dict() + to_fields = set(self.simple_attributes).intersection(include_fields) + for field in to_fields: + response[field] = getattr(self, self.FLATTEN_ATTRIBUTES[field][0]) + return response + + def to_dict(self, include_fields=None): + if not include_fields: + include_fields = set(self.FLATTEN_ATTRIBUTES.keys()) + + vnf_package_response = self._basic_vnf_package_info( + include_fields=include_fields) + + user_defined_data = self._get_user_defined_data( + include_fields=include_fields) + + if user_defined_data: + vnf_package_response.update(user_defined_data) + + if (self.onboarding_state == + fields.PackageOnboardingStateType.ONBOARDED): + + software_images = self.vnf_deployment_flavours.to_dict( + include_fields=include_fields) + if software_images: + vnf_package_response.update( + {'softwareImages': software_images}) + + vnf_package_response.update(self._get_vnfd( + include_fields=include_fields)) + + checksum = self._get_checksum(include_fields=include_fields) + if checksum: + vnf_package_response.update(checksum) + + return vnf_package_response + @base.TackerObjectRegistry.register class VnfPackagesList(ovoo_base.ObjectListBase, base.TackerObject): diff --git a/tacker/objects/vnf_software_image.py b/tacker/objects/vnf_software_image.py index 8dee08d49..78c187626 100644 --- a/tacker/objects/vnf_software_image.py +++ b/tacker/objects/vnf_software_image.py @@ -18,6 +18,7 @@ from oslo_versionedobjects import base as ovoo_base from sqlalchemy.orm import joinedload from tacker.common import exceptions +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 @@ -80,6 +81,37 @@ def _vnf_sw_image_get_by_id(context, id): @base.TackerObjectRegistry.register class VnfSoftwareImage(base.TackerObject, base.TackerPersistentObject): + ALL_ATTRIBUTES = { + "softwareImages": { + 'id': ('software_image_id', 'string', 'VnfSoftwareImage'), + 'imagePath': ('image_path', 'string', 'VnfSoftwareImage'), + 'diskFormat': ('disk_format', 'string', 'VnfSoftwareImage'), + 'userMetadata/*': ('metadata', 'key_value_pair', + {"key_column": "key", "value_column": "value", + "model": "VnfSoftwareImageMetadata"}), + 'size': ('size', 'number', 'VnfSoftwareImage'), + 'createdAt': ('created_at', 'datetime', 'VnfSoftwareImage'), + 'name': ('name', 'string', 'VnfSoftwareImage'), + 'minDisk': ('min_disk', 'number', 'VnfSoftwareImage'), + 'version': ('version', 'string', 'VnfSoftwareImage'), + 'provider': ('provider', 'string', 'VnfSoftwareImage'), + 'minRam': ('min_ram', 'number', 'VnfSoftwareImage'), + 'containerFormat': ('container_format', 'string', + 'VnfSoftwareImage'), + "checksum": { + 'hash': ('hash', 'string', 'VnfSoftwareImage'), + 'algorithm': ('algorithm', 'string', 'VnfSoftwareImage') + } + } + } + + FLATTEN_ATTRIBUTES = utils.flatten_dict(ALL_ATTRIBUTES.copy()) + SIMPLE_ATTRIBUTES = ['id', 'imagePath', 'diskFormat', 'size', + 'createdAt', 'name', 'minDisk', 'version', 'provider', 'minRam', + 'containerFormat'] + COMPLEX_ATTRIBUTES = ['softwareImages', 'softwareImages/userMetadata', + 'softwareImages/checksum'] + # Version 1.0: Initial version VERSION = '1.0' @@ -199,6 +231,64 @@ class VnfSoftwareImage(base.TackerObject, base.TackerPersistentObject): return cls._from_db_object(context, cls(), db_sw_image, expected_attrs=expected_attrs) + def _get_user_metadata(self, include_fields=None): + # Need special handling for field containing key-value pair. + # If user requests softwareImages/userMetadata/key1 and if + # softwareImages/userMetadata contains key1=value1, key2=value2, + # it should return only keys that are requested in include_fields. + # If user requests only softwareImages/userMetadata, then in that + # case, it should return all key/value pairs. If any of the requested + # key is not present, then it will siliently ignore it. + key = 'softwareImages/userMetadata' + if key in include_fields or '%s/*' % key in \ + include_fields: + return self.metadata + else: + # Check if user has requested specified keys from + # softwareImages/userMetadata. + key_list = [] + special_key = '%s/' % key + for field in include_fields: + if field.startswith(special_key): + key_list.append(field[len(special_key):]) + + data_resp = dict() + for key_req in key_list: + if key_req in self.metadata: + data_resp[key_req] = self.metadata[key_req] + + if len(key_list) > 0: + return data_resp + + def to_dict(self, include_fields=None): + response = dict() + fields = ['softwareImages/%s' % attribute for attribute in + self.SIMPLE_ATTRIBUTES] + to_fields = set(fields).intersection(include_fields) + for field in to_fields: + display_field = field.split("/")[-1] + response[display_field] = getattr(self, + self.FLATTEN_ATTRIBUTES[field][0]) + + # add checksum + to_fields = set([key for key in self.FLATTEN_ATTRIBUTES.keys() + if key.startswith('softwareImages/checksum')]) + checksum = dict() + to_fields = to_fields.intersection(include_fields) + for field in to_fields: + display_field = field.split("/")[-1] + checksum[display_field] = getattr(self, + self.FLATTEN_ATTRIBUTES[field][0]) + + if checksum: + response.update({"checksum": checksum}) + + user_metadata = self._get_user_metadata(include_fields) + if user_metadata is not None: + response.update({"userMetadata": user_metadata}) + + return response + @base.TackerObjectRegistry.register class VnfSoftwareImagesList(ovoo_base.ObjectListBase, base.TackerObject): diff --git a/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_df_simple.yaml b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_df_simple.yaml new file mode 100644 index 000000000..4ba8dbd21 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_df_simple.yaml @@ -0,0 +1,275 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Simple deployment flavour for Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - helloworld3_types.yaml + +topology_template: + inputs: + descriptor_id: + type: string + descriptor_version: + type: string + provider: + type: string + product_name: + type: string + software_version: + type: string + vnfm_info: + type: list + entry_schema: + type: string + flavour_id: + type: string + flavour_description: + type: string + + substitution_mappings: + node_type: company.provider.VNF + properties: + flavour_id: simple + requirements: + virtual_link_external: [ CP1, virtual_link ] + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_description: A simple flavour + interfaces: + Vnflcm: + # supporting only 'instantiate', 'terminate', 'modify' + # not supporting LCM script, supporting only default LCM + instantiate: [] + instantiate_start: [] + instantiate_end: [] + terminate: [] + terminate_start: [] + terminate_end: [] + modify_information: [] + modify_information_start: [] + modify_information_end: [] + # change_flavour: [] + # change_flavour_start: [] + # change_flavour_end: [] + # change_external_connectivity: [] + # change_external_connectivity_start: [] + # change_external_connectivity_end: [] + # operate: [] + # operate_start: [] + # operate_end: [] + # heal: [] + # heal_start: [] + # heal_end: [] + # scale: [] + # scale_start: [] + # scale_end: [] + # scale_to_level: [] + # scale_to_level_start: [] + # scale_to_level_end: [] + + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU1 + description: VDU1 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + sw_image_data: + name: Software of VDU1 + version: '0.4.0' + checksum: + algorithm: sha-256 + hash: b9c3036539fd7a5f87a1bf38eb05fdde8b556a1a7e664dbeda90ed3cd74b4f9d + container_format: bare + disk_format: qcow2 + min_disk: 1 GB + size: 1 GB + + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: ../Files/images/cirros-0.4.0-x86_64-disk.img + + capabilities: + virtual_compute: + properties: + virtual_memory: + virtual_mem_size: 512 MB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 1 GB + + VDU2: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU2 + description: VDU2 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 3 + + capabilities: + virtual_compute: + properties: + virtual_memory: + virtual_mem_size: 512 MB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 1 GB + requirements: + - virtual_storage: VirtualStorage + + VirtualStorage: + type: tosca.nodes.nfv.Vdu.VirtualBlockStorage + properties: + virtual_block_storage_data: + size_of_storage: 30 GB + rdma_enabled: true + sw_image_data: + name: VrtualStorage + version: '0.4.0' + checksum: + algorithm: sha-256 + hash: b9c3036539fd7a5f87a1bf38eb05fdde8b556a1a7e664dbeda90ed3cd74b4f9d + container_format: bare + disk_format: qcow2 + min_disk: 2 GB + min_ram: 8192 MB + size: 2 GB + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: ../Files/images/cirros-0.4.0-x86_64-disk.img + + CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + vnic_type: direct-physical + requirements: + - virtual_binding: VDU1 + #- virtual_link: # the target node is determined in the NSD + + CP2: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 1 + requirements: + - virtual_binding: VDU1 + - virtual_link: internalVL2 + + CP3: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 2 + requirements: + - virtual_binding: VDU2 + - virtual_link: internalVL2 + + internalVL2: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 11.11.0.0/24 + + policies: + - scaling_aspects: + type: tosca.policies.nfv.ScalingAspects + properties: + aspects: + worker_instance: + name: worker_instance_aspect + description: worker_instance scaling aspect + max_scale_level: 2 + step_deltas: + - delta_1 + + - VDU2_initial_delta: + type: tosca.policies.nfv.VduInitialDelta + properties: + initial_delta: + number_of_instances: 1 + targets: [ VDU2 ] + + - VDU2_scaling_aspect_deltas: + type: tosca.policies.nfv.VduScalingAspectDeltas + properties: + aspect: worker_instance + deltas: + delta_2: + number_of_instances: 1 + targets: [ VDU2 ] + + - instantiation_levels: + type: tosca.policies.nfv.InstantiationLevels + properties: + levels: + instantiation_level_1: + description: Smallest size + scale_info: + worker_instance: + scale_level: 0 + instantiation_level_2: + description: Largest size + scale_info: + worker_instance: + scale_level: 2 + default_level: instantiation_level_1 + + - VDU1_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 3 + targets: [ VDU1 ] + + - VDU2_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 1 + targets: [ VDU2 ] + + - internalVL2_instantiation_levels: + type: tosca.policies.nfv.VirtualLinkInstantiationLevels + properties: + levels: + instantiation_level_1: + bitrate_requirements: + root: 1048576 + leaf: 1048576 + instantiation_level_2: + bitrate_requirements: + root: 1048576 + leaf: 1048576 + targets: [ internalVL2 ] diff --git a/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_top.vnfd.yaml b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_top.vnfd.yaml new file mode 100644 index 000000000..e690da899 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_top.vnfd.yaml @@ -0,0 +1,31 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Sample VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - helloworld3_types.yaml + - helloworld3_df_simple.yaml + +topology_template: + inputs: + selected_flavour: + type: string + description: VNF deployment flavour selected by the consumer. It is provided in the API + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_id: { get_input: selected_flavour } + descriptor_id: b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 + provider: Company + product_name: Sample VNF + software_version: '1.0' + descriptor_version: '1.0' + vnfm_info: + - Tacker + requirements: + #- virtual_link_external # mapped in lower-level templates + #- virtual_link_internal # mapped in lower-level templates diff --git a/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_types.yaml b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_types.yaml new file mode 100644 index 000000000..12b92b710 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_types.yaml @@ -0,0 +1,53 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: VNF type definition + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: + company.provider.VNF: + derived_from: tosca.nodes.nfv.VNF + properties: + descriptor_id: + type: string + constraints: [ valid_values: [ b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 ] ] + default: b1bb0ce7-ebca-4fa7-95ed-4840d70a1177 + descriptor_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + provider: + type: string + constraints: [ valid_values: [ 'Company' ] ] + default: 'Company' + product_name: + type: string + constraints: [ valid_values: [ 'Sample VNF' ] ] + default: 'Sample VNF' + software_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + vnfm_info: + type: list + entry_schema: + type: string + constraints: [ valid_values: [ Tacker ] ] + default: [ Tacker ] + flavour_id: + type: string + constraints: [ valid_values: [ simple ] ] + default: simple + flavour_description: + type: string + default: "" + requirements: + - virtual_link_external: + capability: tosca.capabilities.nfv.VirtualLinkable + - virtual_link_internal: + capability: tosca.capabilities.nfv.VirtualLinkable + interfaces: + Vnflcm: + type: tosca.interfaces.nfv.Vnflcm diff --git a/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/TOSCA-Metadata/TOSCA.meta b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/TOSCA-Metadata/TOSCA.meta new file mode 100644 index 000000000..432a1b476 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/TOSCA-Metadata/TOSCA.meta @@ -0,0 +1,7 @@ +TOSCA-Meta-File-Version: 1.0 +Created-by: Hiroyuki JO +CSAR-Version: 1.1 +Entry-Definitions: Definitions/helloworld3_top.vnfd.yaml + +Name: Files/images/cirros-0.4.0-x86_64-disk.img +Content-type: application/x-iso9066-image diff --git a/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_df_simple.yaml b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_df_simple.yaml new file mode 100644 index 000000000..315adfca8 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_df_simple.yaml @@ -0,0 +1,275 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Simple deployment flavour for Demo VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - helloworld3_types.yaml + +topology_template: + inputs: + descriptor_id: + type: string + descriptor_version: + type: string + provider: + type: string + product_name: + type: string + software_version: + type: string + vnfm_info: + type: list + entry_schema: + type: string + flavour_id: + type: string + flavour_description: + type: string + + substitution_mappings: + node_type: company.provider.VNF + properties: + flavour_id: simple + requirements: + virtual_link_external: [ CP1, virtual_link ] + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_description: A simple flavour + interfaces: + Vnflcm: + # supporting only 'instantiate', 'terminate', 'modify' + # not supporting LCM script, supporting only default LCM + instantiate: [] + instantiate_start: [] + instantiate_end: [] + terminate: [] + terminate_start: [] + terminate_end: [] + modify_information: [] + modify_information_start: [] + modify_information_end: [] + # change_flavour: [] + # change_flavour_start: [] + # change_flavour_end: [] + # change_external_connectivity: [] + # change_external_connectivity_start: [] + # change_external_connectivity_end: [] + # operate: [] + # operate_start: [] + # operate_end: [] + # heal: [] + # heal_start: [] + # heal_end: [] + # scale: [] + # scale_start: [] + # scale_end: [] + # scale_to_level: [] + # scale_to_level_start: [] + # scale_to_level_end: [] + + VDU1: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU1 + description: VDU1 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 1 + sw_image_data: + name: Demo software of VDU1 + version: '0.4.0' + checksum: + algorithm: sha-512 + hash: 9a318acd9d049bbd6a8c662b18be95414b3b1f2e3d27e38e59bf99347193ac2831220aa54296ee35aee8f53c399000f095c8eca7eb611c5b2c83eeb7c30834a8 + container_format: bare + disk_format: qcow2 + min_disk: 4 GB + size: 4 GB + + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: ../Files/images/cirros-0.4.0-x86_64-disk.img + + capabilities: + virtual_compute: + properties: + virtual_memory: + virtual_mem_size: 512 MB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 1 GB + + VDU2: + type: tosca.nodes.nfv.Vdu.Compute + properties: + name: VDU2 + description: VDU2 compute node + vdu_profile: + min_number_of_instances: 1 + max_number_of_instances: 3 + + capabilities: + virtual_compute: + properties: + virtual_memory: + virtual_mem_size: 512 MB + virtual_cpu: + num_virtual_cpu: 1 + virtual_local_storage: + - size_of_storage: 1 GB + requirements: + - virtual_storage: VirtualStorage + + VirtualStorage: + type: tosca.nodes.nfv.Vdu.VirtualBlockStorage + properties: + virtual_block_storage_data: + size_of_storage: 30 GB + rdma_enabled: true + sw_image_data: + name: DemoVirtualStorage + version: '0.4.0' + checksum: + algorithm: sha-512 + hash: 9a318acd9d049bbd6a8c662b18be95414b3b1f2e3d27e38e59bf99347193ac2831220aa54296ee35aee8f53c399000f095c8eca7eb611c5b2c83eeb7c30834a8 + container_format: bare + disk_format: qcow2 + min_disk: 8 GB + min_ram: 8192 MB + size: 8 GB + artifacts: + sw_image: + type: tosca.artifacts.nfv.SwImage + file: ../Files/images/cirros-0.4.0-x86_64-disk.img + + CP1: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 0 + vnic_type: direct-physical + requirements: + - virtual_binding: VDU1 + #- virtual_link: # the target node is determined in the NSD + + CP2: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 1 + requirements: + - virtual_binding: VDU1 + - virtual_link: internalVL2 + + CP3: + type: tosca.nodes.nfv.VduCp + properties: + layer_protocols: [ ipv4 ] + order: 2 + requirements: + - virtual_binding: VDU2 + - virtual_link: internalVL2 + + internalVL2: + type: tosca.nodes.nfv.VnfVirtualLink + properties: + connectivity_type: + layer_protocols: [ ipv4 ] + description: Internal Virtual link in the VNF + vl_profile: + max_bitrate_requirements: + root: 1048576 + leaf: 1048576 + min_bitrate_requirements: + root: 1048576 + leaf: 1048576 + virtual_link_protocol_data: + - associated_layer_protocol: ipv4 + l3_protocol_data: + ip_version: ipv4 + cidr: 11.11.0.0/24 + + policies: + - scaling_aspects: + type: tosca.policies.nfv.ScalingAspects + properties: + aspects: + worker_instance: + name: worker_instance_aspect + description: worker_instance scaling aspect + max_scale_level: 2 + step_deltas: + - delta_1 + + - VDU2_initial_delta: + type: tosca.policies.nfv.VduInitialDelta + properties: + initial_delta: + number_of_instances: 1 + targets: [ VDU2 ] + + - VDU2_scaling_aspect_deltas: + type: tosca.policies.nfv.VduScalingAspectDeltas + properties: + aspect: worker_instance + deltas: + delta_2: + number_of_instances: 1 + targets: [ VDU2 ] + + - instantiation_levels: + type: tosca.policies.nfv.InstantiationLevels + properties: + levels: + instantiation_level_1: + description: Smallest size + scale_info: + worker_instance: + scale_level: 0 + instantiation_level_2: + description: Largest size + scale_info: + worker_instance: + scale_level: 2 + default_level: instantiation_level_1 + + - VDU1_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 3 + targets: [ VDU1 ] + + - VDU2_instantiation_levels: + type: tosca.policies.nfv.VduInstantiationLevels + properties: + levels: + instantiation_level_1: + number_of_instances: 1 + instantiation_level_2: + number_of_instances: 1 + targets: [ VDU2 ] + + - internalVL2_instantiation_levels: + type: tosca.policies.nfv.VirtualLinkInstantiationLevels + properties: + levels: + instantiation_level_1: + bitrate_requirements: + root: 1048576 + leaf: 1048576 + instantiation_level_2: + bitrate_requirements: + root: 1048576 + leaf: 1048576 + targets: [ internalVL2 ] diff --git a/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_top.vnfd.yaml b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_top.vnfd.yaml new file mode 100644 index 000000000..9707ddde0 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_top.vnfd.yaml @@ -0,0 +1,31 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: Demo VNF + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + - helloworld3_types.yaml + - helloworld3_df_simple.yaml + +topology_template: + inputs: + selected_flavour: + type: string + description: VNF deployment flavour selected by the consumer. It is provided in the API + + node_templates: + VNF: + type: company.provider.VNF + properties: + flavour_id: { get_input: selected_flavour } + descriptor_id: 5189df9e-7018-11ea-b97a-000c292ec2ea + provider: DemoLabs + product_name: Demo VNF + software_version: '1.0' + descriptor_version: '1.0' + vnfm_info: + - Tacker + requirements: + #- virtual_link_external # mapped in lower-level templates + #- virtual_link_internal # mapped in lower-level templates diff --git a/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_types.yaml b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_types.yaml new file mode 100644 index 000000000..6dd59503c --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_types.yaml @@ -0,0 +1,53 @@ +tosca_definitions_version: tosca_simple_yaml_1_2 + +description: VNF type definition + +imports: + - etsi_nfv_sol001_common_types.yaml + - etsi_nfv_sol001_vnfd_types.yaml + +node_types: + company.provider.VNF: + derived_from: tosca.nodes.nfv.VNF + properties: + descriptor_id: + type: string + constraints: [ valid_values: [ 5189df9e-7018-11ea-b97a-000c292ec2ea ] ] + default: 5189df9e-7018-11ea-b97a-000c292ec2ea + descriptor_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + provider: + type: string + constraints: [ valid_values: [ 'DemoLabs' ] ] + default: 'DemoLabs' + product_name: + type: string + constraints: [ valid_values: [ 'Demo VNF' ] ] + default: 'Demo VNF' + software_version: + type: string + constraints: [ valid_values: [ '1.0' ] ] + default: '1.0' + vnfm_info: + type: list + entry_schema: + type: string + constraints: [ valid_values: [ Tacker ] ] + default: [ Tacker ] + flavour_id: + type: string + constraints: [ valid_values: [ simple ] ] + default: simple + flavour_description: + type: string + default: "" + requirements: + - virtual_link_external: + capability: tosca.capabilities.nfv.VirtualLinkable + - virtual_link_internal: + capability: tosca.capabilities.nfv.VirtualLinkable + interfaces: + Vnflcm: + type: tosca.interfaces.nfv.Vnflcm diff --git a/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/TOSCA-Metadata/TOSCA.meta b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/TOSCA-Metadata/TOSCA.meta new file mode 100644 index 000000000..432a1b476 --- /dev/null +++ b/tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/TOSCA-Metadata/TOSCA.meta @@ -0,0 +1,7 @@ +TOSCA-Meta-File-Version: 1.0 +Created-by: Hiroyuki JO +CSAR-Version: 1.1 +Entry-Definitions: Definitions/helloworld3_top.vnfd.yaml + +Name: Files/images/cirros-0.4.0-x86_64-disk.img +Content-type: application/x-iso9066-image diff --git a/tacker/tests/functional/vnfpkgm/test_vnf_package.py b/tacker/tests/functional/vnfpkgm/test_vnf_package.py index fb5a8cbec..0697f15ed 100644 --- a/tacker/tests/functional/vnfpkgm/test_vnf_package.py +++ b/tacker/tests/functional/vnfpkgm/test_vnf_package.py @@ -13,8 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. +from copy import deepcopy import ddt import os +from six.moves import urllib import tempfile import time import zipfile @@ -23,6 +25,7 @@ from oslo_serialization import jsonutils import tacker.conf from tacker.tests.functional import base +from tacker.tests import utils CONF = tacker.conf.CONF @@ -37,6 +40,25 @@ class VnfPackageTest(base.BaseTackerTest): def setUp(self): super(VnfPackageTest, self).setUp() self.base_url = "/vnfpkgm/v1/vnf_packages" + # Here we create and upload vnf package. Also get 'show' api response + # as reference data for attribute filter tests + self.package_id1 = self._create_and_upload_vnf("vnfpkgm1") + show_url = self.base_url + "/" + self.package_id1 + resp, self.package1 = self.http_client.do_request(show_url, "GET") + self.assertEqual(200, resp.status_code) + + self.package_id2 = self._create_and_upload_vnf("vnfpkgm2") + show_url = self.base_url + "/" + self.package_id2 + resp, self.package2 = self.http_client.do_request(show_url, "GET") + self.assertEqual(200, resp.status_code) + + def tearDown(self): + for package_id in [self.package_id1, self.package_id2]: + self._disable_operational_state(package_id) + self._delete_vnf_package(package_id) + self._wait_for_delete(package_id) + + super(VnfPackageTest, self).tearDown() def _wait_for_delete(self, package_uuid): show_url = self.base_url + "/" + package_uuid @@ -128,43 +150,58 @@ class VnfPackageTest(base.BaseTackerTest): self.assertIn(vnf_package_list[0], package_uuids) self.assertIn(vnf_package_list[1], package_uuids) - def _get_csar_file_path(self, file_name): - file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), - '../../etc/samples/' + file_name)) - return file_path + def _get_csar_dir_path(self, csar_name): + csar_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), + "../../etc/samples/etsi/nfv", csar_name)) + return csar_dir - def test_upload_from_file_update_show_and_delete(self): - """This method tests multiple operations on vnf package. + def _create_and_upload_vnf(self, sample_name): + body = jsonutils.dumps({"userDefinedData": {"foo": "bar"}}) + vnf_package = self._create_vnf_package(body) + csar_dir = self._get_csar_dir_path(sample_name) + file_path, vnfd_id = utils.create_csar_with_unique_vnfd_id(csar_dir) + self.addCleanup(os.remove, file_path) - - upload package from file - - update the package state to DISABLED. - - show the package - - delete the package - """ + 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']) + + return 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) - file_path = self._get_csar_file_path("sample_vnf_package_csar.zip") - upload_url = "{base_path}/{id}/package_content".format( - base_path=self.base_url, id=vnf_package['id']) - with open(file_path, 'rb') as file_object: - resp, resp_body = self.http_client.do_request(upload_url, - "PUT", body=file_object, content_type='application/zip') - - self.assertEqual(202, resp.status_code) - self._wait_for_onboard(vnf_package['id']) - update_req_body = jsonutils.dumps( {"operationalState": "DISABLED", "userDefinedData": {"key1": "changed_val1", "key2": "val2", "new_key": "new_val"}}) - operational_state = "DISABLED" - user_defined_data = {"key1": "changed_val1", "new_key": "new_val"} - expected_result = {"operationalState": operational_state, - "userDefinedData": user_defined_data} + expected_result = {"operationalState": "DISABLED", + "userDefinedData": { + "key1": "changed_val1", "new_key": "new_val"}} + + csar_dir = self._get_csar_dir_path("vnfpkgm1") + file_path, vnfd_id = utils.create_csar_with_unique_vnfd_id(csar_dir) + self.addCleanup(os.remove, file_path) + 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( @@ -174,26 +211,91 @@ class VnfPackageTest(base.BaseTackerTest): self.assertEqual(200, resp.status_code) self.assertEqual(expected_result, resp_body) - - # Show vnf package and verify whether the userDefinedData - # and operationalState parameters are updated correctly. - show_url = self.base_url + "/" + vnf_package['id'] - resp, body = self.http_client.do_request(show_url, "GET") - self.assertEqual(200, resp.status_code) - self.assertEqual(operational_state, body['operationalState']) - expected_user_defined_data = {'key1': 'changed_val1', 'key2': 'val2', - 'key3': 'val3', 'new_key': 'new_val'} - self.assertEqual(expected_user_defined_data, body['userDefinedData']) - self._delete_vnf_package(vnf_package['id']) self._wait_for_delete(vnf_package['id']) + def test_index_attribute_filter(self): + filter_expr = { + 'filter': "(gt,softwareImages/minDisk,7);" + "(eq,onboardingState,ONBOARDED);" + "(eq,softwareImages/checksum/algorithm,'sha-512')" + } + filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) + resp, body = self.http_client.do_request(filter_url, "GET") + package = deepcopy(self.package2) + for attr in ['softwareImages', 'checksum', 'userDefinedData']: + package.pop(attr, None) + expected_result = {'vnf_packages': [package]} + self.assertEqual(expected_result, body) + + def test_index_attribute_selector_all_fields(self): + """Test for attribute selector 'all_fields' + + We intentionally use attribute filter along with attribute selector. + It is because when these tests run with concurrency > 1, there will + be multiple sample packages present at a time. Hence attribute + selector will be applied on all of them. It will be difficult to + predict the expected result. Hence we are limiting the result set by + filtering one of the vnf package which was created for this speific + test. + """ + filter_expr = {'filter': '(eq,id,%s)' % self.package_id1, + 'all_fields': ''} + filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) + resp, body = self.http_client.do_request(filter_url, "GET") + expected_result = {'vnf_packages': [self.package1]} + self.assertEqual(expected_result, body) + + def test_index_attribute_selector_exclude_default(self): + filter_expr = {'filter': '(eq,id,%s)' % self.package_id2, + 'exclude_default': ''} + filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) + resp, body = self.http_client.do_request(filter_url, "GET") + package2 = deepcopy(self.package2) + for attr in ['softwareImages', 'checksum', 'userDefinedData']: + package2.pop(attr, None) + expected_result = {'vnf_packages': [package2]} + self.assertEqual(expected_result, body) + + def test_index_attribute_selector_exclude_fields(self): + filter_expr = {'filter': '(eq,id,%s)' % self.package_id2, + 'exclude_fields': 'checksum,softwareImages/checksum'} + filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) + resp, body = self.http_client.do_request(filter_url, "GET") + package2 = deepcopy(self.package2) + for software_image in package2['softwareImages']: + software_image.pop('checksum', None) + package2.pop('checksum', None) + expected_result = {'vnf_packages': [package2]} + self.assertEqual(expected_result, body) + + def test_index_attribute_selector_fields(self): + filter_expr = {'filter': '(eq,id,%s)' % self.package_id1, + 'fields': 'softwareImages/checksum/hash,' + 'softwareImages/containerFormat,softwareImages/name,' + 'userDefinedData'} + filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) + resp, body = self.http_client.do_request(filter_url, "GET") + package1 = deepcopy(self.package1) + + # Prepare expected result + for software_image in package1['softwareImages']: + software_image['checksum'].pop('algorithm', None) + for attr in ['createdAt', 'diskFormat', 'id', 'imagePath', + 'minDisk', 'minRam', 'provider', 'size', 'userMetadata', + 'version']: + software_image.pop(attr, None) + package1.pop('checksum', None) + expected_result = {'vnf_packages': [package1]} + self.assertEqual(expected_result, body) + def _create_and_onboard_vnf_package(self, file_name=None): body = jsonutils.dumps({"userDefinedData": {"foo": "bar"}}) vnf_package = self._create_vnf_package(body) if file_name is None: file_name = "sample_vnf_package_csar.zip" - file_path = self._get_csar_file_path(file_name) + file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), + '../../etc/samples/' + file_name)) with open(file_path, 'rb') as file_object: resp, resp_body = self.http_client.do_request( '{base_path}/{id}/package_content'.format( diff --git a/tacker/tests/unit/db/test_vnf_package.py b/tacker/tests/unit/db/test_vnf_package.py index 6b65bdc33..09d6843b3 100644 --- a/tacker/tests/unit/db/test_vnf_package.py +++ b/tacker/tests/unit/db/test_vnf_package.py @@ -49,7 +49,7 @@ 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'} + expected_result = {'key1': 'value1', 'key2': 'value2'} result = vnf_package._add_user_defined_data( self.context, vnf_package_db.id, vnf_package_db.user_data) self.assertEqual(expected_result, result) diff --git a/tacker/tests/unit/vnfpkgm/fakes.py b/tacker/tests/unit/vnfpkgm/fakes.py index b3ef226c8..2ab5573b8 100644 --- a/tacker/tests/unit/vnfpkgm/fakes.py +++ b/tacker/tests/unit/vnfpkgm/fakes.py @@ -14,6 +14,7 @@ # under the License. +from copy import deepcopy import datetime import io import iso8601 @@ -26,70 +27,188 @@ import zipfile from tacker.api.vnfpkgm.v1.router import VnfpkgmAPIRouter from tacker import context -from tacker.db.db_sqlalchemy import models +from tacker.objects import vnf_deployment_flavour as vnf_deployment_flavour_obj from tacker.objects import vnf_package as vnf_package_obj +from tacker.objects import vnf_package_vnfd as vnf_package_vnfd_obj +from tacker.objects import vnf_software_image as vnf_software_image_obj from tacker.tests import constants from tacker.tests import uuidsentinel from tacker import wsgi -VNFPACKAGE_RESPONSE = {'_links': { - 'packageContent': { - 'href': - '/vnfpkgm/v1/vnf_packages/' - 'f26f181d-7891-4720-b022-b074ec1733ef/package_content'}, - 'self': { - 'href': - '/vnfpkgm/v1/vnf_packages/' - 'f26f181d-7891-4720-b022-b074ec1733ef'}, -}, +VNFPACKAGE_RESPONSE = { + '_links': { + 'packageContent': { + 'href': '/vnfpkgm/v1/vnf_packages/' + 'f26f181d-7891-4720-b022-b074ec1733ef/package_content'}, + 'self': { + 'href': '/vnfpkgm/v1/vnf_packages/' + 'f26f181d-7891-4720-b022-b074ec1733ef'} + }, + 'checksum': { + 'algorithm': 'fake vnf package algorithm', + 'hash': 'fake vnf package hash' + }, 'id': 'f26f181d-7891-4720-b022-b074ec1733ef', - 'onboardingState': 'CREATED', - 'operationalState': 'DISABLED', + 'onboardingState': 'ONBOARDED', + 'operationalState': 'ENABLED', 'usageState': 'NOT_IN_USE', - 'userDefinedData': {'abc': 'xyz'} + 'vnfProductName': 'fake vnf product name', + 'vnfProvider': 'fake vnf provider', + 'vnfSoftwareVersion': 'fake vnf software version', + 'vnfdId': uuidsentinel.vnfd_id, + 'vnfdVersion': 'fake vnfd version', + 'userDefinedData': {'key1': 'value1', 'key2': 'value2'}, + 'softwareImages': [{ + 'checksum': {'algorithm': 'fake-algorithm', + 'hash': 'fake software image hash'}, + 'containerFormat': 'bare', + 'createdAt': datetime.datetime(1900, 1, 1, 1, 1, 1, + tzinfo=iso8601.UTC), + 'diskFormat': 'qcow2', + 'id': 'fake_software_image_id', + 'imagePath': 'fake image path', + 'minDisk': 1, + 'minRam': 0, + 'name': 'fake software image name', + 'provider': 'fake provider', + 'size': 1, + 'userMetadata': {'key3': 'value3', 'key4': 'value4'}, + 'version': '11.22.33' + }], } -VNFPACKAGE_INDEX_RESPONSE = {'vnf_packages': [{'_links': { - 'packageContent': { - 'href': - '/vnfpkgm/v1/vnf_packages/' - 'f26f181d-7891-4720-b022-b074ec1733ef/package_content'}, - 'self': { - 'href': '/vnfpkgm/v1/vnf_packages/' - 'f26f181d-7891-4720-b022-b074ec1733ef'}}, - 'id': 'f26f181d-7891-4720-b022-b074ec1733ef', - 'onboardingState': 'CREATED', - 'operationalState': 'DISABLED', - 'usageState': 'NOT_IN_USE', - 'userDefinedData': {}}] +VNFPACKAGE_INDEX_RESPONSE = { + 'vnf_packages': [VNFPACKAGE_RESPONSE] } -def fake_vnf_package(**updates): - vnf_package = { - 'algorithm': None, - 'deleted': False, - 'deleted_at': None, - 'updated_at': None, +def index_response(remove_attrs=None, vnf_package_updates=None): + # Returns VNFPACKAGE_INDEX_RESPONSE + # parameter remove_attrs is a list of attribute names + # to be removed before returning the response + if not remove_attrs: + return VNFPACKAGE_INDEX_RESPONSE + vnf_package = deepcopy(VNFPACKAGE_RESPONSE) + for attr in remove_attrs: + vnf_package.pop(attr, None) + if vnf_package_updates: + vnf_package.update(vnf_package_updates) + return {'vnf_packages': [vnf_package]} + + +def _fake_software_image(updates=None): + software_image = { + 'id': uuidsentinel.software_image_id, + 'disk_format': 'qcow2', + 'min_ram': 0, + 'min_disk': 1, + 'container_format': 'bare', + 'provider': 'fake provider', + 'image_path': 'fake image path', + 'software_image_id': 'fake_software_image_id', + 'size': 1, + 'name': 'fake software image name', + 'hash': 'fake software image hash', + 'version': '11.22.33', + 'algorithm': 'fake-algorithm', + 'metadata': {'key3': 'value3', 'key4': 'value4'}, 'created_at': datetime.datetime(1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC), - 'hash': None, - 'location_glance_store': None, - 'onboarding_state': 'CREATED', - 'operational_state': 'DISABLED', + } + if updates: + software_image.update(updates) + return software_image + + +def return_software_image(updates=None): + software_image = _fake_software_image(updates) + obj = vnf_software_image_obj.VnfSoftwareImage(**software_image) + return obj + + +def _fake_deployment_flavour(updates=None): + deployment_flavour = { + 'id': uuidsentinel.deployment_flavour_id, + 'package_uuid': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'flavour_id': 'fake flavour id', + 'flavour_description': 'fake flavour description', + 'instantiation_levels': {"level1": 1, "level2": 2} + } + if updates: + deployment_flavour.update(updates) + return deployment_flavour + + +def _return_deployment_flavour(deployment_flavour_updates=None, + software_image_updates=None): + flavour = _fake_deployment_flavour(deployment_flavour_updates) + obj = vnf_deployment_flavour_obj.VnfDeploymentFlavour(**flavour) + + software_image = return_software_image(software_image_updates) + software_image_list = vnf_software_image_obj.VnfSoftwareImagesList() + software_image_list.objects = [software_image] + + obj.software_images = software_image_list + return obj + + +def _fake_vnfd(updates=None): + vnfd = { + 'id': uuidsentinel.vnfd_unused_id, + 'package_uuid': 'f26f181d-7891-4720-b022-b074ec1733ef', + 'vnfd_id': uuidsentinel.vnfd_id, + 'vnf_provider': 'fake vnf provider', + 'vnf_product_name': 'fake vnf product name', + 'vnfd_version': 'fake vnfd version', + 'vnf_software_version': 'fake vnf software version' + } + if updates: + vnfd.update(updates) + return vnfd + + +def _return_vnfd(updates=None): + vnfd = _fake_vnfd(updates) + return vnf_package_vnfd_obj.VnfPackageVnfd(**vnfd) + + +def fake_vnf_package(updates=None): + vnf_package = { + 'id': constants.UUID, + 'hash': 'fake vnf package hash', + 'algorithm': 'fake vnf package algorithm', + 'location_glance_store': 'fake location', + 'onboarding_state': 'ONBOARDED', + 'operational_state': 'ENABLED', 'tenant_id': uuidsentinel.tenant_id, 'usage_state': 'NOT_IN_USE', - 'user_data': {'abc': 'xyz'}, - 'id': constants.UUID, + 'user_data': {'key1': 'value1', 'key2': 'value2'}, } - if updates: vnf_package.update(updates) - return vnf_package +def return_vnfpkg_obj(vnf_package_updates=None, vnfd_updates=None, + deployment_flavour_updates=None, software_image_updates=None): + vnf_package = fake_vnf_package(vnf_package_updates) + obj = vnf_package_obj.VnfPackage(**vnf_package) + obj.vnfd = _return_vnfd(vnfd_updates) + + deployment_flavour = _return_deployment_flavour( + deployment_flavour_updates, software_image_updates) + flavour_list = vnf_deployment_flavour_obj.VnfDeploymentFlavoursList() + flavour_list.objects = [deployment_flavour] + obj.vnf_deployment_flavours = flavour_list + return obj + + +def return_vnf_package_list(): + vnf_package = return_vnfpkg_obj() + return [vnf_package] + + class InjectContext(wsgi.Middleware): """Add a 'tacker.context' to WSGI environ.""" @@ -103,64 +222,6 @@ class InjectContext(wsgi.Middleware): return self.application -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_vnf_package(onboarded=False, **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 - - if onboarded: - updates = {'onboarding_state': 'ONBOARDED', - 'operational_state': 'ENABLED', - 'algorithm': 'test', - 'hash': 'test', - 'location_glance_store': 'file:test/path/pkg-uuid', - 'updated_at': datetime.datetime( - 1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC)} - model_obj.update(fake_vnf_package(**updates)) - else: - model_obj.update(fake_vnf_package(**updates)) - - return model_obj - - -def return_vnfpkg_obj(onboarded=False, **updates): - vnf_package = vnf_package_obj.VnfPackage._from_db_object( - context, vnf_package_obj.VnfPackage(), - return_vnf_package(onboarded=onboarded, **updates), - expected_attrs=None) - return vnf_package - - -def return_vnf_package_list(): - vnf_package = return_vnfpkg_obj() - return [vnf_package] - - def wsgi_app_v1(fake_auth_context=None): inner_app_v1 = VnfpkgmAPIRouter() if fake_auth_context is not None: diff --git a/tacker/tests/unit/vnfpkgm/test_controller.py b/tacker/tests/unit/vnfpkgm/test_controller.py index b7f3dc764..0a2cd2c16 100644 --- a/tacker/tests/unit/vnfpkgm/test_controller.py +++ b/tacker/tests/unit/vnfpkgm/test_controller.py @@ -12,7 +12,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - import ddt import mock from oslo_serialization import jsonutils @@ -21,13 +20,14 @@ from six.moves import urllib from webob import exc from tacker.api.vnfpkgm.v1 import controller -from tacker.common import exceptions +from tacker.common import exceptions as tacker_exc from tacker.conductor.conductorrpc.vnf_pkgm_rpc import VNFPackageRPCAPI from tacker.glance_store import store as glance_store from tacker import objects from tacker.objects import fields from tacker.objects import vnf_package from tacker.objects.vnf_package import VnfPackagesList +from tacker.objects.vnf_software_image import VnfSoftwareImage from tacker.tests import constants from tacker.tests.unit import base from tacker.tests.unit import fake_request @@ -68,11 +68,13 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.CREATED, resp.status_code) + @mock.patch.object(VnfSoftwareImage, 'get_by_id') @mock.patch.object(vnf_package.VnfPackage, "get_by_id") - def test_show(self, mock_vnf_by_id): + def test_show(self, mock_vnf_by_id, mock_sw_image_by_id): req = fake_request.HTTPRequest.blank( '/vnfpkgm/v1/vnf_packages/%s' % constants.UUID) - mock_vnf_by_id.return_value = fakes.return_vnf_package() + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj() + mock_sw_image_by_id.return_value = fakes.return_software_image() expected_result = fakes.VNFPACKAGE_RESPONSE res_dict = self.controller.show(req, constants.UUID) self.assertEqual(expected_result, res_dict) @@ -92,18 +94,376 @@ class TestController(base.TestCase): self.assertRaises(exc.HTTPNotFound, self.controller.show, req, constants.UUID) - @mock.patch.object(VnfPackagesList, "get_all") - def test_index(self, mock_vnf_list): - req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages/') + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data('/vnfpkgm/v1/vnf_packages') + def test_index(self, path, mock_vnf_list): + req = fake_request.HTTPRequest.blank(path) mock_vnf_list.return_value = fakes.return_vnf_package_list() res_dict = self.controller.index(req) - expected_result = fakes.VNFPACKAGE_INDEX_RESPONSE + expected_result = fakes.index_response(remove_attrs=[ + 'softwareImages', 'checksum', 'userDefinedData']) self.assertEqual(expected_result, res_dict) + @mock.patch.object(VnfPackagesList, "get_by_filters") + def test_index_attribute_selector_all_fields(self, mock_vnf_list): + params = {'all_fields': ''} + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + expected_result = fakes.index_response() + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + def test_index_attribute_selector_exclude_default(self, mock_vnf_list): + params = {'exclude_default': ''} + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + expected_result = fakes.index_response(remove_attrs=[ + 'softwareImages', 'checksum', 'userDefinedData']) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'exclude_fields': 'softwareImages'}, + {'exclude_fields': 'checksum'}, + {'exclude_fields': 'userDefinedData'}, + ) + def test_index_attribute_selector_exclude_fields(self, params, + mock_vnf_list): + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + remove_attrs = [params['exclude_fields']] + expected_result = fakes.index_response(remove_attrs=remove_attrs) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'fields': 'softwareImages'}, + {'fields': 'checksum'}, + {'fields': 'userDefinedData'}, + ) + def test_index_attribute_selector_fields(self, params, mock_vnf_list): + """Test valid attribute names with fields parameter + + We can specify complex attributes in fields. Hence the data only + contains such attributes. + """ + complex_attrs = ['softwareImages', 'checksum', 'userDefinedData'] + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + remove_attrs = [x for x in complex_attrs if x != params['fields']] + expected_result = fakes.index_response(remove_attrs=remove_attrs) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + def test_index_attribute_selector_user_defined_data_combination(self, + mock_vnf_list): + """Query user defined data with fields parameter + + This test queries combination of user defined data. i.e. fields of + different complex attributes. + """ + params = { + 'fields': 'userDefinedData/key1,softwareImages/userMetadata/key3', + } + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + vnf_package_updates = { + 'userDefinedData': {'key1': 'value1'}, + 'softwareImages': [{'userMetadata': {'key3': 'value3'}}] + } + expected_result = fakes.index_response(remove_attrs=[ + 'checksum'], vnf_package_updates=vnf_package_updates) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + def test_index_attribute_selector_user_defined_data(self, mock_vnf_list): + params = {'fields': 'userDefinedData/key1,userDefinedData/key2'} + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + expected_result = fakes.index_response(remove_attrs=[ + 'checksum', 'softwareImages']) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + def test_index_attribute_selector_nested_complex_attribute(self, + mock_vnf_list): + params = {'fields': 'softwareImages/checksum/algorithm,' + 'softwareImages/minRam'} + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + vnf_package_updates = { + 'softwareImages': [{ + 'minRam': 0, + 'checksum': {'algorithm': 'fake-algorithm'} + }] + } + expected_result = fakes.index_response(remove_attrs=[ + 'checksum', 'userDefinedData'], + vnf_package_updates=vnf_package_updates) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'filter': '(eq,vnfdId,dummy_vnfd_id)'}, + {'filter': '(in,vnfdId,dummy_vnfd_id)'}, + {'filter': '(cont,vnfdId,dummy_vnfd_id)'}, + {'filter': '(neq,vnfdId,dummy_vnfd_id)'}, + {'filter': '(nin,vnfdId,dummy_vnfd_id)'}, + {'filter': '(ncont,vnfdId,dummy_vnfd_id)'}, + {'filter': '(gt,softwareImages/createdAt,2020-03-11 04:10:15+00:00)'}, + {'filter': '(gte,softwareImages/createdAt,2020-03-14 04:10:15+00:00)'}, + {'filter': '(lt,softwareImages/createdAt,2020-03-20 04:10:15+00:00)'}, + {'filter': '(lte,softwareImages/createdAt,2020-03-11 04:10:15+00:00)'}, + ) + def test_index_filter_operator(self, filter_params, mock_vnf_list): + """Tests all supported operators in filter expression """ + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + expected_result = fakes.index_response(remove_attrs=[ + 'softwareImages', 'checksum', 'userDefinedData']) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + def test_index_filter_combination(self, mock_vnf_list): + """Test multiple filter parameters separated by semicolon """ + params = {'filter': '(eq,vnfdId,dummy_vnfd_id);(eq,id,dummy_id)'} + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + expected_result = fakes.index_response(remove_attrs=[ + 'softwareImages', 'checksum', 'userDefinedData']) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'filter': '(eq,id,dummy_value)'}, + {'filter': '(eq,vnfdId,dummy_value)'}, + {'filter': '(eq,onboardingState,ONBOARDED)'}, + {'filter': '(eq,operationalState,ENABLED)'}, + {'filter': '(eq,usageState,NOT_IN_USE)'}, + {'filter': '(eq,vnfProvider,dummy_value)'}, + {'filter': '(eq,vnfProductName,dummy_value)'}, + {'filter': '(eq,vnfSoftwareVersion,dummy_value)'}, + {'filter': '(eq,vnfdVersion,dummy_value)'}, + {'filter': '(eq,userDefinedData/key1,dummy_value)'}, + {'filter': '(eq,checksum/algorithm,dummy_value)'}, + {'filter': '(eq,checksum/hash,dummy_value)'}, + {'filter': '(eq,softwareImages/id,dummy_value)'}, + {'filter': '(eq,softwareImages/imagePath,dummy_value)'}, + {'filter': '(eq,softwareImages/diskFormat,dummy_value)'}, + {'filter': '(eq,softwareImages/userMetadata/key3,dummy_value)'}, + {'filter': '(eq,softwareImages/size,0)'}, + {'filter': '(gt,softwareImages/createdAt,2020-03-14 04:10:15+00:00)'}, + {'filter': '(eq,softwareImages/name,dummy_value)'}, + {'filter': '(eq,softwareImages/minDisk,0)'}, + {'filter': '(eq,softwareImages/version,dummy_value)'}, + {'filter': '(eq,softwareImages/provider,dummy_value)'}, + {'filter': '(eq,softwareImages/minRam,0)'}, + {'filter': '(eq,softwareImages/containerFormat,dummy_value)'}, + {'filter': '(eq,softwareImages/checksum/hash,dummy_value)'}, + {'filter': '(eq,softwareImages/checksum/algorithm,dummy_value)'}, + ) + def test_index_filter_attributes(self, filter_params, mock_vnf_list): + """Test various attributes supported for filter parameter """ + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + expected_result = fakes.index_response(remove_attrs=[ + 'softwareImages', 'checksum', 'userDefinedData']) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'filter': "(eq,vnfProductName,dummy_value)"}, + {'filter': "(eq,vnfProductName,dummy value)"}, + {'filter': "(eq,vnfProductName,'dummy value')"}, + {'filter': "(eq,vnfProductName,'dummy (hi) value')"}, + {'filter': "(eq,vnfProductName,'dummy ''hi'' value')"}, + {'filter': "(eq,vnfProductName,'''dummy ''hi'' value''')"}, + ) + def test_index_filter_valid_string_values(self, filter_params, + mock_vnf_list): + """Tests all the supported string values. + + For example: + - values which are not enclosed in single quotes + - values which are enclosed in single quotes + - values having single quotes within them + - values having round brackets in them + """ + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + res_dict = self.controller.index(req) + expected_result = fakes.index_response(remove_attrs=[ + 'softwareImages', 'checksum', 'userDefinedData']) + self.assertEqual(expected_result, res_dict) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'filter': "(eq,vnfProductName,value"}, + {'filter': "eq,vnfProductName,value)"}, + {'filter': "(eq,vnfProductName,value);"}, + {'filter': "(eq , vnfProductName ,value)"}, + ) + def test_index_filter_invalid_expression(self, filter_params, + mock_vnf_list): + """Test invalid filter expression """ + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + mock_vnf_list.return_value = fakes.return_vnf_package_list() + self.assertRaises(tacker_exc.ValidationError, self.controller.index, + req) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'filter': "(eq,vnfProductName,singl'quote)"}, + {'filter': "(eq,vnfProductName,three''' quotes)"}, + {'filter': "(eq,vnfProductName,round ) bracket)"}, + {'filter': "(eq,vnfProductName,'dummy 'hi' value')"}, + {'filter': "(eq,vnfProductName,'dummy's value')"}, + {'filter': "(eq,vnfProductName,'three ''' quotes')"}, + ) + def test_index_filter_invalid_string_values(self, filter_params, + mock_vnf_list): + """Test invalid string values as per ETSI NFV SOL013 5.2.2 """ + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + mock_vnf_list.return_value = fakes.return_vnf_package_list() + self.assertRaises(tacker_exc.ValidationError, self.controller.index, + req) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'filter': '(eq,vnfdId,value1,value2)'}, + {'filter': '(fake,vnfdId,dummy_vnfd_id)'}, + {'filter': '(,vnfdId,dummy_vnfd_id)'}, + ) + def test_index_filter_invalid_operator(self, params, mock_vnf_list): + """Test invalid operator in filter expression """ + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + self.assertRaises(tacker_exc.ValidationError, self.controller.index, + req) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'filter': '(eq,fakeattr,fakevalue)'}, + {'filter': '(eq,,fakevalue)'}, + ) + def test_index_filter_invalid_attribute(self, params, mock_vnf_list): + """Test invalid attribute in filter expression """ + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + self.assertRaises(tacker_exc.ValidationError, self.controller.index, + req) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'filter': '(eq,operationalState,fake_value)'}, + {'filter': '(eq,softwareImages/size,fake_value)'}, + {'filter': '(gt,softwareImages/createdAt,fake_value)'}, + {'filter': '(eq,softwareImages/minDisk,fake_value)'}, + {'filter': '(eq,softwareImages/minRam,fake_value)'}, + ) + def test_index_filter_invalid_value_type(self, filter_params, + mock_vnf_list): + """Test values which doesn't match with attribute data type """ + query = urllib.parse.urlencode(filter_params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + self.assertRaises(tacker_exc.ValidationError, self.controller.index, + req) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'fields': 'nonExistentField'}, + {'exclude_fields': 'nonExistentField'} + ) + def test_index_attribute_selector_invalid_fields(self, params, + mock_vnf_list): + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + self.assertRaises(tacker_exc.ValidationError, self.controller.index, + req) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'fields': 'softwareImages', 'all_fields': ''}, + {'exclude_fields': 'checksum', 'all_fields': ''}, + {'fields': 'softwareImages', 'exclude_fields': 'checksum'} + ) + def test_index_attribute_selector_invalid_combination(self, params, + mock_vnf_list): + """Test invalid combination of attribute selector parameters """ + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + self.assertRaises(tacker_exc.ValidationError, self.controller.index, + req) + + @mock.patch.object(VnfPackagesList, "get_by_filters") + @ddt.data( + {'exclude_default': 'softwareImages'}, + {'all_fields': 'checksum'}, + ) + def test_index_attribute_selector_unexpected_value(self, params, + mock_vnf_list): + """Test values with the parameters which doesn't need values. """ + query = urllib.parse.urlencode(params) + req = fake_request.HTTPRequest.blank('/vnfpkgm/v1/vnf_packages?' + + query) + mock_vnf_list.return_value = fakes.return_vnf_package_list() + self.assertRaises(tacker_exc.ValidationError, self.controller.index, + req) + @mock.patch.object(vnf_package.VnfPackage, "get_by_id") @mock.patch.object(VNFPackageRPCAPI, "delete_vnf_package") def test_delete_with_204_status(self, mock_delete_rpc, mock_vnf_by_id): - mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj() + vnfpkg_updates = {'operational_state': 'DISABLED'} + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( + vnf_package_updates=vnfpkg_updates) req = fake_request.HTTPRequest.blank( '/vnf_packages/%s' % constants.UUID) req.headers['Content-Type'] = 'application/json' @@ -160,7 +520,12 @@ class TestController(base.TestCase): mock_glance_store): 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() + updates = {'onboarding_state': 'CREATED', + 'operational_state': 'DISABLED'} + vnf_package_dict = fakes.fake_vnf_package(updates) + vnf_package_obj = objects.VnfPackage(**vnf_package_dict) + mock_vnf_by_id.return_value = vnf_package_obj + mock_vnf_pack_save.return_value = vnf_package_obj mock_glance_store.return_value = 'location', 'size', 'checksum',\ 'multihash', 'loc_meta' req = fake_request.HTTPRequest.blank( @@ -208,7 +573,7 @@ class TestController(base.TestCase): file_path = "tacker/tests/etc/samples/test_data.zip" file_obj = open(file_path, "rb") vnf_obj = fakes.return_vnfpkg_obj() - vnf_obj.__setattr__('onboarding_state', 'test') + vnf_obj.__setattr__('onboarding_state', 'ONBOARDED') mock_vnf_by_id.return_value = vnf_obj req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/package_content' % constants.UUID) @@ -225,7 +590,12 @@ class TestController(base.TestCase): mock_upload_vnf_package_from_uri, mock_url_open): body = {"addressInformation": "http://test_data.zip"} - mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj() + updates = {'onboarding_state': 'CREATED', + 'operational_state': 'DISABLED'} + vnf_package_dict = fakes.fake_vnf_package(updates) + vnf_package_obj = objects.VnfPackage(**vnf_package_dict) + mock_vnf_by_id.return_value = vnf_package_obj + mock_vnf_pack_save.return_value = vnf_package_obj req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/package_content/upload_from_uri' % constants.UUID) @@ -266,7 +636,7 @@ class TestController(base.TestCase): mock_url_open): body = {"addressInformation": "http://test.zip"} vnf_obj = fakes.return_vnfpkg_obj() - vnf_obj.__setattr__('onboarding_state', 'test') + vnf_obj.__setattr__('onboarding_state', 'ONBOARDED') mock_vnf_by_id.return_value = vnf_obj req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/package_content/upload_from_uri' @@ -290,9 +660,9 @@ class TestController(base.TestCase): @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'} + vnf_package_updates = {'operational_state': 'DISABLED'} mock_vnf_by_id.return_value = \ - fakes.return_vnfpkg_obj(**update_onboarding_state) + fakes.return_vnfpkg_obj(vnf_package_updates=vnf_package_updates) req_body = {"operationalState": "ENABLED", "userDefinedData": {"testKey1": "val01", @@ -334,9 +704,10 @@ class TestController(base.TestCase): @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"}}) + fake_obj = fakes.return_vnfpkg_obj(vnf_package_updates={ + "operational_state": "DISABLED", "onboarding_state": "CREATED", + "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", @@ -355,17 +726,23 @@ class TestController(base.TestCase): @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"}} + vnf_package_updates = {"operational_state": "DISABLED", + "onboarding_state": "CREATED", + "user_data": {"testKey1": "val01", + "testKey2": "val02", + "testkey3": "val03"}} + req_body = {"userDefinedData": {"testKey1": "val01", + "testKey2": "val02", + "testkey3": "val03"}} fake_obj = fakes.return_vnfpkg_obj( - **{'user_data': body["userDefinedData"]}) + vnf_package_updates=vnf_package_updates) 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) + req, constants.UUID, body=req_body) def test_patch_with_invalid_uuid(self): body = {"operationalState": "ENABLED"} @@ -395,9 +772,9 @@ class TestController(base.TestCase): @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 + vnf_package_updates = {'operational_state': 'DISABLED'} + mock_vnf_by_id.return_value = \ + fakes.return_vnfpkg_obj(vnf_package_updates=vnf_package_updates) body = {"operationalState": "DISABLED", "userDefinedData": {"testKey1": "val01", "testKey2": "val02", "testkey3": "val03"}} @@ -409,7 +786,10 @@ class TestController(base.TestCase): @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() + vnf_package_updates = {'onboarding_state': 'CREATED', + 'operational_state': 'DISABLED'} + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( + vnf_package_updates=vnf_package_updates) body = {"operationalState": "DISABLED"} req = fake_request.HTTPRequest.blank('/vnf_packages/%s' % constants.UUID) @@ -423,7 +803,7 @@ class TestController(base.TestCase): 'application/zip,text/plain') def test_get_vnf_package_vnfd_with_valid_accept_headers( self, accept_headers, mock_vnf_by_id, mock_get_vnf_package_vnfd): - mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=True) + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj() mock_get_vnf_package_vnfd.return_value = fakes.return_vnfd_data() req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/vnfd' @@ -436,7 +816,7 @@ class TestController(base.TestCase): @mock.patch.object(vnf_package.VnfPackage, "get_by_id") def test_get_vnf_package_vnfd_with_invalid_accept_header( self, mock_vnf_by_id): - mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=True) + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj() req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/vnfd' % constants.UUID) @@ -450,7 +830,7 @@ class TestController(base.TestCase): @mock.patch.object(vnf_package.VnfPackage, "get_by_id") def test_get_vnf_package_vnfd_failed_with_bad_request( self, mock_vnf_by_id, mock_get_vnf_package_vnfd): - mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=True) + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj() mock_get_vnf_package_vnfd.return_value = fakes.return_vnfd_data() req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/vnfd' @@ -466,7 +846,7 @@ class TestController(base.TestCase): def test_get_vnf_package_vnfd_for_content_type_text_plain(self, mock_vnf_by_id, mock_get_vnf_package_vnfd): - mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=True) + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj() fake_vnfd_data = fakes.return_vnfd_data(multiple_yaml_files=False) mock_get_vnf_package_vnfd.return_value = fake_vnfd_data req = fake_request.HTTPRequest.blank( @@ -483,7 +863,12 @@ class TestController(base.TestCase): @mock.patch.object(vnf_package.VnfPackage, "get_by_id") def test_get_vnf_package_vnfd_failed_with_invalid_status( self, mock_vnf_by_id): - mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=False) + vnf_package_updates = { + 'onboarding_state': 'CREATED', + 'operational_state': 'DISABLED' + } + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( + vnf_package_updates=vnf_package_updates) req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/vnfd' % constants.UUID) @@ -513,7 +898,7 @@ class TestController(base.TestCase): % constants.UUID) req.headers['Accept'] = 'application/zip' req.method = 'GET' - mock_vnf_by_id.side_effect = exceptions.VnfPackageNotFound + mock_vnf_by_id.side_effect = tacker_exc.VnfPackageNotFound self.assertRaises(exc.HTTPNotFound, self.controller.get_vnf_package_vnfd, req, constants.UUID) @@ -522,8 +907,8 @@ class TestController(base.TestCase): @mock.patch.object(vnf_package.VnfPackage, "get_by_id") def test_get_vnf_package_vnfd_failed_with_internal_server_error( self, mock_vnf_by_id, mock_get_vnf_package_vnfd): - mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=True) - mock_get_vnf_package_vnfd.side_effect = exceptions.FailedToGetVnfdData + mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj() + mock_get_vnf_package_vnfd.side_effect = tacker_exc.FailedToGetVnfdData req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/vnfd' % constants.UUID)