From 3f5c81604e0a87c76df99a66ea7ac287f2337f5b Mon Sep 17 00:00:00 2001 From: tpatil Date: Mon, 30 Mar 2020 05:49:11 +0000 Subject: [PATCH] Support attribute filtering for GET /vnf_packages API Certain resource representations can become quite big, in particular ``GET /vnfpackages`` API which is a container for multiple sub-resources. In these cases, it can be desired to reduce the amount of data exchanged over the interface and processed by the API consumer application. An attribute selector i.e filter, allows the API consumer to choose which attributes it wants to be contained in the response. Only attributes that are not required to be present, i.e. those with a lower bound of zero on their cardinality (e.g. 0..1, 0..N) and that are not conditionally mandatory, are allowed to be omitted as part of the selection process. This patch implements attribute filtering for ``GET /vnfpackages`` API. It will support following query parameters:- * filter: Filter criterion built with attributes of VNF Package and REST operators. * all_fields: Return all fields * fields= : Return fields specified in the * exclude_fields=: Return fields not specified in the * exclude_default parameters: Return all fields except default fields * exclude_default and fields=: Return all fields except default fields and fields in the * When no parameter is specified it is treated as exclude_default. You can find more details about supported query parameters in document: ETSI GS NFV-SOL 013 V2.6.1 (2019-03 in section 5.2 and 5.3 Co-Authored-By: Prashant Bhole Change-Id: I1513d7d87967de5248787cd298c3bec65c2dde9d Implements: bp/enhance-vnf-package-support-part1 --- tacker/api/views/__init__.py | 92 ++++ tacker/api/views/vnf_packages.py | 117 +++-- tacker/api/vnfpkgm/v1/controller.py | 34 +- tacker/common/utils.py | 17 + tacker/objects/vnf_deployment_flavour.py | 19 + tacker/objects/vnf_package.py | 148 +++++- tacker/objects/vnf_software_image.py | 90 ++++ .../Definitions/helloworld3_df_simple.yaml | 275 +++++++++++ .../Definitions/helloworld3_top.vnfd.yaml | 31 ++ .../Definitions/helloworld3_types.yaml | 53 ++ .../nfv/vnfpkgm1/TOSCA-Metadata/TOSCA.meta | 7 + .../Definitions/helloworld3_df_simple.yaml | 275 +++++++++++ .../Definitions/helloworld3_top.vnfd.yaml | 31 ++ .../Definitions/helloworld3_types.yaml | 53 ++ .../nfv/vnfpkgm2/TOSCA-Metadata/TOSCA.meta | 7 + .../functional/vnfpkgm/test_vnf_package.py | 176 +++++-- tacker/tests/unit/db/test_vnf_package.py | 2 +- tacker/tests/unit/vnfpkgm/fakes.py | 259 ++++++---- tacker/tests/unit/vnfpkgm/test_controller.py | 453 ++++++++++++++++-- 19 files changed, 1896 insertions(+), 243 deletions(-) create mode 100644 tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_df_simple.yaml create mode 100644 tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_top.vnfd.yaml create mode 100644 tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/Definitions/helloworld3_types.yaml create mode 100644 tacker/tests/etc/samples/etsi/nfv/vnfpkgm1/TOSCA-Metadata/TOSCA.meta create mode 100644 tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_df_simple.yaml create mode 100644 tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_top.vnfd.yaml create mode 100644 tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/Definitions/helloworld3_types.yaml create mode 100644 tacker/tests/etc/samples/etsi/nfv/vnfpkgm2/TOSCA-Metadata/TOSCA.meta 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)