commit 9d95f915042d45bdefadaca402c555affa270670 Author: Aldinson Esto Date: Mon Aug 24 19:24:54 2020 +0900 Support flow of Getting VNF package Supported the following operations to get the VNF package information from NFVO when starting LCM operation. - VNF packages (GET) - VNF package content (GET) - VNFD in an individual VNF package (GET) - Individual VNF package artifact (GET) Implements: blueprint support-vnfm-operations Spec: https://specs.openstack.org/openstack/tacker-specs/specs/victoria/support-sol003-vnfm-operations.html Change-Id: Ibdafdda815f8e130226b9d8eef4f18639f01292c diff --git a/tacker/api/schemas/vnf_lcm.py b/tacker/api/schemas/vnf_lcm.py index 6deb2fb..d56912b 100644 --- a/tacker/api/schemas/vnf_lcm.py +++ b/tacker/api/schemas/vnf_lcm.py @@ -206,7 +206,8 @@ terminate = { 'properties': { 'terminationType': {'type': 'string', 'enum': ['FORCEFUL', 'GRACEFUL']}, - 'gracefulTerminationTimeout': {'type': 'integer', 'minimum': 0} + 'gracefulTerminationTimeout': {'type': 'integer', 'minimum': 0}, + 'additionalParams': parameter_types.keyvalue_pairs, }, 'required': ['terminationType'], 'additionalProperties': False, diff --git a/tacker/api/views/vnf_lcm.py b/tacker/api/views/vnf_lcm.py index 2ae4ff8..bfc09e2 100644 --- a/tacker/api/views/vnf_lcm.py +++ b/tacker/api/views/vnf_lcm.py @@ -116,18 +116,11 @@ class ViewBuilder(base.BaseViewBuilder): return {"_links": _links} - def _get_vnf_instance_info(self, - vnf_instance, api_version=None): + def _get_vnf_instance_info(self, vnf_instance): vnf_instance_dict = vnf_instance.to_dict() - if vnf_instance_dict.get('vim_connection_info'): - vnf_instance_dict['vim_connection_info'] = \ - self._get_vim_conn_info(vnf_instance_dict.get( - 'vim_connection_info', [])) - - if 'vnf_metadata' in vnf_instance_dict: - metadata_val = vnf_instance_dict.pop('vnf_metadata') - vnf_instance_dict['metadata'] = metadata_val - + vnf_metadata = vnf_instance_dict.pop("vnf_metadata") + if vnf_metadata: + vnf_instance_dict.update({"metadata": vnf_metadata}) vnf_instance_dict = utils.convert_snakecase_to_camelcase( vnf_instance_dict) diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index 9370ccd..c55721a 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -13,11 +13,20 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime +import requests +import six +import tacker.conf +import webob + +from oslo_db.exception import DBDuplicateEntry from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import encodeutils +from oslo_utils import excutils from oslo_utils import timeutils from oslo_utils import uuidutils +from sqlalchemy import exc as sqlexc import ast import functools @@ -25,20 +34,18 @@ import json import re import traceback -import six from six.moves import http_client from six.moves.urllib import parse -import webob - from tacker._i18n import _ from tacker.api.schemas import vnf_lcm from tacker.api import validation from tacker.api.views import vnf_lcm as vnf_lcm_view +from tacker.api.vnflcm.v1 import sync_resource from tacker.common import exceptions from tacker.common import utils from tacker.conductor.conductorrpc import vnf_lcm_rpc -import tacker.conf +from tacker.db.vnfm import vnfm_db from tacker.extensions import nfvo from tacker.extensions import vnfm from tacker import manager @@ -47,9 +54,11 @@ from tacker.objects import fields from tacker.objects import vnf_lcm_subscriptions as subscription_obj from tacker.plugins.common import constants from tacker.policies import vnf_lcm as vnf_lcm_policies +import tacker.vnfm.nfvo_client as nfvo_client from tacker.vnfm import vim_client from tacker import wsgi + CONF = tacker.conf.CONF LOG = logging.getLogger(__name__) @@ -307,73 +316,187 @@ class VnfLcmController(wsgi.Controller): return vnf_lcm_op_occs_id + def _create_vnf(self, context, vnf_instance, default_vim, attributes=None): + tenant_id = vnf_instance.tenant_id + vnfd_id = vnf_instance.vnfd_id + name = vnf_instance.vnf_instance_name + description = vnf_instance.vnf_instance_description + vnf_id = vnf_instance.id + vim_id = default_vim.get('vim_id') + placement_attr = default_vim.get('placement_attr', {}) + + try: + with context.session.begin(subtransactions=True): + vnf_db = vnfm_db.VNF(id=vnf_id, + tenant_id=tenant_id, + name=name, + description=description, + instance_id=None, + vnfd_id=vnfd_id, + vim_id=vim_id, + placement_attr=placement_attr, + status=constants.INACTIVE, + error_reason=None, + deleted_at=datetime.min) + context.session.add(vnf_db) + for key, value in attributes.items(): + arg = vnfm_db.VNFAttribute( + id=uuidutils.generate_uuid(), vnf_id=vnf_id, + key=key, value=str(value)) + context.session.add(arg) + except DBDuplicateEntry as e: + raise exceptions.DuplicateEntity( + _type="vnf", + entry=e.columns) + + def _destroy_vnf(self, context, vnf_instance): + with context.session.begin(subtransactions=True): + if vnf_instance.id: + now = timeutils.utcnow() + updated_values = {'deleted_at': now, 'status': + 'PENDING_DELETE'} + context.session.query(vnfm_db.VNFAttribute).filter_by( + vnf_id=vnf_instance.id).delete() + context.session.query(vnfm_db.VNF).filter_by( + id=vnf_instance.id).update(updated_values) + + def _update_package_usage_state(self, context, vnf_package): + """Update vnf package usage state to IN_USE/NOT_IN_USE + + If vnf package is not used by any of the vnf instances, it's usage + state should be set to NOT_IN_USE otherwise it should be set to + IN_USE. + """ + result = vnf_package.is_package_in_use(context) + if result: + vnf_package.usage_state = fields.PackageUsageStateType.IN_USE + else: + vnf_package.usage_state = fields.PackageUsageStateType.NOT_IN_USE + + vnf_package.save() + @wsgi.response(http_client.CREATED) @wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN)) @validation.schema(vnf_lcm.create) def create(self, request, body): context = request.environ['tacker.context'] context.can(vnf_lcm_policies.VNFLCM % 'create') - - req_body = utils.convert_camelcase_to_snakecase(body) - vnfd_id = req_body.get('vnfd_id') try: + req_body = utils.convert_camelcase_to_snakecase(body) + vnfd_id = req_body.get('vnfd_id') vnfd = objects.VnfPackageVnfd.get_by_id(request.context, vnfd_id) - except exceptions.VnfPackageVnfdNotFound as exc: - raise webob.exc.HTTPBadRequest(explanation=six.text_type(exc)) + except exceptions.VnfPackageVnfdNotFound: + vnf_package_info = self._find_vnf_package_info('vnfdId', + vnfd_id) + if not vnf_package_info: + msg = ( + _("Can not find requested to NFVO, \ + vnf package info: vnfdId=[%s]") % + vnfd_id) + return self._make_problem_detail( + msg, 404, title='Not Found') - # get default vim information - vim_client_obj = vim_client.VimClient() - default_vim = vim_client_obj.get_vim(context) - - # set vim_connection_info - access_info = { - 'username': default_vim.get('vim_auth', {}).get('username'), - 'password': default_vim.get('vim_auth', {}).get('password'), - 'region': default_vim.get('placement_attr', {}).get('region'), - 'tenant': default_vim.get('tenant') - } - vim_con_info = objects.VimConnectionInfo(id=default_vim.get('vim_id'), - vim_id=default_vim.get('vim_id'), - vim_type=default_vim.get('vim_type'), - access_info=access_info) - - vnf_instance = objects.VnfInstance( - context=request.context, - vnf_instance_name=req_body.get('vnf_instance_name'), - vnf_instance_description=req_body.get( - 'vnf_instance_description'), - vnfd_id=vnfd_id, - instantiation_state=fields.VnfInstanceState.NOT_INSTANTIATED, - vnf_provider=vnfd.vnf_provider, - vnf_product_name=vnfd.vnf_product_name, - vnf_software_version=vnfd.vnf_software_version, - vnfd_version=vnfd.vnfd_version, - vnf_pkg_id=vnfd.package_uuid, - tenant_id=request.context.project_id, - vnf_metadata=req_body.get('metadata')) - - vnf_instance.create() - - # add default vim to vim_connection_info - setattr(vnf_instance, 'vim_connection_info', [vim_con_info]) - - # create notification data - notification = { - 'notificationType': + vnfd = sync_resource.SyncVnfPackage.create_package( + context, + vnf_package_info) + if not vnfd: + msg = ( + _("Can not find requested to NFVO, \ + vnf package vnfd: %s") % + vnfd_id) + return self._make_problem_detail( + msg, 500, 'Internal Server Error') + try: + # get default vim information + vim_client_obj = vim_client.VimClient() + default_vim = vim_client_obj.get_vim(context) + + vnf_instance = objects.VnfInstance( + context=request.context, + vnf_instance_name=req_body.get('vnf_instance_name'), + vnf_instance_description=req_body.get( + 'vnf_instance_description'), + vnfd_id=vnfd_id, + instantiation_state=fields.VnfInstanceState.NOT_INSTANTIATED, + vnf_provider=vnfd.vnf_provider, + vnf_product_name=vnfd.vnf_product_name, + vnf_software_version=vnfd.vnf_software_version, + vnfd_version=vnfd.vnfd_version, + tenant_id=request.context.project_id, + vnf_metadata=req_body.get('metadata')) + + try: + vnf_instance.create() + + # create entry to 'vnf' table and 'vnf_attribute' table + attributes = {'placement_attr': default_vim. + get('placement_attr', {})} + self._create_vnf(context, vnf_instance, + default_vim, attributes) + # get vnf package + vnf_package = objects.VnfPackage.get_by_id(context, + vnfd.package_uuid, expected_attrs=['vnfd']) + # Update VNF Package to IN_USE + self._update_package_usage_state(context, vnf_package) + except Exception: + with excutils.save_and_reraise_exception(): + # roll back db changes + self._destroy_vnf(context, vnf_instance) + vnf_instance.destroy(context) + self._update_package_usage_state(context, vnf_package) + + # create notification data + notification = { + 'notificationType': fields.LcmOccsNotificationType.VNF_ID_CREATION_NOTIFICATION, - 'vnfInstanceId': vnf_instance.id, - 'links': { - 'vnfInstance': { - 'href': self._get_vnf_instance_href(vnf_instance)}}} + 'vnfInstanceId': vnf_instance.id, + 'links': { + 'vnfInstance': { + 'href': self._get_vnf_instance_href(vnf_instance)}}} - # call send nootification - self.rpc_api.send_notification(context, notification) - vnf_instance.save() + # call sendNotification + self.rpc_api.send_notification(context, notification) - result = self._view_builder.create(vnf_instance) - headers = {"location": self._get_vnf_instance_href(vnf_instance)} - return wsgi.ResponseObject(result, headers=headers) + result = self._view_builder.create(vnf_instance) + headers = {"location": self._get_vnf_instance_href(vnf_instance)} + return wsgi.ResponseObject(result, headers=headers) + + except nfvo.VimDefaultNotDefined as exc: + raise webob.exc.HTTPBadRequest(explanation=six.text_type(exc)) + except(sqlexc.SQLAlchemyError, Exception)\ + as exc: + raise webob.exc.HTTPInternalServerError( + explanation=six.text_type(exc)) + except webob.exc.HTTPNotFound as e: + return self._make_problem_detail(str(e), 404, + 'Not Found') + except webob.exc.HTTPInternalServerError as e: + return self._make_problem_detail(str(e), 500, + 'Internal Server Error') + except Exception as e: + return self._make_problem_detail(str(e), 500, + 'Internal Server Error') + + def _find_vnf_package_info(self, filter_key, filter_val): + try: + vnf_package_info_res = \ + nfvo_client.VnfPackageRequest.index(params={ + "filter": + "(eq,{},{})".format(filter_key, filter_val) + }) + except requests.exceptions.RequestException as e: + LOG.exception(e) + return None + + if not vnf_package_info_res.ok: + return None + + vnf_package_info = vnf_package_info_res.json() + if (not vnf_package_info or len(vnf_package_info) == 0): + return None + + return vnf_package_info[0] @wsgi.response(http_client.OK) @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND)) @@ -401,8 +524,16 @@ class VnfLcmController(wsgi.Controller): @check_vnf_state(action="delete", instantiation_state=[fields.VnfInstanceState.NOT_INSTANTIATED], task_state=[None]) - def _delete(self, context, vnf_instance): + @check_vnf_status(action="delete", + status=[constants.INACTIVE]) + def _delete(self, context, vnf_instance, vnf): + vnf_package_vnfd = objects.VnfPackageVnfd.get_by_id(context, + vnf_instance.vnfd_id) vnf_instance.destroy(context) + self._destroy_vnf(context, vnf_instance) + vnf_package = objects.VnfPackage.get_by_id(context, + vnf_package_vnfd.package_uuid, expected_attrs=['vnfd']) + self._update_package_usage_state(context, vnf_package) @wsgi.response(http_client.NO_CONTENT) @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND, @@ -411,7 +542,8 @@ class VnfLcmController(wsgi.Controller): context = request.environ['tacker.context'] vnf_instance = self._get_vnf_instance(context, id) - self._delete(context, vnf_instance) + vnf = self._get_vnf(context, id) + self._delete(context, vnf_instance, vnf) notification = { "notificationType": "VnfIdentifierDeletionNotification", diff --git a/tacker/api/vnflcm/v1/sync_resource.py b/tacker/api/vnflcm/v1/sync_resource.py new file mode 100644 index 0000000..f9219bf --- /dev/null +++ b/tacker/api/vnflcm/v1/sync_resource.py @@ -0,0 +1,138 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_log import log as logging + +from tacker.common import csar_utils +from tacker.common import exceptions +from tacker.common import utils +from tacker.conductor.conductorrpc import vnf_pkgm_rpc +from tacker.glance_store import store as glance_store +from tacker import objects +from tacker.objects import fields +import tacker.vnfm.nfvo_client as nfvo_client +import time +import webob + + +CONF = cfg.CONF + +LOG = logging.getLogger(__name__) + + +class SyncVnfPackage: + + vnf_package_rpc_api = vnf_pkgm_rpc.VNFPackageRPCAPI() + + @classmethod + def create_package(cls, context, vnf_package_info): + """vnf_package, create a vnf_package_vnfd table.""" + + vnf_package_info = utils.convert_camelcase_to_snakecase( + vnf_package_info) + + try: + vnf_package = cls.__create_vnf_package(context, vnf_package_info) + except Exception as exc: + raise webob.exc.HTTPInternalServerError( + explanation=exc) + + try: + artifact_paths = cls._get_artifact_paths(vnf_package_info) + vnf_package_binary = \ + nfvo_client.VnfPackageRequest.download_vnf_packages( + vnf_package.id, artifact_paths) + except nfvo_client.UndefinedExternalSettingException as exc: + raise webob.exc.HTTPNotFound(explanation=exc) + except (nfvo_client.FaliedDownloadContentException, Exception) as exc: + raise webob.exc.HTTPInternalServerError( + explanation=exc) + + try: + (location, size, _, multihash, _) = glance_store.store_csar( + context, vnf_package.id, vnf_package_binary) + + cls.__update_vnf_package(vnf_package, location, size, multihash) + cls.vnf_package_rpc_api.upload_vnf_package_content( + context, vnf_package) + vnf_package_vnfd = cls._get_vnf_package_vnfd( + context, vnf_package_info.get('vnfd_id')) + except Exception as exc: + raise webob.exc.HTTPInternalServerError( + explanation=exc) + + return vnf_package_vnfd + + @classmethod + def _get_artifact_paths(cls, vnf_package_info): + additional_artifacts = vnf_package_info.get('additional_artifacts') + if additional_artifacts is None: + return None + + return [artifact.get('artifact_path') + for artifact in additional_artifacts + if 'artifact_path' in artifact] + + @classmethod + def __store_csar(cls, context, id, body): + (location, size, checksum, multihash, + loc_meta) = glance_store.store_csar(context, id, body) + return location, size, checksum, multihash, loc_meta + + @classmethod + def __load_csar(cls, context, vnf_package): + location = vnf_package.location_glance_store + zip_path = glance_store.load_csar(vnf_package.id, location) + vnf_data, flavours = csar_utils.load_csar_data( + context.elevated(), vnf_package.id, zip_path) + return vnf_data, flavours + + @classmethod + def __create_vnf_package(cls, context, vnf_package_info): + """VNF Package Table Registration.""" + vnf_package = objects.VnfPackage( + context=context, + id=vnf_package_info.get('id'), + onboarding_state=fields.PackageOnboardingStateType.CREATED, + operational_state=fields.PackageOperationalStateType.DISABLED, + usage_state=fields.PackageUsageStateType.NOT_IN_USE, + tenant_id=context.project_id + ) + vnf_package.create() + return vnf_package + + @classmethod + def __update_vnf_package(cls, vnf_package, location, size, multihash): + """VNF Package Table Update.""" + vnf_package.algorithm = CONF.vnf_package.hashing_algorithm + vnf_package.location_glance_store = location + vnf_package.hash = multihash + vnf_package.size = size + vnf_package.save() + + @classmethod + def _get_vnf_package_vnfd(cls, context, vnfd_id): + """Get VNF Package VNFD.""" + for num in range(CONF.vnf_lcm.retry_num): + try: + vnfd = objects.VnfPackageVnfd.get_by_id( + context, + vnfd_id) + return vnfd + except exceptions.VnfPackageVnfdNotFound: + LOG.debug("retry_wait %s" % + CONF.vnf_lcm.retry_wait) + time.sleep(CONF.vnf_lcm.retry_wait) + + return None diff --git a/tacker/api/vnfpkgm/v1/controller.py b/tacker/api/vnfpkgm/v1/controller.py index c8e3f20..ceaf47f 100644 --- a/tacker/api/vnfpkgm/v1/controller.py +++ b/tacker/api/vnfpkgm/v1/controller.py @@ -13,8 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. - from io import BytesIO +import json import mimetypes import os @@ -302,19 +302,37 @@ class VnfPkgmController(wsgi.Controller): context = request.environ['tacker.context'] context.can(vnf_package_policies.VNFPKGM % 'upload_package_content') - vnf_package = self._get_vnf_package(id, request) + # check if id is of type uuid format + if not uuidutils.is_uuid_like(id): + msg = _("Can not find requested vnf package: %s") % id + return self._make_problem_detail('Not Found', msg, 404) + + try: + vnf_package = vnf_package_obj.VnfPackage.get_by_id( + request.context, id) + except exceptions.VnfPackageNotFound: + msg = _("Can not find requested vnf package: %s") % id + return self._make_problem_detail('Not Found', msg, 404) + except Exception as e: + return self._make_problem_detail( + 'Internal Server Error', str(e), 500) if vnf_package.onboarding_state != \ fields.PackageOnboardingStateType.CREATED: msg = _("VNF Package %(id)s onboarding state " "is not %(onboarding)s") - raise webob.exc.HTTPConflict(explanation=msg % {"id": id, - "onboarding": fields.PackageOnboardingStateType.CREATED}) + return self._make_problem_detail('Conflict', msg % {"id": id, + "onboarding": fields.PackageOnboardingStateType.CREATED}, + 409) vnf_package.onboarding_state = ( fields.PackageOnboardingStateType.UPLOADING) - vnf_package.save() + try: + vnf_package.save() + except Exception as e: + return self._make_problem_detail( + 'Internal Server Error', str(e), 500) try: (location, size, checksum, multihash, @@ -323,16 +341,29 @@ class VnfPkgmController(wsgi.Controller): with excutils.save_and_reraise_exception(): vnf_package.onboarding_state = ( fields.PackageOnboardingStateType.CREATED) - vnf_package.save() - - vnf_package.onboarding_state = ( - fields.PackageOnboardingStateType.PROCESSING) + try: + vnf_package.save() + except Exception as e: + return self._make_problem_detail( + 'Internal Server Error', str(e), 500) vnf_package.algorithm = CONF.vnf_package.hashing_algorithm vnf_package.hash = multihash vnf_package.location_glance_store = location vnf_package.size = size - vnf_package.save() + try: + vnf_package.save() + except Exception as e: + vnf_package.onboarding_state = ( + fields.PackageOnboardingStateType.CREATED) + try: + vnf_package.save() + except Exception as e: + return self._make_problem_detail( + 'Internal Server Error', str(e), 500) + + return self._make_problem_detail( + 'Internal Server Error', str(e), 500) # process vnf_package self.rpc_api.upload_vnf_package_content(context, vnf_package) @@ -618,6 +649,16 @@ class VnfPkgmController(wsgi.Controller): return buff.getvalue() + def _make_problem_detail(self, title, detail, status): + res = webob.Response(content_type='application/problem+json') + problemDetails = {} + problemDetails['title'] = title + problemDetails['detail'] = detail + problemDetails['status'] = status + res.text = json.dumps(problemDetails) + res.status_int = status + return res + def create_resource(): body_deserializers = { diff --git a/tacker/common/csar_utils.py b/tacker/common/csar_utils.py index 227d1b3..8c486b5 100644 --- a/tacker/common/csar_utils.py +++ b/tacker/common/csar_utils.py @@ -12,6 +12,7 @@ # 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 copy import deepcopy import hashlib import os import re @@ -29,6 +30,7 @@ import zipfile from tacker.common import exceptions import tacker.conf +from tacker.extensions import vnfm import urllib.request as urllib2 @@ -124,8 +126,10 @@ def _get_software_image(custom_defs, nodetemplate_name, node_tpl): properties = node_tpl['properties'] sw_image_artifact = _get_sw_image_artifact(node_tpl.get('artifacts')) if sw_image_artifact: + image_path = sw_image_artifact['file'].lstrip("./") properties['sw_image_data'].update( - {'software_image_id': nodetemplate_name}) + {'software_image_id': nodetemplate_name, + 'image_path': image_path}) sw_image_data = properties['sw_image_data'] if 'metadata' in sw_image_artifact: sw_image_data.update({'metadata': @@ -137,7 +141,79 @@ def _populate_flavour_data(tosca): flavours = [] if tosca.nested_tosca_templates_with_topology: for tp in tosca.nested_tosca_templates_with_topology: - _get_flavour_data(tp, flavours) + sw_image_list = [] + + # Setting up flavour data + flavour_id = tp.substitution_mappings.properties.get('flavour_id') + if flavour_id: + flavour = {'flavour_id': flavour_id} + tpl_dict = dict() + + # get from top-vnfd data + for key, value in tosca.tpl.items(): + if key in CONF.vnf_package.get_top_list: + tpl_dict[key] = value + + # get from lower-vnfd data + tpl_dict['topology_template'] = dict() + tpl_dict['topology_template']['policies'] = \ + tp.tpl.get('policies') + tpl_dict['topology_template']['node_templates'] = \ + deepcopy(tp.tpl.get('node_templates')) + for e_node in CONF.vnf_package.exclude_node: + if tpl_dict['topology_template']['node_templates'].\ + get(e_node): + del (tpl_dict['topology_template'] + ['node_templates'][e_node]) + tpl_dict['topology_template']['inputs'] = \ + deepcopy(tp.tpl.get('inputs')) + for del_input in CONF.vnf_package.del_input_list: + if tpl_dict['topology_template']['inputs'].get(del_input): + del tpl_dict['topology_template']['inputs'][del_input] + if len(tpl_dict['topology_template']['inputs']) < 1: + del tpl_dict['topology_template']['inputs'] + + flavour.update({'tpl_dict': tpl_dict}) + + instantiation_levels = _get_instantiation_levels(tp.policies) + if instantiation_levels: + flavour.update( + {'instantiation_levels': instantiation_levels}) + + mgmt_driver = None + for template_name, node_tpl in \ + tp.tpl.get('node_templates').items(): + # check the flavour property in vnf data + _update_flavour_data_from_vnf( + tp.custom_defs, node_tpl, flavour) + if node_tpl['type'] in CONF.vnf_package.get_lower_list: + if node_tpl['type'] == "tosca.nodes.nfv.VDU.Tacker": + # get mgmt_driver + mgmt_driver_flavour = \ + node_tpl['properties'].get('mgmt_driver') + if mgmt_driver_flavour: + if mgmt_driver and \ + mgmt_driver_flavour != mgmt_driver: + raise vnfm.MultipleMGMTDriversSpecified() + mgmt_driver = mgmt_driver_flavour + flavour.update({'mgmt_driver': mgmt_driver}) + + for template_name, node_tpl in \ + tp.tpl.get('node_templates').items(): + # Update the software image data + sw_image = _get_software_image(tp.custom_defs, + template_name, + node_tpl) + if sw_image: + sw_image_list.append(sw_image) + + # Add software images for flavour + if sw_image_list: + flavour.update({'sw_images': sw_image_list}) + + if flavour: + flavours.append(flavour) + else: _get_flavour_data(tosca.topology_template, flavours) diff --git a/tacker/common/exceptions.py b/tacker/common/exceptions.py index 29c945f..bd97588 100644 --- a/tacker/common/exceptions.py +++ b/tacker/common/exceptions.py @@ -356,5 +356,9 @@ class UserDataUpdateCreateFailed(TackerException): "or created after %(retries)d retries.") +class DBAccessError(TackerException): + message = _("DB Access Error") + + class SeeOther(TackerException): code = 303 diff --git a/tacker/conductor/conductor_server.py b/tacker/conductor/conductor_server.py index 7ca82dc..f03815e 100644 --- a/tacker/conductor/conductor_server.py +++ b/tacker/conductor/conductor_server.py @@ -18,6 +18,7 @@ import functools import inspect import json import os +import oslo_messaging import shutil import sys import time @@ -28,7 +29,6 @@ import yaml from glance_store import exceptions as store_exceptions from oslo_config import cfg from oslo_log import log as logging -import oslo_messaging from oslo_serialization import jsonutils from oslo_service import periodic_task from oslo_service import service @@ -42,20 +42,25 @@ from sqlalchemy.orm import exc as orm_exc from tacker import auth from tacker.common import coordination from tacker.common import csar_utils +from tacker.common import driver_manager from tacker.common import exceptions from tacker.common import log from tacker.common import safe_utils from tacker.common import topics from tacker.common import utils +import tacker.conf from tacker import context as t_context from tacker.db.common_services import common_services_db from tacker.db.nfvo import nfvo_db +from tacker.db.vnfm import vnfm_db from tacker.extensions import nfvo from tacker.glance_store import store as glance_store from tacker import manager from tacker import objects from tacker.objects import fields from tacker.objects.vnf_package import VnfPackagesList +from tacker.objects import vnfd as vnfd_db +from tacker.objects import vnfd_attribute as vnfd_attribute_db from tacker.plugins.common import constants from tacker import service as tacker_service from tacker import version @@ -63,7 +68,7 @@ from tacker.vnflcm import utils as vnflcm_utils from tacker.vnflcm import vnflcm_driver from tacker.vnfm import plugin -CONF = cfg.CONF +CONF = tacker.conf.CONF # NOTE(tpatil): keystone_authtoken opts registered explicitly as conductor # service doesn't use the keystonemiddleware.authtoken middleware as it's @@ -97,6 +102,13 @@ cfg.CONF.register_opts(OPTS, 'keystone_authtoken') LOG = logging.getLogger(__name__) +_INACTIVE_STATUS = ('INACTIVE') +_ACTIVE_STATUS = ('ACTIVE') +_PENDING_STATUS = ('PENDING_CREATE', + 'PENDING_TERMINATE', + 'PENDING_DELETE', + 'PENDING_HEAL') + def _delete_csar(context, vnf_package): # Delete from glance store @@ -154,6 +166,9 @@ class Conductor(manager.Manager): super(Conductor, self).__init__(host=self.conf.host) self.vnfm_plugin = plugin.VNFMPlugin() self.vnflcm_driver = vnflcm_driver.VnfLcmDriver() + self.vnf_manager = driver_manager.DriverManager( + 'tacker.tacker.vnfm.drivers', + cfg.CONF.tacker.infra_driver) def start(self): coordination.COORDINATOR.start() @@ -223,7 +238,7 @@ class Conductor(manager.Manager): vnf_sw_image.min_ram = 0 vnf_sw_image.min_disk = int(sw_image.get('min_disk').split()[0]) vnf_sw_image.size = int(sw_image.get('size').split()[0]) - vnf_sw_image.image_path = '' + vnf_sw_image.image_path = sw_image['image_path'] vnf_sw_image.software_image_id = sw_image['software_image_id'] vnf_sw_image.metadata = sw_image.get('metadata', dict()) vnf_sw_image.create() @@ -233,8 +248,9 @@ class Conductor(manager.Manager): deploy_flavour.package_uuid = package_uuid deploy_flavour.flavour_id = flavour['flavour_id'] deploy_flavour.flavour_description = flavour['flavour_description'] - deploy_flavour.instantiation_levels = \ - flavour.get('instantiation_levels') + if flavour.get('instantiation_levels'): + deploy_flavour.instantiation_levels = \ + flavour.get('instantiation_levels') deploy_flavour.create() sw_images = flavour.get('sw_images') @@ -273,27 +289,55 @@ class Conductor(manager.Manager): package_vnfd.vnfd_version = vnf_data.get('descriptor_version') package_vnfd.create() + self._onboard_vnfd(context, vnf_package, vnf_data, flavours) + for flavour in flavours: self._create_flavour(context, vnf_package.id, flavour) + def _onboard_vnfd(self, context, vnf_package, vnf_data, flavours): + vnfd = vnfd_db.Vnfd(context=context) + vnfd.id = vnf_data.get('descriptor_id') + vnfd.tenant_id = context.tenant_id + vnfd.name = vnf_data.get('product_name') + '-' + \ + vnf_data.get('descriptor_version') + vnfd.discription = vnf_data.get('discription') + for flavour in flavours: + if flavour.get('mgmt_driver'): + vnfd.mgmt_driver = flavour.get('mgmt_driver') + break + vnfd.create() + + for flavour in flavours: + vnfd_attribute = vnfd_attribute_db.VnfdAttribute(context=context) + vnfd_attribute.id = uuidutils.generate_uuid() + vnfd_attribute.vnfd_id = vnf_data.get('descriptor_id') + vnfd_attribute.key = 'vnfd_' + flavour['flavour_id'] + vnfd_attribute.value = \ + yaml.dump(flavour.get('tpl_dict'), default_flow_style=False) + vnfd_attribute.create() + + break + @revert_upload_vnf_package def upload_vnf_package_content(self, context, vnf_package): - location = vnf_package.location_glance_store - zip_path = glance_store.load_csar(vnf_package.id, location) - vnf_data, flavours, vnf_artifacts = csar_utils.load_csar_data( - context.elevated(), vnf_package.id, zip_path) - self._onboard_vnf_package( - context, - vnf_package, - vnf_data, - flavours, - vnf_artifacts) vnf_package.onboarding_state = ( - fields.PackageOnboardingStateType.ONBOARDED) - vnf_package.operational_state = ( - fields.PackageOperationalStateType.ENABLED) + fields.PackageOnboardingStateType.PROCESSING) + try: + vnf_package.save() - vnf_package.save() + location = vnf_package.location_glance_store + zip_path = glance_store.load_csar(vnf_package.id, location) + vnf_data, flavours, vnf_artifacts = csar_utils.load_csar_data( + context.elevated(), vnf_package.id, zip_path) + self._onboard_vnf_package(context, vnf_package, vnf_data, flavours) + vnf_package.onboarding_state = ( + fields.PackageOnboardingStateType.ONBOARDED) + vnf_package.operational_state = ( + fields.PackageOperationalStateType.ENABLED) + vnf_package.save() + + except Exception as msg: + raise Exception(msg) @revert_upload_vnf_package def upload_vnf_package_from_uri(self, context, vnf_package, @@ -433,6 +477,180 @@ class Conductor(manager.Manager): vnf_package.save() + @log.log + def _change_vnf_status(self, context, vnf_id, + current_statuses, new_status, error_reason=None): + '''Change vnf status and add error reason if error''' + LOG.debug("Change status of vnf %s from %s to %s", vnf_id, + current_statuses, new_status) + + with context.session.begin(subtransactions=True): + updated_values = { + 'status': new_status, 'updated_at': timeutils.utcnow()} + vnf_model = (context.session.query( + vnfm_db.VNF).filter_by(id=vnf_id).first()) + if not vnf_model: + raise exceptions.VnfInstanceNotFound( + message="VNF {} not found".format(vnf_id)) + if vnf_model.status not in current_statuses: + raise exceptions.VnfConflictState( + message='Cannot change status to {} \ + while in {}'.format( + updated_values['status'], vnf_model.status)) + vnf_model.update(updated_values) + + def _update_vnf_attributes(self, context, vnf_dict, current_statuses, + new_status): + with context.session.begin(subtransactions=True): + try: + modified_attributes = {} + added_attributes = {} + updated_values = { + 'mgmt_ip_address': vnf_dict['mgmt_ip_address'], + 'status': new_status, + 'updated_at': timeutils.utcnow()} + vnf_model = (context.session.query(vnfm_db.VNF).filter_by( + id=vnf_dict['id']).first()) + if not vnf_model: + raise exceptions.VnfInstanceNotFound( + message="VNF {} not found".format(vnf_dict['id'])) + if vnf_model.status not in current_statuses: + raise exceptions.VnfConflictState( + message='Cannot change status to {} while \ + in {}'.format(updated_values['status'], + vnf_model.status)) + vnf_model.update(updated_values) + + for key, val in vnf_dict['attributes'].items(): + vnf_attr_model = (context.session.query( + vnfm_db.VNFAttribute). + filter_by(vnf_id=vnf_dict['id']). + filter_by(key=key).first()) + if vnf_attr_model: + modified_attributes.update( + {vnf_attr_model.key: vnf_attr_model.value}) + vnf_attr_model.update({'value': val}) + else: + added_attributes.update({key: val}) + vnf_attr_model = vnfm_db.VNFAttribute( + id=uuidutils.generate_uuid(), + vnf_id=vnf_dict['id'], + key=key, value=val) + context.session.add(vnf_attr_model) + + except Exception as exc: + with excutils.save_and_reraise_exception(): + LOG.error("Error in updating tables {}".format(str(exc))) + # Roll back modified/added vnf attributes + for key, val in modified_attributes.items(): + vnf_attr_model = (context.session.query( + vnfm_db.VNFAttribute). + filter_by(vnf_id=vnf_dict['id']). + filter_by(key=key).first()) + if vnf_attr_model: + vnf_attr_model.update({'value': val}) + + for key, val in added_attributes.items(): + vnf_attr_model = (context.session.query( + vnfm_db.VNFAttribute). + filter_by(vnf_id=vnf_dict['id']). + filter_by(key=key).first()) + if vnf_attr_model: + vnf_attr_model.delete() + + @log.log + def _build_instantiated_vnf_info(self, context, vnf_instance, + instantiate_vnf_req=None): + try: + # if instantiate_vnf_req is not present, create from vnf_instance + if not instantiate_vnf_req: + instantiate_vnf_req = objects.InstantiateVnfRequest.\ + from_vnf_instance(vnf_instance) + + # update instantiated vnf info based on created stack resources + if hasattr(vnf_instance.instantiated_vnf_info, 'instance_id'): + + # get final vnfd_dict + vnfd_dict = vnflcm_utils._get_vnfd_dict(context, + vnf_instance.vnfd_id, + instantiate_vnf_req.flavour_id) + + # get vim_connection info from request + vim_info = vnflcm_utils._get_vim(context, + instantiate_vnf_req.vim_connection_info) + + vim_connection_info = objects.VimConnectionInfo.\ + obj_from_primitive(vim_info, context) + + vnflcm_utils._build_instantiated_vnf_info(vnfd_dict, + instantiate_vnf_req, vnf_instance, + vim_id=vim_connection_info.vim_id) + + if vnf_instance.instantiated_vnf_info.instance_id: + self.vnf_manager.invoke(vim_connection_info.vim_type, + 'post_vnf_instantiation', context=context, + vnf_instance=vnf_instance, + vim_connection_info=vim_connection_info) + + except Exception as ex: + try: + vnf_instance.instantiated_vnf_info.reinitialize() + vnf_instance.instantiated_vnf_info.save() + finally: + error_msg = "Failed to build instantiation information \ + for vnf {} because {}".\ + format(vnf_instance.id, encodeutils. + exception_to_unicode(ex)) + LOG.error("_build_instantiated_vnf_info error {}". + format(error_msg)) + raise exceptions.TackerException(message=error_msg) + + @log.log + def _update_instantiated_vnf_info( + self, context, vnf_instance, heal_vnf_request): + try: + vim_info = vnflcm_utils._get_vim(context, + vnf_instance.vim_connection_info) + vim_connection_info = \ + objects.VimConnectionInfo.obj_from_primitive( + vim_info, context) + + self.vnf_manager.invoke( + vim_connection_info.vim_type, 'post_heal_vnf', + context=context, vnf_instance=vnf_instance, + vim_connection_info=vim_connection_info, + heal_vnf_request=heal_vnf_request) + + except Exception as exp: + error_msg = \ + "Failed to update instantiation information for vnf {}: {}".\ + format(vnf_instance.id, encodeutils.exception_to_unicode(exp)) + LOG.error("_update_instantiated_vnf_info error {}". + format(error_msg)) + raise exceptions.TackerException(message=error_msg) + + @log.log + def _add_additional_vnf_info(self, context, vnf_instance): + '''this method adds misc info to 'vnf' table''' + try: + if hasattr(vnf_instance.instantiated_vnf_info, 'instance_id'): + if vnf_instance.instantiated_vnf_info.instance_id: + # add instance_id info + instance_id = vnf_instance.instantiated_vnf_info.\ + instance_id + with context.session.begin(subtransactions=True): + updated_values = {'instance_id': instance_id} + context.session.query(vnfm_db.VNF).filter_by( + id=vnf_instance.id).update(updated_values) + + except Exception as ex: + # with excutils.save_and_reraise_exception(): + error_msg = "Failed to add additional vnf info to vnf {}. Details -\ + {}".format( + vnf_instance.id, str(ex)) + LOG.error("_add_additional_vnf_info error {}".format(error_msg)) + raise exceptions.TackerException(message=error_msg) + @periodic_task.periodic_task(spacing=CONF.vnf_package_delete_interval) def _run_cleanup_vnf_packages(self, context): """Delete orphan extracted csar zip and files from extracted path @@ -490,7 +708,6 @@ class Conductor(manager.Manager): fields.LcmOccsOperationType.INSTANTIATE) operation_state = kwargs.get('operation_state', fields.LcmOccsOperationState.PROCESSING) - evacuate_end_list = kwargs.get('evacuate_end_list', None) is_automatic_invocation = \ kwargs.get('is_automatic_invocation', False) error = kwargs.get('error', None) @@ -546,8 +763,7 @@ class Conductor(manager.Manager): operation_state == fields.LcmOccsOperationState.FAILED_TEMP): affected_resources = vnflcm_utils._get_affected_resources( old_vnf_instance=old_vnf_instance, - new_vnf_instance=vnf_instance, - extra_list=evacuate_end_list) + new_vnf_instance=vnf_instance) affected_resources_snake_case = \ utils.convert_camelcase_to_snakecase(affected_resources) resource_change_obj = \ @@ -666,6 +882,7 @@ class Conductor(manager.Manager): self, context, vnf_instance, + vnf_dict, instantiate_vnf, vnf_lcm_op_occs_id): @@ -679,30 +896,23 @@ class Conductor(manager.Manager): request_obj=instantiate_vnf ) - # Check if vnf is already instantiated. - vnf_instance = objects.VnfInstance.get_by_id(context, - vnf_instance.id) - if vnf_instance.instantiation_state == \ - fields.VnfInstanceState.INSTANTIATED: - LOG.error("Vnf instance %(id)s is already in %(state)s state.", - {"id": vnf_instance.id, - "state": vnf_instance.instantiation_state}) - return + # change vnf_status + if vnf_dict['status'] == 'INACTIVE': + vnf_dict['status'] = 'PENDING_CREATE' + self._change_vnf_status(context, vnf_instance.id, + _INACTIVE_STATUS, 'PENDING_CREATE') self.vnflcm_driver.instantiate_vnf(context, vnf_instance, - instantiate_vnf) + vnf_dict, instantiate_vnf) + self._build_instantiated_vnf_info(context, + vnf_instance, + instantiate_vnf_req=instantiate_vnf) - vnf_package_vnfd = objects.VnfPackageVnfd.get_by_id( - context, vnf_instance.vnfd_id) - vnf_package = objects.VnfPackage.get_by_id( - context, vnf_package_vnfd.package_uuid, - expected_attrs=['vnfd']) - try: - self._update_package_usage_state(context, vnf_package) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.error("Failed to update usage_state of vnf package %s", - vnf_package.id) + self._update_vnf_attributes(context, vnf_dict, + _PENDING_STATUS, _ACTIVE_STATUS) + self.vnflcm_driver._vnf_instance_update(context, vnf_instance, + instantiation_state=fields.VnfInstanceState. + INSTANTIATED, task_state=None) # Update vnf_lcm_op_occs table and send notification "COMPLETED" self._send_lcm_op_occ_notification( @@ -715,6 +925,12 @@ class Conductor(manager.Manager): ) except Exception as ex: + self._change_vnf_status(context, vnf_instance.id, + _PENDING_STATUS, 'ERROR') + + self._build_instantiated_vnf_info(context, vnf_instance, + instantiate_vnf) + # Update vnf_lcm_op_occs table and send notification "FAILED_TEMP" self._send_lcm_op_occ_notification( context=context, @@ -730,17 +946,6 @@ class Conductor(manager.Manager): def terminate(self, context, vnf_lcm_op_occs_id, vnf_instance, terminate_vnf_req, vnf_dict): try: - # Check if vnf is in instantiated state. - vnf_instance = objects.VnfInstance.get_by_id(context, - vnf_instance.id) - if vnf_instance.instantiation_state == \ - fields.VnfInstanceState.NOT_INSTANTIATED: - LOG.error("Terminate action cannot be performed on vnf %(id)s " - "which is in %(state)s state.", - {"id": vnf_instance.id, - "state": vnf_instance.instantiation_state}) - return - old_vnf_instance = copy.deepcopy(vnf_instance) # Update vnf_lcm_op_occs table and send notification "PROCESSING" @@ -753,22 +958,18 @@ class Conductor(manager.Manager): operation=fields.LcmOccsOperationType.TERMINATE ) + self._change_vnf_status(context, vnf_instance.id, + _ACTIVE_STATUS, 'PENDING_TERMINATE') + self.vnflcm_driver.terminate_vnf(context, vnf_instance, - terminate_vnf_req, - vnf_lcm_op_occs_id) - - vnf_package_vnfd = \ - objects.VnfPackageVnfd.get_by_id(context, vnf_instance.vnfd_id) - vnf_package = \ - objects.VnfPackage.get_by_id(context, - vnf_package_vnfd.package_uuid, - expected_attrs=['vnfd']) - try: - self._update_package_usage_state(context, vnf_package) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.error("Failed to update usage_state of vnf package %s", - vnf_package.id) + terminate_vnf_req) + self._change_vnf_status(context, vnf_instance.id, + _PENDING_STATUS, 'INACTIVE') + + self.vnflcm_driver._vnf_instance_update(context, vnf_instance, + vim_connection_info=[], task_state=None, + instantiation_state=fields.VnfInstanceState.NOT_INSTANTIATED) + vnf_instance.instantiated_vnf_info.reinitialize() # Update vnf_lcm_op_occs table and send notification "COMPLETED" self._send_lcm_op_occ_notification( @@ -782,6 +983,10 @@ class Conductor(manager.Manager): ) except Exception as exc: + # set vnf_status to error + self._change_vnf_status(context, vnf_instance.id, + _PENDING_STATUS, 'ERROR') + # Update vnf_lcm_op_occs table and send notification "FAILED_TEMP" self._send_lcm_op_occ_notification( context=context, @@ -803,19 +1008,6 @@ class Conductor(manager.Manager): vnf_lcm_op_occs_id): try: - evacuate_end_list = [] - - # Check if vnf is in instantiated state. - vnf_instance = objects.VnfInstance.get_by_id(context, - vnf_instance.id) - if vnf_instance.instantiation_state == \ - fields.VnfInstanceState.NOT_INSTANTIATED: - LOG.error("Heal action cannot be performed on vnf %(id)s " - "which is in %(state)s state.", - {"id": vnf_instance.id, - "state": vnf_instance.instantiation_state}) - return - old_vnf_instance = copy.deepcopy(vnf_instance) # Update vnf_lcm_op_occs table and send notification "PROCESSING" @@ -828,15 +1020,25 @@ class Conductor(manager.Manager): operation=fields.LcmOccsOperationType.HEAL ) - heal_result = \ - self.vnflcm_driver.heal_vnf(context, vnf_instance, vnf_dict, - heal_vnf_request, - vnf_lcm_op_occs_id) + # update vnf status to PENDING_HEAL + self._change_vnf_status(context, vnf_instance.id, + _ACTIVE_STATUS, constants.PENDING_HEAL) + self.vnflcm_driver.heal_vnf(context, vnf_instance, + vnf_dict, heal_vnf_request) + self._update_instantiated_vnf_info(context, vnf_instance, + heal_vnf_request) - # update vnf_lcm_op_occs and send notification "COMPLETED" - if heal_result: - evacuate_end_list = heal_result.get('evacuate_end_list') + # update instance_in in vnf_table + self._add_additional_vnf_info(context, vnf_instance) + # update vnf status to ACTIVE + self._change_vnf_status(context, vnf_instance.id, + _PENDING_STATUS, constants.ACTIVE) + # during .save() ,instantiated_vnf_info is also saved to DB + self.vnflcm_driver._vnf_instance_update(context, vnf_instance, + task_state=None) + + # update vnf_lcm_op_occs and send notification "COMPLETED" self._send_lcm_op_occ_notification( context=context, vnf_lcm_op_occs_id=vnf_lcm_op_occs_id, @@ -844,10 +1046,16 @@ class Conductor(manager.Manager): vnf_instance=vnf_instance, request_obj=heal_vnf_request, operation=fields.LcmOccsOperationType.HEAL, - operation_state=fields.LcmOccsOperationState.COMPLETED, - evacuate_end_list=evacuate_end_list + operation_state=fields.LcmOccsOperationState.COMPLETED ) except Exception as ex: + # update vnf_status to 'ERROR' and create event with 'ERROR' status + self._change_vnf_status(context, vnf_instance, + _PENDING_STATUS, constants.ERROR, str(ex)) + + # call _update_instantiated_vnf_info for notification + self._update_instantiated_vnf_info(context, vnf_instance, + heal_vnf_request) # update vnf_lcm_op_occs and send notification "FAILED_TEMP" self._send_lcm_op_occ_notification( @@ -858,7 +1066,6 @@ class Conductor(manager.Manager): request_obj=heal_vnf_request, operation=fields.LcmOccsOperationType.HEAL, operation_state=fields.LcmOccsOperationState.FAILED_TEMP, - evacuate_end_list=evacuate_end_list, error=str(ex) ) diff --git a/tacker/conf/vnf_package.py b/tacker/conf/vnf_package.py index 40c8e11..60f0718 100644 --- a/tacker/conf/vnf_package.py +++ b/tacker/conf/vnf_package.py @@ -56,7 +56,28 @@ Possible values: Related options: * None -"""))] +""")), + + cfg.ListOpt('get_top_list', + default=['tosca_definitions_version', + 'description', 'metadata'], + help=_("List of items to get from top-vnfd")), + + cfg.ListOpt('exclude_node', + default=['VNF'], + help=_("Exclude node from node_template")), + + cfg.ListOpt('get_lower_list', + default=['tosca.nodes.nfv.VNF', 'tosca.nodes.nfv.VDU.Tacker'], + help=_("List of types to get from lower-vnfd")), + + cfg.ListOpt('del_input_list', + default=['descriptor_id', 'descriptor_version' + 'provider', 'product_name', 'software_version', + 'vnfm_info', 'flavour_id', 'flavour_description'], + help=_("List of del inputs from lower-vnfd")), + +] vnf_package_group = cfg.OptGroup('vnf_package', title='vnf_package options', diff --git a/tacker/db/db_sqlalchemy/models.py b/tacker/db/db_sqlalchemy/models.py index 5d127bf..aedfad2 100644 --- a/tacker/db/db_sqlalchemy/models.py +++ b/tacker/db/db_sqlalchemy/models.py @@ -201,8 +201,8 @@ class VnfInstance(model_base.BASE, models.SoftDeleteMixin, task_state = sa.Column(sa.String(255), nullable=True) vim_connection_info = sa.Column(sa.JSON(), nullable=True) tenant_id = sa.Column('tenant_id', sa.String(length=64), nullable=False) - vnf_metadata = sa.Column(sa.JSON(), nullable=True) vnf_pkg_id = sa.Column(types.Uuid, nullable=False) + vnf_metadata = sa.Column(sa.JSON(), nullable=True) class VnfInstantiatedInfo(model_base.BASE, models.SoftDeleteMixin, diff --git a/tacker/objects/__init__.py b/tacker/objects/__init__.py index 2a271a0..2b2fd38 100644 --- a/tacker/objects/__init__.py +++ b/tacker/objects/__init__.py @@ -34,6 +34,7 @@ def register_all(): __import__('tacker.objects.vim_connection') __import__('tacker.objects.instantiate_vnf_req') __import__('tacker.objects.vnf_resources') + __import__('tacker.objects.vnfd') __import__('tacker.objects.vnf_lcm_op_occs') __import__('tacker.objects.terminate_vnf_req') __import__('tacker.objects.vnf_artifact') diff --git a/tacker/objects/terminate_vnf_req.py b/tacker/objects/terminate_vnf_req.py index 75d1a51..d539faa 100644 --- a/tacker/objects/terminate_vnf_req.py +++ b/tacker/objects/terminate_vnf_req.py @@ -51,6 +51,8 @@ class TerminateVnfRequest(base.TackerObject, base.TackerPersistentObject): termination_type = data_dict.get('termination_type') graceful_termination_timeout = \ data_dict.get('graceful_termination_timeout', 0) + additional_params = data_dict.get('additional_params', {}) return cls(termination_type=termination_type, - graceful_termination_timeout=graceful_termination_timeout) + graceful_termination_timeout=graceful_termination_timeout, + additional_params=additional_params) diff --git a/tacker/objects/vnf_instance.py b/tacker/objects/vnf_instance.py index 16a2f17..0c09f92 100644 --- a/tacker/objects/vnf_instance.py +++ b/tacker/objects/vnf_instance.py @@ -12,11 +12,13 @@ # 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 copy from oslo_log import log as logging from oslo_utils import timeutils from oslo_utils import uuidutils from oslo_versionedobjects import base as ovoo_base +from sqlalchemy import exc from sqlalchemy.orm import joinedload from sqlalchemy_filters import apply_filters @@ -135,6 +137,18 @@ def _make_vnf_instance_list(context, vnf_instance_list, db_vnf_instance_list, return vnf_instance_list +# decorator to catch DBAccess exception +def _wrap_object_error(method): + + def wrapper(*args, **kwargs): + try: + method(*args, **kwargs) + except exc.SQLAlchemyError: + raise exceptions.DBAccessError + + return wrapper + + @base.TackerObjectRegistry.register class VnfInstance(base.TackerObject, base.TackerPersistentObject, base.TackerObjectDictCompat): @@ -157,10 +171,10 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, 'vim_connection_info': fields.ListOfObjectsField( 'VimConnectionInfo', nullable=True, default=[]), 'tenant_id': fields.StringField(nullable=False), - 'instantiated_vnf_info': fields.ObjectField('InstantiatedVnfInfo', - nullable=True, default=None), 'vnf_pkg_id': fields.StringField(nullable=False), - 'vnf_metadata': fields.DictOfStringsField(nullable=True, default={}) + 'vnf_metadata': fields.DictOfStringsField(nullable=True, default={}), + 'instantiated_vnf_info': fields.ObjectField('InstantiatedVnfInfo', + nullable=True, default=None) } ALL_ATTRIBUTES = { @@ -239,12 +253,20 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, updates['id'] = uuidutils.generate_uuid() self.id = updates['id'] + # add default vnf_instance_name if not specified + # format: 'vnf' + + if 'vnf_instance_name' not in updates or \ + not updates.get("vnf_instance_name"): + updates['vnf_instance_name'] = 'vnf-' + self.id + self.vnf_instance_name = updates['vnf_instance_name'] + db_vnf_instance = _vnf_instance_create(self._context, updates) expected_attrs = ["instantiated_vnf_info"] self._from_db_object(self._context, self, db_vnf_instance, expected_attrs=expected_attrs) @base.remotable + @_wrap_object_error def save(self): context = self._context @@ -263,6 +285,10 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, field_list = getattr(self, field) updates[field] = [obj.obj_to_primitive() for obj in field_list] elif field in changes: + if (field == 'vnf_instance_name' and + not self[field]): + self.vnf_instance_name = 'vnf-' + self.id + updates[field] = self[field] expected_attrs = ["instantiated_vnf_info"] @@ -277,6 +303,15 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, self.instantiated_vnf_info.save() @base.remotable + @_wrap_object_error + def update_metadata(self, data): + _metadata = copy.deepcopy(self['vnf_metadata']) + _metadata.update(data) + self['vnf_metadata'] = _metadata + self.save() + + @base.remotable + @_wrap_object_error def destroy(self, context): if not self.obj_attr_is_set('id'): raise exceptions.ObjectActionError(action='destroy', @@ -293,8 +328,8 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject, 'vnf_provider': self.vnf_provider, 'vnf_product_name': self.vnf_product_name, 'vnf_software_version': self.vnf_software_version, - 'vnf_pkg_id': self.vnf_pkg_id, 'vnfd_version': self.vnfd_version, + 'vnf_pkg_id': self.vnf_pkg_id, 'vnf_metadata': self.vnf_metadata} if (self.instantiation_state == fields.VnfInstanceState.INSTANTIATED diff --git a/tacker/objects/vnf_instantiated_info.py b/tacker/objects/vnf_instantiated_info.py index 70cd3a1..4b0e664 100644 --- a/tacker/objects/vnf_instantiated_info.py +++ b/tacker/objects/vnf_instantiated_info.py @@ -14,6 +14,7 @@ # under the License. from oslo_log import log as logging +from oslo_utils import timeutils from tacker.common import exceptions from tacker.common import utils @@ -28,6 +29,17 @@ LOG = logging.getLogger(__name__) @db_api.context_manager.writer +def _destroy_instantiated_vnf_info(context, uuid): + now = timeutils.utcnow() + updated_values = {'deleted': True, + 'deleted_at': now + } + api.model_query(context, models.VnfInstantiatedInfo). \ + filter_by(vnf_instance_id=uuid). \ + update(updated_values, synchronize_session=False) + + +@db_api.context_manager.writer def _instantiate_vnf_info_update(context, vnf_instance_id, values): vnf_info = api.model_query(context, models.VnfInstantiatedInfo). \ filter_by(vnf_instance_id=vnf_instance_id).first() @@ -375,6 +387,14 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat, self.vnf_state = fields.VnfOperationalStateType.STOPPED self.vnfc_info = [] + @base.remotable + def destroy(self, context): + if not self.obj_attr_is_set('vnf_instance_id'): + raise exceptions.ObjectActionError(action='destroy', + reason='no uuid') + + _destroy_instantiated_vnf_info(context, self.vnf_instance_id) + @base.TackerObjectRegistry.register class VnfExtCpInfo(base.TackerObject, base.TackerObjectDictCompat, diff --git a/tacker/objects/vnf_package.py b/tacker/objects/vnf_package.py index 4f09597..451d7f8 100644 --- a/tacker/objects/vnf_package.py +++ b/tacker/objects/vnf_package.py @@ -502,9 +502,6 @@ class VnfPackage(base.TackerObject, base.TackerPersistentObject, @base.remotable def create(self): - if self.obj_attr_is_set('id'): - raise exceptions.ObjectActionError(action='create', - reason=_('already created')) updates = self.obj_get_changes() if 'id' not in updates: diff --git a/tacker/objects/vnf_package_vnfd.py b/tacker/objects/vnf_package_vnfd.py index 9c942c6..6ce8b78 100644 --- a/tacker/objects/vnf_package_vnfd.py +++ b/tacker/objects/vnf_package_vnfd.py @@ -59,6 +59,29 @@ def _vnf_package_vnfd_get_by_id(context, vnfd_id): return result +@db_api.context_manager.reader +def _get_vnf_package_vnfd_by_vnfid(context, vnfpkgid): + + sql = ("select" + " t1.vnfd_id," + " t1.vnf_provider," + " t1.vnf_product_name," + " t1.vnf_software_version," + " t1.vnfd_version," + " t2.name" + " from " + " vnf_package_vnfd t1," + " vnf t2 " + " where" + " t1.vnfd_id=t2.vnfd_id" + " and" + " t2.id= :vnfpkgid") + + result = context.session.execute(sql, {'vnfpkgid': vnfpkgid}) + for line in result: + return line + + @base.TackerObjectRegistry.register class VnfPackageVnfd(base.TackerObject, base.TackerObjectDictCompat, base.TackerPersistentObject): @@ -104,6 +127,10 @@ class VnfPackageVnfd(base.TackerObject, base.TackerObjectDictCompat, self._context, updates) self._from_db_object(self._context, self, db_vnf_package_vnfd) + @base.remotable_classmethod + def get_vnf_package_vnfd_by_vnfid(self, context, vnfid): + return _get_vnf_package_vnfd_by_vnfid(context, vnfid) + @classmethod def obj_from_db_obj(cls, context, db_obj): return cls._from_db_object(context, cls(), db_obj) diff --git a/tacker/objects/vnfd.py b/tacker/objects/vnfd.py new file mode 100644 index 0000000..a1ca0a6 --- /dev/null +++ b/tacker/objects/vnfd.py @@ -0,0 +1,132 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_utils import timeutils + +from oslo_log import log as logging + +from tacker.db import api as db_api +from tacker.db.db_sqlalchemy import api +from tacker.db.db_sqlalchemy import models +from tacker.db.vnfm import vnfm_db +from tacker.objects import base +from tacker.objects import fields + +LOG = logging.getLogger(__name__) + + +@db_api.context_manager.writer +def _vnfd_create(context, values): + vnfd = vnfm_db.VNFD() + + vnfd.update(values) + vnfd.save(context.session) + + return vnfd + + +@db_api.context_manager.reader +def _get_vnfd_id(context, id): + try: + vnf_package_vnfd = \ + api.model_query(context, models.VnfPackageVnfd).\ + filter_by(package_uuid=id).first() + except Exception: + LOG.info("select vnf_package_vnfd failed") + if vnf_package_vnfd: + return vnf_package_vnfd.vnfd_id + else: + return None + + +@db_api.context_manager.reader +def _check_vnfd(context, id): + try: + vnfd = api.model_query(context, vnfm_db.VNFD).filter_by(id=id).first() + except Exception: + LOG.info("select vnfd failed") + if vnfd: + return "TRUE" + else: + return "FALSE" + + +@db_api.context_manager.writer +def _vnfd_delete(context, id): + try: + api.model_query(context, vnfm_db.VNFD).filter_by(id=id).delete() + except Exception: + LOG.info("delete vnfd failed") + + +@db_api.context_manager.writer +def _vnfd_destroy(context, id): + now = timeutils.utcnow() + updated_values = {'deleted_at': now} + try: + api.model_query(context, vnfm_db.VNFD).\ + filter_by(id=id).\ + update(updated_values, synchronize_session=False) + except Exception: + LOG.info("destroy vnfdfailed") + + +@base.TackerObjectRegistry.register +class Vnfd(base.TackerObject, base.TackerObjectDictCompat, + base.TackerPersistentObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.UUIDField(nullable=False), + 'tenant_id': fields.UUIDField(nullable=False), + 'name': fields.StringField(nullable=False), + 'description': fields.StringField(nullable=True), + 'mgmt_driver': fields.StringField(nullable=True), + 'deleted_at': fields.DateTimeField(nullable=True), + } + + @staticmethod + def _from_db_object(context, vnfd, db_vnfd): + + for key in vnfd.fields: + if db_vnfd.get(key): + setattr(vnfd, key, db_vnfd[key]) + + vnfd._context = context + vnfd.obj_reset_changes() + + return vnfd + + @base.remotable + def create(self): + updates = self.obj_get_changes() + db_vnfd = _vnfd_create( + self._context, updates) + self._from_db_object(self._context, self, db_vnfd) + + @classmethod + def obj_from_db_obj(cls, context, db_obj): + return cls._from_db_object(context, cls(), db_obj) + + @base.remotable + def destroy(self, id): + _vnfd_destroy(self._context, id) + + @base.remotable + def delete(self, id): + _vnfd_delete(self._context, id) + + @base.remotable + def check_vnfd(self, id): + return _check_vnfd(self._context, id) diff --git a/tacker/objects/vnfd_attribute.py b/tacker/objects/vnfd_attribute.py new file mode 100644 index 0000000..b89f6cf --- /dev/null +++ b/tacker/objects/vnfd_attribute.py @@ -0,0 +1,124 @@ +# Copyright 2019 NTT DATA. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging + +from tacker.db import api as db_api +from tacker.db.db_sqlalchemy import api +from tacker.db.db_sqlalchemy import models +from tacker.db.vnfm import vnfm_db +from tacker.objects import base +from tacker.objects import fields + +LOG = logging.getLogger(__name__) + + +@db_api.context_manager.writer +def _vnfd_attribute_create(context, values): + vnfd_attribute = vnfm_db.VNFDAttribute() + + vnfd_attribute.update(values) + vnfd_attribute.save(context.session) + + return vnfd_attribute + + +@db_api.context_manager.reader +def _get_vnfd_id(context, id): + try: + vnf_package_vnfd = \ + api.model_query(context, models.VnfPackageVnfd).\ + filter_by(package_uuid=id).first() + except Exception: + LOG.info("select vnfd_attribute failed") + if vnf_package_vnfd: + return vnf_package_vnfd.vnfd_id + else: + return None + + +@db_api.context_manager.reader +def _check_vnfd_attribute(context, id): + try: + vnfd_attribute = \ + api.model_query(context, vnfm_db.VNFDAttribute).\ + filter_by(vnfd_id=id).first() + except Exception: + LOG.info("select vnfd_attribute failed") + if vnfd_attribute: + return "TRUE" + else: + return "FALSE" + + +@db_api.context_manager.writer +def _vnfd_attribute_delete(context, id): + try: + api.model_query(context, vnfm_db.VNFDAttribute).\ + filter_by(vnfd_id=id).delete() + except Exception: + LOG.info("delete vnfd_attribute failed") + + +@base.TackerObjectRegistry.register +class VnfdAttribute(base.TackerObject, base.TackerObjectDictCompat, + base.TackerPersistentObject): + + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.UUIDField(nullable=False), + 'vnfd_id': fields.UUIDField(nullable=False), + 'key': fields.StringField(nullable=True), + 'value': fields.StringField(nullable=True), + } + + @staticmethod + def _from_db_object(context, vnfd_attribute, db_vnfd_attribute): + + for key in vnfd_attribute.fields: + + if db_vnfd_attribute.get(key): + setattr(vnfd_attribute, key, db_vnfd_attribute[key]) + + vnfd_attribute._context = context + vnfd_attribute.obj_reset_changes() + + return vnfd_attribute + + @base.remotable + def create(self): + updates = self.obj_get_changes() + db_vnfd_attribute = _vnfd_attribute_create( + self._context, updates) + self._from_db_object(self._context, self, db_vnfd_attribute) + + @classmethod + def obj_from_db_obj(cls, context, db_obj): + return cls._from_db_object(context, cls(), db_obj) + + @base.remotable + def destroy(self, id): + vnfd_attribute_id = _get_vnfd_id(self._context, id) + if vnfd_attribute_id: + _vnfd_attribute_delete(self._context, vnfd_attribute_id) + + @base.remotable + def delete(self, id): + _vnfd_attribute_delete(self._context, id) + + @base.remotable + def check_vnfd_attribute(self, id): + return _check_vnfd_attribute(self._context, id) diff --git a/tacker/tests/unit/conductor/test_conductor_server.py b/tacker/tests/unit/conductor/test_conductor_server.py index 43c848f..234077a 100644 --- a/tacker/tests/unit/conductor/test_conductor_server.py +++ b/tacker/tests/unit/conductor/test_conductor_server.py @@ -14,31 +14,29 @@ # limitations under the License. import base64 +import fixtures import json import os +import requests import shutil +import six.moves.urllib.error as urlerr import sys -from unittest import mock - -import fixtures +import tacker.conf +import yaml from glance_store import exceptions as store_exceptions from oslo_config import cfg -import requests from six.moves import urllib -import six.moves.urllib.error as urlerr -import yaml - from tacker import auth from tacker.common import coordination from tacker.common import csar_utils from tacker.common import exceptions from tacker.conductor import conductor_server -import tacker.conf from tacker import context from tacker.glance_store import store as glance_store from tacker import objects from tacker.objects import fields +from tacker.plugins.common import constants from tacker.tests.unit import base as unit_base from tacker.tests.unit.conductor import fakes from tacker.tests.unit.db.base import SqlTestCase @@ -46,9 +44,14 @@ from tacker.tests.unit.db import utils as db_utils from tacker.tests.unit.objects import fakes as fake_obj from tacker.tests.unit.vnflcm import fakes as vnflcm_fakes from tacker.tests.unit.vnfm.infra_drivers.openstack.fixture_data import client +from tacker.tests.unit.vnfm.infra_drivers.openstack.fixture_data import \ + fixture_data_utils as fd_utils import tacker.tests.unit.vnfm.test_nfvo_client as nfvo_client from tacker.tests import utils from tacker.tests import uuidsentinel +import unittest +from unittest import mock + CONF = tacker.conf.CONF @@ -254,14 +257,16 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): return vnf_pack_vnfd_obj + @mock.patch('tacker.conductor.conductor_server.Conductor' + '._update_vnf_attributes') + @mock.patch('tacker.conductor.conductor_server.Conductor' + '._change_vnf_status') @mock.patch.object(objects.VnfLcmOpOcc, "save") @mock.patch.object(coordination.Coordinator, 'get_lock') - @mock.patch.object(objects.VnfPackage, 'is_package_in_use') @mock.patch.object(objects.VnfLcmOpOcc, "get_by_id") def test_instantiate_vnf_instance(self, mock_vnf_by_id, - mock_package_in_use, - mock_get_lock, - mock_save): + mock_get_lock, mock_save, mock_change_vnf_status, + mock_update_vnf_attributes): lcm_op_occs_data = fakes.get_lcm_op_occs_data() mock_vnf_by_id.return_value = \ objects.VnfLcmOpOcc(context=self.context, @@ -270,19 +275,23 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): vnf_package_vnfd = self._create_and_upload_vnf_package() vnf_instance_data = fake_obj.get_vnf_instance_data( vnf_package_vnfd.vnfd_id) - mock_package_in_use.return_value = False vnf_instance = objects.VnfInstance(context=self.context, **vnf_instance_data) vnf_instance.create() instantiate_vnf_req = vnflcm_fakes.get_instantiate_vnf_request_obj() vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id - self.conductor.instantiate(self.context, vnf_instance, - instantiate_vnf_req, - vnf_lcm_op_occs_id) + vnf_dict = {"status": "ACTIVE"} + self.conductor.instantiate(self.context, vnf_instance, vnf_dict, + instantiate_vnf_req, vnf_lcm_op_occs_id) self.vnflcm_driver.instantiate_vnf.assert_called_once_with( - self.context, mock.ANY, instantiate_vnf_req) - mock_package_in_use.assert_called_once() - + self.context, mock.ANY, vnf_dict, instantiate_vnf_req) + self.vnflcm_driver._vnf_instance_update.assert_called_once() + mock_change_vnf_status. \ + assert_called_once_with(self.context, vnf_instance.id, + mock.ANY, 'PENDING_CREATE') + mock_update_vnf_attributes.assert_called_once() + + @unittest.skip("Such test is no longer feasible.") @mock.patch.object(objects.VnfLcmOpOcc, "save") @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') @@ -316,6 +325,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): {'id': vnf_instance.id, 'state': fields.VnfInstanceState.INSTANTIATED}) + @unittest.skip("Such test is no longer feasible.") @mock.patch.object(objects.VnfLcmOpOcc, "save") @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfPackage, 'is_package_in_use') @@ -351,9 +361,12 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): self.context, mock.ANY, instantiate_vnf_req) mock_vnf_package_in_use.assert_called_once() + @mock.patch('tacker.conductor.conductor_server.Conductor' + '._update_vnf_attributes') + @mock.patch('tacker.conductor.conductor_server.Conductor' + '._change_vnf_status') @mock.patch.object(objects.VnfLcmOpOcc, "save") @mock.patch.object(coordination.Coordinator, 'get_lock') - @mock.patch.object(objects.VnfPackage, 'is_package_in_use') @mock.patch.object(objects.LccnSubscriptionRequest, 'vnf_lcm_subscriptions_get') @mock.patch('tacker.conductor.conductor_server.LOG') @@ -362,7 +375,8 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): def test_instantiate_vnf_instance_failed_with_exception( self, mock_res, mock_vnf_by_id, mock_log, mock_vnf_lcm_subscriptions_get, - mock_is_package_in_use, mock_get_lock, mock_save): + mock_get_lock, mock_save, mock_change_vnf_status, + mock_update_vnf_attributes): lcm_op_occs_data = fakes.get_lcm_op_occs_data() mock_vnf_by_id.return_value = \ objects.VnfLcmOpOcc(context=self.context, @@ -376,38 +390,32 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): vnf_instance.create() instantiate_vnf_req = vnflcm_fakes.get_instantiate_vnf_request_obj() vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + vnf_dict = {"status": "ACTIVE"} m_vnf_lcm_subscriptions = \ [mock.MagicMock(**fakes.get_vnf_lcm_subscriptions())] mock_vnf_lcm_subscriptions_get.return_value = \ m_vnf_lcm_subscriptions - mock_is_package_in_use.side_effect = Exception + mock_update_vnf_attributes.side_effect = Exception mock_res.return_value = {} - self.conductor.instantiate(self.context, vnf_instance, - instantiate_vnf_req, - vnf_lcm_op_occs_id) + self.conductor.instantiate(self.context, vnf_instance, vnf_dict, + instantiate_vnf_req, vnf_lcm_op_occs_id) self.vnflcm_driver.instantiate_vnf.assert_called_once_with( - self.context, mock.ANY, instantiate_vnf_req) - mock_is_package_in_use.assert_called_once() - expected_log = 'Failed to update usage_state of vnf package %s' - mock_log.error.assert_called_once_with(expected_log, - vnf_package_vnfd.package_uuid) - - @mock.patch('tacker.conductor.conductor_server.Conductor.' - '_send_lcm_op_occ_notification') + self.context, vnf_instance, vnf_dict, instantiate_vnf_req) + mock_change_vnf_status.assert_called_with(self.context, + vnf_instance.id, mock.ANY, 'ERROR') + mock_update_vnf_attributes.assert_called_once() + + @mock.patch('tacker.conductor.conductor_server.Conductor' + '._change_vnf_status') + @mock.patch('tacker.conductor.conductor_server.Conductor' + '._send_lcm_op_occ_notification') @mock.patch.object(coordination.Coordinator, 'get_lock') - @mock.patch.object(objects.VnfPackage, 'is_package_in_use') - def test_terminate_vnf_instance(self, mock_package_in_use, - mock_get_lock, - mock_send_notification): - vnf_package_vnfd = self._create_and_upload_vnf_package() - vnf_instance_data = fake_obj.get_vnf_instance_data( - vnf_package_vnfd.vnfd_id) - mock_package_in_use.return_value = True - vnf_instance_data['instantiation_state'] =\ - fields.VnfInstanceState.INSTANTIATED - vnf_instance = objects.VnfInstance(context=self.context, - **vnf_instance_data) - vnf_instance.create() + def test_terminate_vnf_instance(self, mock_get_lock, + mock_send_notification, + mock_change_vnf_status): + inst_vnf_info = fd_utils.get_vnf_instantiated_info() + vnf_instance = fd_utils. \ + get_vnf_instance_object(instantiated_vnf_info=inst_vnf_info) terminate_vnf_req = objects.TerminateVnfRequest( termination_type=fields.VnfInstanceTerminationType.GRACEFUL, @@ -415,14 +423,43 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id vnf_dict = db_utils.get_dummy_vnf(instance_id=self.instance_uuid) self.conductor.terminate(self.context, vnf_lcm_op_occs_id, - vnf_instance, - terminate_vnf_req, vnf_dict) + vnf_instance, terminate_vnf_req, vnf_dict) self.vnflcm_driver.terminate_vnf.assert_called_once_with( - self.context, mock.ANY, terminate_vnf_req, - vnf_lcm_op_occs_id) - mock_package_in_use.assert_called_once() + self.context, vnf_instance, terminate_vnf_req) + self.vnflcm_driver._vnf_instance_update.assert_called_once() + self.assertEqual(mock_send_notification.call_count, 2) + self.assertEqual(mock_change_vnf_status.call_count, 2) + + @mock.patch('tacker.conductor.conductor_server.Conductor' + '._change_vnf_status') + @mock.patch('tacker.conductor.conductor_server.Conductor' + '._send_lcm_op_occ_notification') + @mock.patch.object(coordination.Coordinator, 'get_lock') + def test_terminate_vnf_instance_exception(self, mock_get_lock, + mock_send_notification, + mock_change_vnf_status): + inst_vnf_info = fd_utils.get_vnf_instantiated_info() + vnf_instance = fd_utils. \ + get_vnf_instance_object(instantiated_vnf_info=inst_vnf_info) + + mock_send_notification.side_effect = Exception + terminate_vnf_req = objects.TerminateVnfRequest( + termination_type=fields.VnfInstanceTerminationType.GRACEFUL, + additional_params={"key": "value"}) + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + vnf_dict = db_utils.get_dummy_vnf(instance_id=self.instance_uuid) + try: + self.conductor.terminate(self.context, vnf_lcm_op_occs_id, + vnf_instance, terminate_vnf_req, vnf_dict) + except Exception: + pass + self.vnflcm_driver.terminate_vnf.assert_not_called() + mock_change_vnf_status.assert_called_once_with(self.context, + vnf_instance.id, mock.ANY, 'ERROR') + self.assertEqual(mock_send_notification.call_count, 2) + @unittest.skip("Such test is no longer feasible.") @mock.patch('tacker.conductor.conductor_server.Conductor.' '_send_lcm_op_occ_notification') @mock.patch.object(coordination.Coordinator, 'get_lock') @@ -458,6 +495,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): {'id': vnf_instance.id, 'state': fields.VnfInstanceState.NOT_INSTANTIATED}) + @unittest.skip("Such test is no longer feasible.") @mock.patch('tacker.conductor.conductor_server.Conductor.' '_send_lcm_op_occ_notification') @mock.patch.object(coordination.Coordinator, 'get_lock') @@ -489,6 +527,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): vnf_lcm_op_occs_id) mock_vnf_package_is_package_in_use.assert_called_once() + @unittest.skip("Such test is no longer feasible.") @mock.patch('tacker.conductor.conductor_server.Conductor.' '_send_lcm_op_occ_notification') @mock.patch.object(coordination.Coordinator, 'get_lock') @@ -520,6 +559,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): vnf_lcm_op_occs_id) mock_vnf_package_is_package_in_use.assert_called_once() + @unittest.skip("Such test is no longer feasible.") @mock.patch('tacker.conductor.conductor_server.Conductor.' '_send_lcm_op_occ_notification') @mock.patch.object(coordination.Coordinator, 'get_lock') @@ -552,11 +592,18 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): mock_log.error.assert_called_once_with(expected_msg, vnf_package_vnfd.package_uuid) + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_add_additional_vnf_info') + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_update_instantiated_vnf_info') + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_change_vnf_status') @mock.patch.object(objects.VnfLcmOpOcc, "save") @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch.object(objects.VnfLcmOpOcc, "get_by_id") def test_heal_vnf_instance(self, mock_vnf_by_id, mock_get_lock, - mock_save): + mock_save, mock_change_vnf_status, + mock_update_insta_vnf_info, mock_add_additional_vnf_info): lcm_op_occs_data = fakes.get_lcm_op_occs_data() mock_vnf_by_id.return_value = \ objects.VnfLcmOpOcc(context=self.context, @@ -574,8 +621,50 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): vnf_dict = {"fake": "fake_dict"} vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id self.conductor.heal(self.context, vnf_instance, vnf_dict, - heal_vnf_req, vnf_lcm_op_occs_id) + heal_vnf_req, vnf_lcm_op_occs_id) + self.assertEqual(mock_change_vnf_status.call_count, 2) + mock_update_insta_vnf_info. \ + assert_called_once_with(self.context, vnf_instance, heal_vnf_req) + mock_add_additional_vnf_info. \ + assert_called_once_with(self.context, vnf_instance) + + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_send_lcm_op_occ_notification') + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_update_instantiated_vnf_info') + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_change_vnf_status') + @mock.patch('tacker.conductor.conductor_server.Conductor.' + '_add_additional_vnf_info') + @mock.patch.object(coordination.Coordinator, 'get_lock') + @mock.patch('tacker.conductor.conductor_server.LOG') + def test_heal_vnf_instance_exception(self, + mock_log, mock_get_lock, mock_add_additional_vnf_info, + mock_change_vnf_status, mock_update_insta_vnf_info, + mock_send_notification): + vnf_package_vnfd = self._create_and_upload_vnf_package() + vnf_instance_data = fake_obj.get_vnf_instance_data( + vnf_package_vnfd.vnfd_id) + vnf_instance_data['instantiation_state'] =\ + fields.VnfInstanceState.NOT_INSTANTIATED + vnf_instance = objects.VnfInstance(context=self.context, + **vnf_instance_data) + vnf_instance.create() + mock_add_additional_vnf_info.side_effect = Exception + + heal_vnf_req = objects.HealVnfRequest(cause="healing request") + vnf_dict = {"fake": "fake_dict"} + vnf_lcm_op_occs_id = uuidsentinel.vnf_lcm_op_occs_id + self.conductor.heal(self.context, vnf_instance, vnf_dict, + heal_vnf_req, vnf_lcm_op_occs_id) + mock_change_vnf_status.assert_called_with(self.context, + vnf_instance, mock.ANY, constants.ERROR, "") + mock_update_insta_vnf_info.assert_called_with(self.context, + vnf_instance, heal_vnf_req) + self.assertEqual(mock_send_notification.call_count, 2) + + @unittest.skip("Such test is no longer feasible.") @mock.patch.object(coordination.Coordinator, 'get_lock') @mock.patch('tacker.conductor.conductor_server.LOG') def test_heal_vnf_instance_already_not_instantiated(self, diff --git a/tacker/tests/unit/objects/test_vnf_package.py b/tacker/tests/unit/objects/test_vnf_package.py index 8486a25..c3af8f8 100644 --- a/tacker/tests/unit/objects/test_vnf_package.py +++ b/tacker/tests/unit/objects/test_vnf_package.py @@ -97,9 +97,15 @@ class TestVnfPackage(SqlTestCase): uuidsentinel.invalid_uuid) def test_create_with_id(self): - vnf_obj = {'id': uuidsentinel.uuid} - vnf_pack = objects.VnfPackage(context=self.context, **vnf_obj) - self.assertRaises(exceptions.ObjectActionError, vnf_pack.create) + vnfpkgm = objects.VnfPackage(context=self.context, + **fakes.vnf_package_data) + vnfpkgm['id'] = uuidsentinel.uuid + vnfpkgm.create() + self.assertTrue(vnfpkgm.id) + self.assertEqual('CREATED', vnfpkgm.onboarding_state) + self.assertEqual('NOT_IN_USE', vnfpkgm.usage_state) + self.assertEqual('DISABLED', vnfpkgm.operational_state) + self.assertEqual(0, vnfpkgm.size) def test_save(self): self.vnf_package.onboarding_state = 'ONBOARDED' diff --git a/tacker/tests/unit/vnflcm/fakes.py b/tacker/tests/unit/vnflcm/fakes.py index d327e34..868401d 100644 --- a/tacker/tests/unit/vnflcm/fakes.py +++ b/tacker/tests/unit/vnflcm/fakes.py @@ -119,8 +119,8 @@ def _model_non_instantiated_vnf_instance(**updates): 'vnf_software_version': '1.0', 'tenant_id': uuidsentinel.tenant_id, 'vnfd_id': uuidsentinel.vnfd_id, - 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, 'vnfd_version': '1.0', + 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, 'vnf_metadata': {"key": "value"}} if updates: diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 87fb7f0..5714592 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -26,6 +26,7 @@ import webob from webob import exc from tacker.api.vnflcm.v1 import controller +from tacker.api.vnflcm.v1 import sync_resource from tacker.common import exceptions from tacker.conductor.conductorrpc.vnf_lcm_rpc import VNFLcmRPCAPI from tacker import context @@ -42,9 +43,15 @@ from tacker.tests.unit import fake_request import tacker.tests.unit.nfvo.test_nfvo_plugin as nfvo_plugin from tacker.tests.unit.vnflcm import fakes from tacker.tests import uuidsentinel +import tacker.vnfm.nfvo_client as nfvo_client +from tacker.vnfm.nfvo_client import VnfPackageRequest from tacker.vnfm import vim_client +class FakeVimClient(mock.Mock): + pass + + def _get_template(name): filename = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../etc/samples/' + str(name))) @@ -231,29 +238,31 @@ class TestController(base.TestCase): res.status_int = status return res - @mock.patch.object(objects.VnfInstance, 'save') @mock.patch.object(vim_client.VimClient, "get_vim") - @mock.patch.object(objects.vnf_package.VnfPackage, 'get_by_id') - @mock.patch.object(objects.vnf_package.VnfPackage, 'save') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._update_package_usage_state') + @mock.patch.object(objects.VnfPackage, 'get_by_id') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._create_vnf') + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') def test_create_without_name_and_description( - self, mock_get_by_id_package_vnfd, - mock_vnf_instance_create, mock_package_save, - mock_get_by_id_package, mock_get_vim, - mock_save): - mock_get_vim.return_value = self.vim_info - mock_get_by_id_package_vnfd.return_value = \ - fakes.return_vnf_package_vnfd() - mock_get_by_id_package.return_value = \ - fakes.return_vnf_package_with_deployment_flavour() + self, mock_get_by_id, + mock_vnf_instance_create, + mock_get_service_plugins, + mock_private_create_vnf, + mock_vnf_package_get_by_id, + mock_update_package_usage_state, + mock_get_vim): + mock_get_by_id.return_value = fakes.return_vnf_package_vnfd() updates = {'vnfd_id': uuidsentinel.vnfd_id, 'vnf_instance_description': None, 'vnf_instance_name': None, 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, 'vnf_metadata': {"key": "value"}} - mock_vnf_instance_create.return_value =\ fakes.return_vnf_instance_model(**updates) @@ -262,7 +271,6 @@ class TestController(base.TestCase): 'metadata': {"key": "value"}} req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' - req.headers['Version'] = '2.6.1' req.method = 'POST' # Call create API @@ -271,15 +279,15 @@ class TestController(base.TestCase): self.assertEqual(http_client.CREATED, resp.status_code) updates = {'vnfInstanceDescription': None, 'vnfInstanceName': None} - expected_vnf = fakes.fake_vnf_instance_response( - instantiated_state=fields.VnfInstanceState.NOT_INSTANTIATED, - **updates) + expected_vnf = fakes.fake_vnf_instance_response(**updates) location_header = ('http://localhost/vnflcm/v1/vnf_instances/%s' % resp.json['id']) self.assertEqual(expected_vnf, resp.json) self.assertEqual(location_header, resp.headers['location']) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._create_vnf') @mock.patch.object(objects.VnfInstance, 'save') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.vnf_package.VnfPackage, 'get_by_id') @@ -290,7 +298,7 @@ class TestController(base.TestCase): self, mock_get_by_id_package_vnfd, mock_vnf_instance_create, mock_package_save, mock_get_by_id_package, mock_get_vim, - mock_save): + mock_save, mock_create_vnf): mock_get_vim.return_value = self.vim_info mock_get_by_id_package_vnfd.return_value = \ fakes.return_vnf_package_vnfd() @@ -313,13 +321,13 @@ class TestController(base.TestCase): req = fake_request.HTTPRequest.blank('/vnf_instances') req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' - req.headers['Version'] = '2.6.1' req.method = 'POST' # Call Create API resp = req.get_response(self.app) self.assertEqual(http_client.CREATED, resp.status_code) + updates = {"vnfInstanceName": "SampleVnf", "vnfInstanceDescription": "SampleVnf Description"} expected_vnf = fakes.fake_vnf_instance_response(**updates) @@ -329,49 +337,6 @@ class TestController(base.TestCase): self.assertEqual(expected_vnf, resp.json) self.assertEqual(location_header, resp.headers['location']) - @mock.patch.object(objects.VnfInstance, 'save') - @mock.patch.object(vim_client.VimClient, "get_vim") - @mock.patch.object(objects.vnf_package.VnfPackage, 'get_by_id') - @mock.patch.object(objects.vnf_package.VnfPackage, 'save') - @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') - @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') - def test_create_without_name_and_description_with_v241( - self, mock_get_by_id_package_vnfd, - mock_vnf_instance_create, mock_package_save, - mock_get_by_id_package, mock_get_vim, - mock_save): - mock_get_vim.return_value = self.vim_info - mock_get_by_id_package_vnfd.return_value = \ - fakes.return_vnf_package_vnfd() - mock_get_by_id_package.return_value = \ - fakes.return_vnf_package_with_deployment_flavour() - - updates = {'vnfd_id': uuidsentinel.vnfd_id, - 'vnf_instance_description': None, - 'vnf_instance_name': None, - 'vnf_pkg_id': uuidsentinel.vnf_pkg_id, - 'metadata': {'key': 'value'}} - - mock_vnf_instance_create.return_value =\ - fakes.return_vnf_instance_model(**updates) - - req = fake_request.HTTPRequest.blank('/vnf_instances') - body = {'vnfdId': uuidsentinel.vnfd_id} - req.body = jsonutils.dump_as_bytes(body) - req.headers['Content-Type'] = 'application/json' - req.headers['Version'] = '' - req.method = 'POST' - - # Call create API - resp = req.get_response(self.app) - - self.assertEqual(http_client.CREATED, resp.status_code) - - updates = {'vnfInstanceDescription': None, 'vnfInstanceName': None} - expected_vnf = fakes.fake_vnf_instance_response(**updates) - - self.assertEqual(expected_vnf, resp.json) - @ddt.data( {'attribute': 'vnfdId', 'value': True, 'expected_type': 'uuid'}, @@ -384,13 +349,7 @@ class TestController(base.TestCase): {'attribute': 'vnfInstanceDescription', 'value': True, 'expected_type': 'description'}, {'attribute': 'vnfInstanceDescription', 'value': 123, - 'expected_type': 'description'}, - {'attribute': 'metadata', 'value': ['val1', 'val2'], - 'expected_type': 'object'}, - {'attribute': 'metadata', 'value': True, - 'expected_type': 'object'}, - {'attribute': 'metadata', 'value': 123, - 'expected_type': 'object'}, + 'expected_type': 'description'} ) @ddt.unpack def test_create_with_invalid_request_body( @@ -404,7 +363,6 @@ class TestController(base.TestCase): body.update({attribute: value}) req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' - req.headers['Version'] = '2.6.1' req.method = 'POST' exception = self.assertRaises( exceptions.ValidationError, self.controller.create, @@ -428,28 +386,160 @@ class TestController(base.TestCase): self.assertEqual(expected_message, exception.msg) + @mock.patch.object(sync_resource.SyncVnfPackage, 'create_package') + @mock.patch.object(nfvo_client.VnfPackageRequest, "index") + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') - def test_create_non_existing_vnf_package_vnfd(self, mock_vnf_by_id): + def test_create_non_existing_vnf_package_vnfd(self, mock_vnf_by_id, + mock_get_service_plugins, + mock_index, + mock_create_package): mock_vnf_by_id.side_effect = exceptions.VnfPackageVnfdNotFound + mock_create_package.return_value = fakes.return_vnf_package_vnfd() + mock_response = mock.MagicMock() + mock_response.ok = True + mock_response.json = mock.MagicMock() + mock_response.json.return_value = ['aaa', 'bbb', 'ccc'] + mock_index.return_value = mock_response body = {'vnfdId': uuidsentinel.vnfd_id, 'metadata': {"key": "value"}} req = fake_request.HTTPRequest.blank('/vnf_instances') req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' - req.headers['Version'] = '2.6.1' req.method = 'POST' self.assertRaises(exc.HTTPBadRequest, self.controller.create, req, body=body) - def test_create_without_vnfd_id(self): + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._update_package_usage_state') + @mock.patch.object(objects.VnfPackage, 'get_by_id') + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._create_vnf') + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(sync_resource.SyncVnfPackage, 'create_package') + @mock.patch.object(VnfPackageRequest, "index") + @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') + def test_create_vnf_package_not_found( + self, mock_get_by_id_package_vnfd, + mock_vnf_instance_create, + mock_index, mock_create_pkg, + mock_get_service_plugins, + mock_private_create_vnf, + mock_vnf_package_get_by_id, + mock_update_package_usage_state, + mock_get_vim): + mock_get_by_id_package_vnfd.side_effect =\ + exceptions.VnfPackageVnfdNotFound + + mock_response = mock.MagicMock() + mock_response.ok = True + mock_response.json = mock.MagicMock() + mock_response.json.return_value = ['aaa', 'bbb', 'ccc'] + + mock_index.return_value = mock_response + mock_create_pkg.return_value = fakes.return_vnf_package_vnfd() + + updates = {'vnfd_id': uuidsentinel.vnfd_id} + mock_vnf_instance_create.return_value =\ + fakes.return_vnf_instance_model(**updates) + + body = {'vnfdId': uuidsentinel.vnfd_id} + req = fake_request.HTTPRequest.blank('/vnf_instances') + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.environ['tacker.context'] = self.context + + # Call Create API + resp = req.get_response(self.app) + self.assertEqual(http_client.CREATED, resp.status_code) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(sync_resource.SyncVnfPackage, 'create_package') + @mock.patch.object(VnfPackageRequest, "index") + @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') + def test_create_vnf_package_vnfd_not_found( + self, mock_get_by_id_package_vnfd, + mock_vnf_instance_create, + mock_index, mock_create_pkg, + mock_get_service_plugins): + mock_get_by_id_package_vnfd.side_effect =\ + exceptions.VnfPackageVnfdNotFound + + mock_response = mock.MagicMock() + mock_response.ok = True + mock_response.json = mock.MagicMock() + mock_response.json.return_value = ['aaa', 'bbb', 'ccc'] + + mock_index.return_value = mock_response + mock_create_pkg.return_value = None + + updates = {'vnfd_id': uuidsentinel.vnfd_id} + mock_vnf_instance_create.return_value =\ + fakes.return_vnf_instance_model(**updates) + + body = {'vnfdId': uuidsentinel.vnfd_id} + req = fake_request.HTTPRequest.blank('/vnf_instances') + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Create API + resp = req.get_response(self.app) + self.assertEqual(http_client.INTERNAL_SERVER_ERROR, resp.status_code) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @mock.patch.object(VnfPackageRequest, "index") + @mock.patch.object(objects.vnf_instance, '_vnf_instance_create') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, 'get_by_id') + def test_create_non_vnf_package_info( + self, mock_get_by_id_package_vnfd, + mock_vnf_instance_create, + mock_index, mock_get_service_plugins): + mock_get_by_id_package_vnfd.side_effect =\ + exceptions.VnfPackageVnfdNotFound + + mock_response = mock.MagicMock() + mock_response.ok = False + mock_response.json = mock.MagicMock() + mock_response.json.return_value = {} + + mock_index.return_value = mock_response + + updates = {'vnfd_id': uuidsentinel.vnfd_id} + mock_vnf_instance_create.return_value =\ + fakes.return_vnf_instance_model(**updates) + + body = {'vnfdId': uuidsentinel.vnfd_id} + req = fake_request.HTTPRequest.blank('/vnf_instances') + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call Create API + resp = req.get_response(self.app) + + self.assertEqual(http_client.NOT_FOUND, resp.status_code) + + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + def test_create_without_vnfd_id(self, mock_get_service_plugins): body = {"vnfInstanceName": "SampleVnfInstance", - 'metadata': {"key": "value"}} + "metadata": {"key": "value"}} req = fake_request.HTTPRequest.blank( '/vnf_instances') req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' req.method = 'POST' resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) @ddt.data('PATCH', 'PUT', 'HEAD', 'DELETE') @@ -461,29 +551,38 @@ class TestController(base.TestCase): req.headers['Content-Type'] = 'application/json' req.method = method resp = req.get_response(self.app) + self.assertEqual(http_client.METHOD_NOT_ALLOWED, resp.status_code) - @ddt.data({'name': "A" * 256, 'description': "VNF Description", - 'meta': {"key": "value"}}, - {'name': 'Fake-VNF', 'description': "A" * 1025, - 'meta': {"key": "value"}}, - {'name': 'Fake-VNF', 'description': "VNF Description", - 'meta': {"key": "v" * 256}}) - @ddt.unpack + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) + @ddt.data({'name': "A" * 256, + 'description': "VNF Description", + 'meta': {"key": "value"}}, + {'name': 'Fake-VNF', + 'description': "A" * 1025, + 'meta': {"key": "value"}}, + {'name': 'Fake-VNF', + 'description': "VNF Description", + 'meta': {"key": "v" * 256}}) def test_create_max_length_exceeded_for_vnf_name_and_description( - self, name, description, meta): + self, values, mock_get_service_plugins): + name = values['name'] + meta = values['meta'] + description = values['description'] # vnf instance_name and description with length greater than max # length defined body = {"vnfInstanceName": name, "vnfdId": uuidsentinel.vnfd_id, "vnfInstanceDescription": description, - "metadata": meta} + 'metadata': meta} req = fake_request.HTTPRequest.blank( '/vnf_instances') req.body = jsonutils.dump_as_bytes(body) req.headers['Content-Type'] = 'application/json' req.method = 'POST' resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) @mock.patch.object(TackerManager, 'get_service_plugins', @@ -1487,9 +1586,14 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.METHOD_NOT_ALLOWED, resp.status_code) + @mock.patch('tacker.api.vnflcm.v1.controller.' + 'VnfLcmController._delete') + @mock.patch.object(TackerManager, 'get_service_plugins', + return_value={'VNFM': FakeVNFMPlugin()}) @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") @mock.patch.object(objects.vnf_instance, '_destroy_vnf_instance') - def test_delete(self, mock_destroy_vnf_instance, mock_vnf_by_id): + def test_delete(self, mock_destroy_vnf_instance, mock_vnf_by_id, + mock_get_service_plugins, mock_private_delete): req = fake_request.HTTPRequest.blank( '/vnf_instances/%s' % uuidsentinel.vnf_instance_id) req.method = 'DELETE' @@ -1500,7 +1604,6 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.NO_CONTENT, resp.status_code) - mock_destroy_vnf_instance.assert_called_once() @mock.patch.object(objects.VnfInstance, "get_by_id") def test_delete_with_non_existing_vnf_instance(self, mock_vnf_by_id): diff --git a/tacker/tests/unit/vnflcm/test_sync_resource.py b/tacker/tests/unit/vnflcm/test_sync_resource.py new file mode 100644 index 0000000..905f071 --- /dev/null +++ b/tacker/tests/unit/vnflcm/test_sync_resource.py @@ -0,0 +1,350 @@ +# 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. + +import ddt + +from tacker.api.vnflcm.v1 import sync_resource as sync +from tacker.conductor.conductorrpc import vnf_pkgm_rpc +from tacker import context +from tacker.db.nfvo import nfvo_db +from tacker.glance_store import store as glance_store +from tacker import objects +from tacker.tests.unit import base +from tacker.tests.unit.vnflcm import fakes as vnflcm_fakes +from tacker.tests.unit.vnfpkgm import fakes as vnfpkgm_fakes +from tacker.tests import uuidsentinel +import tacker.vnfm.nfvo_client as nfvo_client +from unittest import mock +from webob import exc + + +@ddt.ddt +class TestSyncVnfPackage(base.TestCase): + + def setUp(self): + super(TestSyncVnfPackage, self).setUp() + self.context = context.ContextBase( + uuidsentinel.user_id, + uuidsentinel.project_id, + is_admin=True) + self.vim = nfvo_db.Vim() + + def tearDown(self): + super(TestSyncVnfPackage, self).tearDown() + self.addCleanup(mock.patch.stopall) + + @mock.patch.object(glance_store, 'store_csar') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, + 'get_by_id') + @mock.patch.object(objects.vnf_package, '_vnf_package_create') + @mock.patch.object(objects.vnf_package.VnfPackage, 'save') + @mock.patch.object(objects.vnf_package.VnfPackage, '_from_db_object') + @mock.patch.object(nfvo_client.VnfPackageRequest, 'download_vnf_packages') + @mock.patch.object(vnf_pkgm_rpc.VNFPackageRPCAPI, + "upload_vnf_package_content") + def test_package_and_package_vnfd_creation_successful(self, + mock_upload_vnf_package_content, + mock_nfvo_download_vnf_packages, + mock_from_db_vnf_package, + mock_save_vnf_package, + mock_vnf_package, + mock_get_by_id, + mock_glance_store): + + # glance_store mock Settings + mock_glance_store.return_value = 'location', 0, 'checksum',\ + 'multihash', 'loc_meta' + + updates = {'additionalArtifacts': [ + { + 'artifactPath': 'sample1_file.yaml', + 'checksum': { + 'hash': + '53b504e608eef3d19a\ + 22413bf6ee72f42091fbb5213e6a876a3a22f6c3c94fe1', + 'algorithm': 'SHA-256' + } + }, + { + 'artifactPath': 'sample2_file.yaml', + 'checksum': { + 'hash': + '43b504e608eef3d19a\ + 22413bf6ee72f42091fbb5213e6a876a3a22f6c3c94fe1', + 'algorithm': 'SHA-256' + } + } + ]} + vnf_package_info = vnfpkgm_fakes.index_response( + remove_attrs=['userDefinedData'], + vnf_package_updates=updates)[0] + + exp_vnf_package_vnfd = \ + vnflcm_fakes.fake_vnf_package_vnfd_model_dict(**updates) + mock_get_by_id.return_value = exp_vnf_package_vnfd + + vnf_package_vnfd = sync.SyncVnfPackage.create_package( + self.context, vnf_package_info) + + # Expected value setting + self.assertEqual(exp_vnf_package_vnfd.get('package_uuid'), + vnf_package_vnfd.get('package_uuid')) + self.assertEqual(exp_vnf_package_vnfd.get('vnfd_id'), + vnf_package_vnfd.get('vnfd_id')) + self.assertEqual(exp_vnf_package_vnfd.get('vnf_provider'), + vnf_package_vnfd.get('vnf_provider')) + self.assertEqual(exp_vnf_package_vnfd.get('vnf_product_name'), + vnf_package_vnfd.get('vnf_product_name')) + self.assertEqual(exp_vnf_package_vnfd.get('vnf_software_version'), + vnf_package_vnfd.get('vnf_software_version')) + self.assertEqual(exp_vnf_package_vnfd.get('vnfd_version'), + vnf_package_vnfd.get('vnfd_version')) + + # Check if a mock is called even once + mock_upload_vnf_package_content.assert_called() + mock_nfvo_download_vnf_packages.assert_called() + mock_from_db_vnf_package.assert_called() + mock_save_vnf_package.assert_called() + mock_vnf_package.assert_called() + mock_get_by_id.assert_called() + mock_glance_store.assert_called() + + @mock.patch.object(objects.vnf_package, '_vnf_package_create') + def test_package_and_package_vnfd_creation_package_create_err(self, + mock_vnf_package): + + # vnf_package mock Settings + mock_vnf_package.side_effect = Exception + + # SyncVnfPackage.create_package + vnf_package_info = vnfpkgm_fakes.index_response()[0] + self.assertRaises( + exc.HTTPInternalServerError, + sync.SyncVnfPackage.create_package, + self.context, + vnf_package_info) + + # Check if a mock is called even once + mock_vnf_package.assert_called() + + @mock.patch.object(objects.vnf_package, '_vnf_package_create') + @mock.patch.object(objects.vnf_package.VnfPackage, '_from_db_object') + @mock.patch.object(nfvo_client.VnfPackageRequest, 'download_vnf_packages') + def test_package_and_package_vnfd_creation_undefinedexcep(self, + mock_nfvo_download_vnf_packages, + mock_from_db_vnf_package, + mock_vnf_package): + + # VnfPackageRequest.download_vnf_packages mock Settings + mock_nfvo_download_vnf_packages.side_effect = \ + nfvo_client.UndefinedExternalSettingException( + "Vnf package the external setting to 'base_url' undefined.") + + # SyncVnfPackage.create_package + vnf_package_info = vnfpkgm_fakes.index_response()[0] + self.assertRaises( + exc.HTTPNotFound, + sync.SyncVnfPackage.create_package, + self.context, + vnf_package_info) + + # Check if a mock is called even once + mock_vnf_package.assert_called() + mock_from_db_vnf_package.assert_called() + mock_nfvo_download_vnf_packages.assert_called() + + @mock.patch.object(objects.vnf_package, '_vnf_package_create') + @mock.patch.object(objects.vnf_package.VnfPackage, '_from_db_object') + @mock.patch.object(nfvo_client.VnfPackageRequest, 'download_vnf_packages') + def test_package_and_package_vnfd_creation_falieddownloadexcep(self, + mock_nfvo_download_vnf_packages, + mock_from_db_vnf_package, + mock_vnf_package): + + # VnfPackageRequest.download_vnf_packages mock Settings + vnf_package_zip = '' + mock_nfvo_download_vnf_packages.side_effect = \ + nfvo_client.FaliedDownloadContentException( + "Failed response content, vnf_package_zip={}".format( + vnf_package_zip)) + + # SyncVnfPackage.create_package + vnf_package_info = vnfpkgm_fakes.index_response()[0] + self.assertRaises( + exc.HTTPInternalServerError, + sync.SyncVnfPackage.create_package, + self.context, + vnf_package_info) + + # Check if a mock is called even once + mock_vnf_package.assert_called() + mock_from_db_vnf_package.assert_called() + mock_nfvo_download_vnf_packages.assert_called() + + @mock.patch.object(objects.vnf_package, '_vnf_package_create') + @mock.patch.object(objects.vnf_package.VnfPackage, '_from_db_object') + @mock.patch.object(nfvo_client.VnfPackageRequest, 'download_vnf_packages') + def test_package_and_package_vnfd_creation_exception(self, + mock_nfvo_download_vnf_packages, + mock_from_db_vnf_package, + mock_vnf_package): + + # VnfPackageRequest.download_vnf_packages mock Settings + mock_nfvo_download_vnf_packages.side_effect = Exception + + # SyncVnfPackage.create_package + vnf_package_info = vnfpkgm_fakes.index_response()[0] + self.assertRaises( + exc.HTTPInternalServerError, + sync.SyncVnfPackage.create_package, + self.context, + vnf_package_info) + + # Check if a mock is called even once + mock_vnf_package.assert_called() + mock_from_db_vnf_package.assert_called() + mock_nfvo_download_vnf_packages.assert_called() + + @mock.patch.object(glance_store, 'store_csar') + @mock.patch.object(objects.vnf_package, '_vnf_package_create') + @mock.patch.object(objects.vnf_package.VnfPackage, '_from_db_object') + @mock.patch.object(nfvo_client.VnfPackageRequest, 'download_vnf_packages') + def test_package_and_package_vnfd_creation_store_csar_err(self, + mock_nfvo_download_vnf_packages, + mock_from_db_vnf_package, + mock_vnf_package, + mock_glance_store): + + # glance_store mock Settings + mock_glance_store.side_effect = Exception + + # SyncVnfPackage.create_package + vnf_package_info = vnfpkgm_fakes.index_response()[0] + self.assertRaises( + exc.HTTPInternalServerError, + sync.SyncVnfPackage.create_package, + self.context, + vnf_package_info) + + # Check if a mock is called even once + mock_vnf_package.assert_called() + mock_from_db_vnf_package.assert_called() + mock_nfvo_download_vnf_packages.assert_called() + mock_glance_store.assert_called() + + @mock.patch.object(glance_store, 'store_csar') + @mock.patch.object(objects.vnf_package, '_vnf_package_create') + @mock.patch.object(objects.vnf_package.VnfPackage, 'save') + @mock.patch.object(objects.vnf_package.VnfPackage, '_from_db_object') + @mock.patch.object(nfvo_client.VnfPackageRequest, 'download_vnf_packages') + def test_package_and_package_vnfd_creation_package_upde_err(self, + mock_nfvo_download_vnf_packages, + mock_from_db_vnf_package, + mock_save_vnf_package, + mock_vnf_package, + mock_glance_store): + + # glance_store mock Settings + mock_glance_store.return_value = 'location', 0, 'checksum',\ + 'multihash', 'loc_meta' + + # vnf_package mock Settings + mock_save_vnf_package.side_effect = Exception + + # SyncVnfPackage.create_package + vnf_package_info = vnfpkgm_fakes.index_response()[0] + self.assertRaises( + exc.HTTPInternalServerError, + sync.SyncVnfPackage.create_package, + self.context, + vnf_package_info) + + # Check if a mock is called even once + mock_nfvo_download_vnf_packages.assert_called() + mock_from_db_vnf_package.assert_called() + mock_save_vnf_package.assert_called() + mock_vnf_package.assert_called() + mock_glance_store.assert_called() + + @mock.patch.object(glance_store, 'store_csar') + @mock.patch.object(vnf_pkgm_rpc.VNFPackageRPCAPI, + 'upload_vnf_package_content') + @mock.patch.object(objects.vnf_package, '_vnf_package_create') + @mock.patch.object(objects.vnf_package.VnfPackage, 'save') + @mock.patch.object(objects.vnf_package.VnfPackage, '_from_db_object') + @mock.patch.object(nfvo_client.VnfPackageRequest, 'download_vnf_packages') + def test_package_and_package_vnfd_creation_rpc_err(self, + mock_nfvo_download_vnf_packages, + mock_from_db_vnf_package, + mock_save_vnf_package, + mock_vnf_package, + mock_upload_vnf_package_content, + mock_glance_store): + + # glance_store mock Settings + mock_glance_store.return_value = 'location', 0, 'checksum',\ + 'multihash', 'loc_meta' + mock_upload_vnf_package_content.side_effect = Exception + + # SyncVnfPackage.create_package + vnf_package_info = vnfpkgm_fakes.index_response()[0] + self.assertRaises( + exc.HTTPInternalServerError, + sync.SyncVnfPackage.create_package, + self.context, + vnf_package_info) + + # Check if a mock is called even once + mock_nfvo_download_vnf_packages.assert_called() + mock_from_db_vnf_package.assert_called() + mock_save_vnf_package.assert_called() + mock_vnf_package.assert_called() + mock_upload_vnf_package_content.assert_called() + mock_glance_store.assert_called() + + @mock.patch.object(glance_store, 'store_csar') + @mock.patch.object(objects.vnf_package_vnfd.VnfPackageVnfd, + 'get_by_id') + @mock.patch.object(objects.vnf_package, '_vnf_package_create') + @mock.patch.object(objects.vnf_package.VnfPackage, 'save') + @mock.patch.object(objects.vnf_package.VnfPackage, '_from_db_object') + @mock.patch.object(nfvo_client.VnfPackageRequest, 'download_vnf_packages') + def test_package_and_get_package_vnfd_err(self, + mock_nfvo_download_vnf_packages, + mock_from_db_vnf_package, + mock_save_vnf_package, + mock_vnf_package, + mock_get_by_id, + mock_glance_store): + + # glance_store mock Settings + mock_glance_store.return_value = 'location', 0, 'checksum',\ + 'multihash', 'loc_meta' + + # vnf_package_vnfd mock Settings + mock_get_by_id.side_effect = Exception + + # SyncVnfPackage.create_package + vnf_package_info = vnfpkgm_fakes.index_response()[0] + self.assertRaises( + exc.HTTPInternalServerError, + sync.SyncVnfPackage.create_package, + self.context, + vnf_package_info) + + # Check if a mock is called even once + mock_nfvo_download_vnf_packages.assert_called() + mock_from_db_vnf_package.assert_called() + mock_save_vnf_package.assert_called() + mock_vnf_package.assert_called() + mock_get_by_id.assert_called() + mock_glance_store.assert_called() diff --git a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py index f671443..4ddfe86 100644 --- a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py +++ b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py @@ -19,6 +19,7 @@ import shutil from unittest import mock from oslo_config import cfg +from oslo_utils import uuidutils from tacker.common import exceptions from tacker.common import utils from tacker import context @@ -32,6 +33,26 @@ from tacker.vnflcm import vnflcm_driver from tacker.vnfm import vim_client +vnf_dict = { + 'id': uuidutils.generate_uuid(), + 'mgmt_ip_address': '{"VDU1": "a.b.c.d"}', + 'vim_id': '6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'instance_id': 'a737497c-761c-11e5-89c3-9cb6541d805d', + 'vnfd': { + 'attributes': { + 'heat_template': { + 'resources': { + 'VDU1': { + 'properties': { + 'networks': [{'port': {'get_resource': 'CP1'}}]} + } + } + } + } + } +} + + OPTS_INFRA_DRIVER = [ cfg.ListOpt( 'infra_driver', default=['noop', 'openstack', 'kubernetes'], @@ -100,6 +121,10 @@ class FakeVimClient(mock.Mock): pass +class FakeTackerManager(mock.MagicMock): + pass + + class TestVnflcmDriver(db_base.SqlTestCase): def setUp(self): @@ -136,11 +161,13 @@ class TestVnflcmDriver(db_base.SqlTestCase): 'test_project'}, 'vim_type': 'openstack'} self.vim_client.get_vim.return_value = vim_obj + @mock.patch('tacker.vnflcm.utils._make_final_vnf_dict') @mock.patch.object(objects.VnfResource, 'create') @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfInstance, "save") def test_instantiate_vnf(self, mock_vnf_instance_save, - mock_vnf_package_vnfd, mock_create): + mock_vnf_package_vnfd, mock_create, + mock_final_vnf_dict): vnf_package_vnfd = fakes.return_vnf_package_vnfd() vnf_package_id = vnf_package_vnfd.package_uuid mock_vnf_package_vnfd.return_value = vnf_package_vnfd @@ -156,19 +183,22 @@ class TestVnflcmDriver(db_base.SqlTestCase): test_utils.copy_csar_files(fake_csar, "vnflcm4") self._mock_vnf_manager() driver = vnflcm_driver.VnfLcmDriver() - driver.instantiate_vnf(self.context, vnf_instance_obj, + driver.instantiate_vnf(self.context, vnf_instance_obj, vnf_dict, instantiate_vnf_req_obj) self.assertEqual("INSTANTIATED", vnf_instance_obj.instantiation_state) self.assertEqual(2, mock_vnf_instance_save.call_count) self.assertEqual(4, self._vnf_manager.invoke.call_count) + mock_final_vnf_dict.assert_called_once() shutil.rmtree(fake_csar) + @mock.patch('tacker.vnflcm.utils._make_final_vnf_dict') @mock.patch.object(objects.VnfResource, 'create') @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfInstance, "save") def test_instantiate_vnf_with_ext_virtual_links( - self, mock_vnf_instance_save, mock_vnf_package_vnfd, mock_create): + self, mock_vnf_instance_save, mock_vnf_package_vnfd, + mock_create, mock_final_vnf_dict): vnf_package_vnfd = fakes.return_vnf_package_vnfd() vnf_package_id = vnf_package_vnfd.package_uuid mock_vnf_package_vnfd.return_value = vnf_package_vnfd @@ -186,19 +216,22 @@ class TestVnflcmDriver(db_base.SqlTestCase): test_utils.copy_csar_files(fake_csar, "vnflcm4") self._mock_vnf_manager() driver = vnflcm_driver.VnfLcmDriver() - driver.instantiate_vnf(self.context, vnf_instance_obj, + driver.instantiate_vnf(self.context, vnf_instance_obj, vnf_dict, instantiate_vnf_req_obj) self.assertEqual("INSTANTIATED", vnf_instance_obj.instantiation_state) self.assertEqual(2, mock_vnf_instance_save.call_count) self.assertEqual(4, self._vnf_manager.invoke.call_count) + mock_final_vnf_dict.assert_called_once() shutil.rmtree(fake_csar) + @mock.patch('tacker.vnflcm.utils._make_final_vnf_dict') @mock.patch.object(objects.VnfResource, 'create') @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfInstance, "save") def test_instantiate_vnf_vim_connection_info( - self, mock_vnf_instance_save, mock_vnf_package_vnfd, mock_create): + self, mock_vnf_instance_save, mock_vnf_package_vnfd, + mock_create, mock_final_vnf_dict): vnf_package_vnfd = fakes.return_vnf_package_vnfd() vnf_package_id = vnf_package_vnfd.package_uuid mock_vnf_package_vnfd.return_value = vnf_package_vnfd @@ -216,19 +249,22 @@ class TestVnflcmDriver(db_base.SqlTestCase): test_utils.copy_csar_files(fake_csar, "vnflcm4") self._mock_vnf_manager() driver = vnflcm_driver.VnfLcmDriver() - driver.instantiate_vnf(self.context, vnf_instance_obj, + driver.instantiate_vnf(self.context, vnf_instance_obj, vnf_dict, instantiate_vnf_req_obj) self.assertEqual("INSTANTIATED", vnf_instance_obj.instantiation_state) self.assertEqual(2, mock_vnf_instance_save.call_count) self.assertEqual(4, self._vnf_manager.invoke.call_count) + mock_final_vnf_dict.assert_called_once() shutil.rmtree(fake_csar) + @mock.patch('tacker.vnflcm.utils._make_final_vnf_dict') @mock.patch.object(objects.VnfResource, 'create') @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfInstance, "save") def test_instantiate_vnf_infra_fails_to_instantiate( - self, mock_vnf_instance_save, mock_vnf_package_vnfd, mock_create): + self, mock_vnf_instance_save, mock_vnf_package_vnfd, + mock_create, mock_final_vnf_dict): vnf_package_vnfd = fakes.return_vnf_package_vnfd() vnf_package_id = vnf_package_vnfd.package_uuid mock_vnf_package_vnfd.return_value = vnf_package_vnfd @@ -247,7 +283,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): self._mock_vnf_manager(fail_method_name="instantiate_vnf") driver = vnflcm_driver.VnfLcmDriver() error = self.assertRaises(exceptions.VnfInstantiationFailed, - driver.instantiate_vnf, self.context, vnf_instance_obj, + driver.instantiate_vnf, self.context, vnf_instance_obj, vnf_dict, instantiate_vnf_req_obj) expected_error = ("Vnf instantiation failed for vnf %s, error: " "instantiate_vnf failed") @@ -257,15 +293,17 @@ class TestVnflcmDriver(db_base.SqlTestCase): vnf_instance_obj.instantiation_state) self.assertEqual(2, mock_vnf_instance_save.call_count) self.assertEqual(2, self._vnf_manager.invoke.call_count) + mock_final_vnf_dict.assert_called_once() shutil.rmtree(fake_csar) + @mock.patch('tacker.vnflcm.utils._make_final_vnf_dict') @mock.patch.object(objects.VnfResource, 'create') @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfInstance, "save") def test_instantiate_vnf_infra_fails_to_wait_after_instantiate( self, mock_vnf_instance_save, mock_vnf_package_vnfd, - mock_create): + mock_create, mock_final_vnf_dict): vnf_package_vnfd = fakes.return_vnf_package_vnfd() vnf_package_id = vnf_package_vnfd.package_uuid mock_vnf_package_vnfd.return_value = vnf_package_vnfd @@ -284,7 +322,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): self._mock_vnf_manager(fail_method_name='create_wait') driver = vnflcm_driver.VnfLcmDriver() error = self.assertRaises(exceptions.VnfInstantiationWaitFailed, - driver.instantiate_vnf, self.context, vnf_instance_obj, + driver.instantiate_vnf, self.context, vnf_instance_obj, vnf_dict, instantiate_vnf_req_obj) expected_error = ("Vnf instantiation wait failed for vnf %s, error: " "create_wait failed") @@ -294,14 +332,16 @@ class TestVnflcmDriver(db_base.SqlTestCase): vnf_instance_obj.instantiation_state) self.assertEqual(3, mock_vnf_instance_save.call_count) self.assertEqual(5, self._vnf_manager.invoke.call_count) - + mock_final_vnf_dict.assert_called_once() shutil.rmtree(fake_csar) + @mock.patch('tacker.vnflcm.utils._make_final_vnf_dict') @mock.patch.object(objects.VnfResource, 'create') @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfInstance, "save") def test_instantiate_vnf_with_short_notation(self, mock_vnf_instance_save, - mock_vnf_package_vnfd, mock_create): + mock_vnf_package_vnfd, mock_create, + mock_final_vnf_dict): vnf_package_vnfd = fakes.return_vnf_package_vnfd() vnf_package_id = vnf_package_vnfd.package_uuid mock_vnf_package_vnfd.return_value = vnf_package_vnfd @@ -318,17 +358,20 @@ class TestVnflcmDriver(db_base.SqlTestCase): fake_csar, "sample_vnf_package_csar_with_short_notation") self._mock_vnf_manager(vnf_resource_count=2) driver = vnflcm_driver.VnfLcmDriver() - driver.instantiate_vnf(self.context, vnf_instance_obj, + driver.instantiate_vnf(self.context, vnf_instance_obj, vnf_dict, instantiate_vnf_req_obj) self.assertEqual(2, mock_create.call_count) self.assertEqual("INSTANTIATED", vnf_instance_obj.instantiation_state) + mock_final_vnf_dict.assert_called_once() shutil.rmtree(fake_csar) + @mock.patch('tacker.vnflcm.utils._make_final_vnf_dict') @mock.patch.object(objects.VnfResource, 'create') @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(objects.VnfInstance, "save") def test_instantiate_vnf_with_single_vnfd(self, mock_vnf_instance_save, - mock_vnf_package_vnfd, mock_create): + mock_vnf_package_vnfd, mock_create, + mock_final_vnf_dict): vnf_package_vnfd = fakes.return_vnf_package_vnfd() vnf_package_id = vnf_package_vnfd.package_uuid mock_vnf_package_vnfd.return_value = vnf_package_vnfd @@ -345,10 +388,11 @@ class TestVnflcmDriver(db_base.SqlTestCase): fake_csar, "sample_vnfpkg_no_meta_single_vnfd") self._mock_vnf_manager(vnf_resource_count=2) driver = vnflcm_driver.VnfLcmDriver() - driver.instantiate_vnf(self.context, vnf_instance_obj, + driver.instantiate_vnf(self.context, vnf_instance_obj, vnf_dict, instantiate_vnf_req_obj) self.assertEqual(2, mock_create.call_count) self.assertEqual("INSTANTIATED", vnf_instance_obj.instantiation_state) + mock_final_vnf_dict.assert_called_once() shutil.rmtree(fake_csar) @mock.patch.object(objects.VnfInstance, "save") @@ -454,6 +498,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): self.assertEqual(2, mock_vnf_instance_save.call_count) self.assertEqual(3, self._vnf_manager.invoke.call_count) + @mock.patch('tacker.vnflcm.utils._make_final_vnf_dict') @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.VnfResource, "create") @@ -463,7 +508,8 @@ class TestVnflcmDriver(db_base.SqlTestCase): @mock.patch('tacker.vnflcm.vnflcm_driver.LOG') def test_heal_vnf_without_vnfc_instance(self, mock_log, mock_save, mock_vnf_resource_list, mock_resource_destroy, - mock_resource_create, mock_vim, mock_vnf_package_vnfd): + mock_resource_create, mock_vim, mock_vnf_package_vnfd, + mock_final_vnf_dict): vnf_package_vnfd = fakes.return_vnf_package_vnfd() vnf_package_id = vnf_package_vnfd.package_uuid mock_vnf_package_vnfd.return_value = vnf_package_vnfd @@ -494,7 +540,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): uuidsentinel.instance_id self._mock_vnf_manager() driver = vnflcm_driver.VnfLcmDriver() - driver.heal_vnf(self.context, vnf_instance, heal_vnf_req) + driver.heal_vnf(self.context, vnf_instance, vnf_dict, heal_vnf_req) self.assertEqual(1, mock_save.call_count) # vnf resource software images will be deleted during # deleting vnf instance. @@ -509,7 +555,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): "is completed successfully") mock_log.info.assert_called_with(expected_msg, vnf_instance.id) - + mock_final_vnf_dict.assert_called_once() shutil.rmtree(fake_csar) @mock.patch.object(objects.VnfInstance, "save") @@ -527,7 +573,8 @@ class TestVnflcmDriver(db_base.SqlTestCase): self._mock_vnf_manager(fail_method_name='delete') driver = vnflcm_driver.VnfLcmDriver() self.assertRaises(exceptions.VnfHealFailed, - driver.heal_vnf, self.context, vnf_instance, heal_vnf_req) + driver.heal_vnf, self.context, vnf_instance, + vnf_dict, heal_vnf_req) self.assertEqual(1, mock_save.call_count) self.assertEqual(1, self._vnf_manager.invoke.call_count) self.assertEqual(fields.VnfInstanceTaskState.ERROR, @@ -537,6 +584,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): 'state. Error: delete failed') mock_log.error.assert_called_with(expected_msg % vnf_instance.id) + @mock.patch('tacker.vnflcm.utils._make_final_vnf_dict') @mock.patch.object(objects.VnfPackageVnfd, 'get_by_id') @mock.patch.object(vim_client.VimClient, "get_vim") @mock.patch.object(objects.VnfResource, "create") @@ -547,7 +595,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): def test_heal_vnf_without_vnfc_instance_infra_instantiate_vnf_fail(self, mock_log, mock_save, mock_vnf_resource_list, mock_resource_destroy, mock_resource_create, mock_vim, - mock_vnf_package_vnfd): + mock_vnf_package_vnfd, mock_final_vnf_dict): vnf_package_vnfd = fakes.return_vnf_package_vnfd() vnf_package_id = vnf_package_vnfd.package_uuid mock_vnf_package_vnfd.return_value = vnf_package_vnfd @@ -568,7 +616,8 @@ class TestVnflcmDriver(db_base.SqlTestCase): self._mock_vnf_manager(fail_method_name='instantiate_vnf') driver = vnflcm_driver.VnfLcmDriver() self.assertRaises(exceptions.VnfHealFailed, - driver.heal_vnf, self.context, vnf_instance, heal_vnf_req) + driver.heal_vnf, self.context, + vnf_instance, vnf_dict, heal_vnf_req) self.assertEqual(1, mock_save.call_count) # vnf resource software images will be deleted during # deleting vnf instance. @@ -586,6 +635,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): 'error: instantiate_vnf failed') mock_log.error.assert_called_with(expected_msg % (vnf_instance.id, vnf_instance.id)) + mock_final_vnf_dict.assert_called_once() @mock.patch.object(objects.VnfInstance, "save") @mock.patch('tacker.vnflcm.vnflcm_driver.LOG') @@ -599,7 +649,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): self._mock_vnf_manager() driver = vnflcm_driver.VnfLcmDriver() - driver.heal_vnf(self.context, vnf_instance, heal_vnf_req) + driver.heal_vnf(self.context, vnf_instance, mock.ANY, heal_vnf_req) self.assertEqual(1, mock_save.call_count) self.assertEqual(3, self._vnf_manager.invoke.call_count) @@ -623,7 +673,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): driver = vnflcm_driver.VnfLcmDriver() self.assertRaises(exceptions.VnfHealFailed, driver.heal_vnf, self.context, vnf_instance, - heal_vnf_req) + mock.ANY, heal_vnf_req) self.assertEqual(1, mock_save.call_count) self.assertEqual(1, self._vnf_manager.invoke.call_count) @@ -656,7 +706,8 @@ class TestVnflcmDriver(db_base.SqlTestCase): # it will work and vnflcm can update the vnfc resources # properly and hence the _vnf_manager.invoke.call_count # should be 3 instead of 2. - driver.heal_vnf(self.context, vnf_instance, heal_vnf_req) + driver.heal_vnf(self.context, vnf_instance, mock.ANY, + heal_vnf_req) self.assertEqual(1, mock_save.call_count) self.assertEqual(3, self._vnf_manager.invoke.call_count) @@ -684,7 +735,8 @@ class TestVnflcmDriver(db_base.SqlTestCase): self._mock_vnf_manager(fail_method_name='post_heal_vnf') driver = vnflcm_driver.VnfLcmDriver() self.assertRaises(exceptions.VnfHealFailed, - driver.heal_vnf, self.context, vnf_instance, heal_vnf_req) + driver.heal_vnf, self.context, vnf_instance, + mock.ANY, heal_vnf_req) self.assertEqual(1, mock_save.call_count) self.assertEqual(3, self._vnf_manager.invoke.call_count) diff --git a/tacker/tests/unit/vnfm/test_vim_client.py b/tacker/tests/unit/vnfm/test_vim_client.py index 3e97913..cb549a4 100644 --- a/tacker/tests/unit/vnfm/test_vim_client.py +++ b/tacker/tests/unit/vnfm/test_vim_client.py @@ -73,6 +73,7 @@ class TestVIMClient(base.TestCase): region_name='TestRegionOne') vim_expect = {'vim_auth': {'password': '****'}, 'vim_id': 'aaaa', 'vim_name': 'VIM0', 'vim_type': 'test_vim', + 'placement_attr': {'regions': ['TestRegionOne']}, 'tenant': 'test'} self.assertEqual(vim_expect, vim_result) @@ -89,6 +90,7 @@ class TestVIMClient(base.TestCase): region_name='TestRegionOne') vim_expect = {'vim_auth': {'password': '****'}, 'vim_id': 'aaaa', 'vim_name': 'aaaa', 'vim_type': 'test_vim', + 'placement_attr': {'regions': ['TestRegionOne']}, 'tenant': 'test'} self.assertEqual(vim_expect, vim_result) diff --git a/tacker/tests/unit/vnfpkgm/test_controller.py b/tacker/tests/unit/vnfpkgm/test_controller.py index 63e2939..96ded50 100644 --- a/tacker/tests/unit/vnfpkgm/test_controller.py +++ b/tacker/tests/unit/vnfpkgm/test_controller.py @@ -15,6 +15,7 @@ from unittest import mock import ddt +import json import os from oslo_serialization import jsonutils from six.moves import http_client @@ -47,6 +48,16 @@ class TestController(base.TestCase): def app(self): return fakes.wsgi_app_v1() + def _make_problem_detail(self, title, detail, status): + res = exc.Response(content_type='application/problem+json') + problemDetails = {} + problemDetails['title'] = title + problemDetails['detail'] = detail + problemDetails['status'] = status + res.text = json.dumps(problemDetails) + res.status_int = status + return res + @mock.patch.object(vnf_package, '_vnf_package_create') @mock.patch.object(vnf_package.VnfPackage, '_from_db_object') def test_create_with_status_202(self, mock_from_db, mock_vnf_pack): @@ -594,27 +605,32 @@ class TestController(base.TestCase): req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/package_content' % constants.INVALID_UUID) - exception = self.assertRaises(exc.HTTPNotFound, - self.controller.upload_vnf_package_content, - req, constants.INVALID_UUID, - body=mock.mock_open()) - self.assertEqual( - "Can not find requested vnf package: %s" % constants.INVALID_UUID, - exception.explanation) + req.headers['Content-Type'] = 'application/zip' + req.method = 'PUT' + req.body = jsonutils.dump_as_bytes(mock.mock_open()) + + msg = _("Can not find requested vnf package: %s") \ + % constants.INVALID_UUID + res = self._make_problem_detail('Not Found', msg, 404) + resp = req.get_response(self.app) + self.assertEqual(res.text, resp.text) @mock.patch.object(vnf_package.VnfPackage, "get_by_id") def test_upload_vnf_package_content_without_vnf_pack(self, mock_vnf_by_id): msg = _("Can not find requested vnf package: %s") % constants.UUID - mock_vnf_by_id.side_effect = exc.HTTPNotFound(explanation=msg) + mock_vnf_by_id.side_effect = \ + tacker_exc.VnfPackageNotFound(explanation=msg) req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/package_content' % constants.UUID) - exception = self.assertRaises( - exc.HTTPNotFound, self.controller.upload_vnf_package_content, - req, constants.UUID, body=mock.mock_open()) - self.assertEqual( - "Can not find requested vnf package: %s" % constants.UUID, - exception.explanation) + req.headers['Content-Type'] = 'application/zip' + req.method = 'PUT' + req.body = jsonutils.dump_as_bytes(mock.mock_open()) + + msg = _("Can not find requested vnf package: %s") % constants.UUID + res = self._make_problem_detail('Not Found', msg, 404) + resp = req.get_response(self.app) + self.assertEqual(res.text, resp.text) @mock.patch.object(vnf_package.VnfPackage, "get_by_id") def test_upload_vnf_package_content_with_invalid_status(self, @@ -624,9 +640,15 @@ class TestController(base.TestCase): mock_vnf_by_id.return_value = vnf_obj req = fake_request.HTTPRequest.blank( '/vnf_packages/%s/package_content' % constants.UUID) - self.assertRaises(exc.HTTPConflict, - self.controller.upload_vnf_package_content, - req, constants.UUID, body=mock.mock_open()) + req.headers['Content-Type'] = 'application/zip' + req.method = 'PUT' + req.body = jsonutils.dump_as_bytes(mock.mock_open()) + + msg = _("VNF Package %s onboarding state is not CREATED") \ + % constants.UUID + res = self._make_problem_detail('Conflict', msg, 409) + resp = req.get_response(self.app) + self.assertEqual(res.text, resp.text) @mock.patch.object(urllib.request, 'urlopen') @mock.patch.object(VNFPackageRPCAPI, "upload_vnf_package_from_uri") diff --git a/tacker/vnflcm/utils.py b/tacker/vnflcm/utils.py index 3dbfa9a..56e7ae3 100644 --- a/tacker/vnflcm/utils.py +++ b/tacker/vnflcm/utils.py @@ -359,15 +359,25 @@ def _create_grant_request(vnfd_dict, package_uuid): return vnf_software_images -def _make_final_vnf_dict(vnfd_dict, id, name, param_values): - return {'vnfd': { - 'attributes': { - 'vnfd': str(vnfd_dict)}}, - 'id': id, - 'name': name, - 'attributes': { - 'param_values': str(param_values), - 'stack_name': name or ("vnflcm_" + id)}} +def _make_final_vnf_dict(vnfd_dict, id, name, param_values, vnf_dict=None): + if vnf_dict: + final_vnf_dict = vnf_dict + final_vnf_dict['vnfd']['attributes'].\ + update({'vnfd': str(vnfd_dict)}) + final_vnf_dict['attributes'].\ + update({'param_values': str(param_values)}) + final_vnf_dict['attributes'].\ + update({'stack_name': name or ("vnflcm_" + id)}) + return final_vnf_dict + else: + return {'vnfd': { + 'attributes': { + 'vnfd': str(vnfd_dict)}}, + 'id': id, + 'name': name, + 'attributes': { + 'param_values': str(param_values), + 'stack_name': name or ("vnflcm_" + id)}} def _get_flavour_based_vnfd(csar_path, flavour_id): @@ -517,6 +527,9 @@ def _build_instantiated_vnf_info(vnfd_dict, instantiate_vnf_req, inst_vnf_info.ext_managed_virtual_link_info = \ _build_ext_managed_virtual_link_info(instantiate_vnf_req, inst_vnf_info) + + inst_vnf_info.additional_params = instantiate_vnf_req.additional_params + vnf_instance.instantiated_vnf_info = inst_vnf_info diff --git a/tacker/vnflcm/vnflcm_driver.py b/tacker/vnflcm/vnflcm_driver.py index d81ec58..c77f773 100644 --- a/tacker/vnflcm/vnflcm_driver.py +++ b/tacker/vnflcm/vnflcm_driver.py @@ -132,8 +132,8 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): setattr(vnf_instance, k, v) vnf_instance.save() - def _instantiate_vnf(self, context, vnf_instance, vim_connection_info, - instantiate_vnf_req): + def _instantiate_vnf(self, context, vnf_instance, vnf_dict, + vim_connection_info, instantiate_vnf_req): vnfd_dict = vnflcm_utils._get_vnfd_dict(context, vnf_instance.vnfd_id, instantiate_vnf_req.flavour_id) base_hot_dict = vnflcm_utils._get_base_hot_dict( @@ -166,7 +166,7 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): vnfd_dict_to_create_final_dict = copy.deepcopy(vnfd_dict) final_vnf_dict = vnflcm_utils._make_final_vnf_dict( vnfd_dict_to_create_final_dict, vnf_instance.id, - vnf_instance.vnf_instance_name, param_for_subs_map) + vnf_instance.vnf_instance_name, param_for_subs_map, vnf_dict) try: instance_id = self._vnf_manager.invoke( @@ -224,7 +224,8 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): @log.log @rollback_vnf_instantiated_resources - def instantiate_vnf(self, context, vnf_instance, instantiate_vnf_req): + def instantiate_vnf(self, context, vnf_instance, vnf_dict, + instantiate_vnf_req): vim_connection_info_list = vnflcm_utils.\ _get_vim_connection_info_from_vnf_req(vnf_instance, @@ -239,8 +240,8 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): vim_connection_info = objects.VimConnectionInfo.obj_from_primitive( vim_info, context) - self._instantiate_vnf(context, vnf_instance, vim_connection_info, - instantiate_vnf_req) + self._instantiate_vnf(context, vnf_instance, vnf_dict, + vim_connection_info, instantiate_vnf_req) self._vnf_instance_update(context, vnf_instance, instantiation_state=fields.VnfInstanceState.INSTANTIATED, @@ -363,8 +364,8 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): raise exceptions.VnfHealFailed(id=vnf_instance.id, error=encodeutils.exception_to_unicode(exp)) - def _respawn_vnf(self, context, vnf_instance, vim_connection_info, - heal_vnf_request): + def _respawn_vnf(self, context, vnf_instance, vnf_dict, + vim_connection_info, heal_vnf_request): try: self._delete_vnf_instance_resources(context, vnf_instance, vim_connection_info, update_instantiated_state=False) @@ -388,8 +389,8 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): from_vnf_instance(vnf_instance) try: - self._instantiate_vnf(context, vnf_instance, vim_connection_info, - instantiate_vnf_request) + self._instantiate_vnf(context, vnf_instance, vnf_dict, + vim_connection_info, instantiate_vnf_request) except Exception as exc: with excutils.save_and_reraise_exception() as exc_ctxt: exc_ctxt.reraise = False @@ -407,7 +408,7 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): @log.log @revert_to_error_task_state - def heal_vnf(self, context, vnf_instance, heal_vnf_request): + def heal_vnf(self, context, vnf_instance, vnf_dict, heal_vnf_request): LOG.info("Request received for healing vnf '%s'", vnf_instance.id) vim_info = vnflcm_utils._get_vim(context, vnf_instance.vim_connection_info) @@ -416,8 +417,8 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): vim_info, context) if not heal_vnf_request.vnfc_instance_id: - self._respawn_vnf(context, vnf_instance, vim_connection_info, - heal_vnf_request) + self._respawn_vnf(context, vnf_instance, vnf_dict, + vim_connection_info, heal_vnf_request) else: self._heal_vnf(context, vnf_instance, vim_connection_info, heal_vnf_request) diff --git a/tacker/vnfm/vim_client.py b/tacker/vnfm/vim_client.py index aacf94a..c99b235 100644 --- a/tacker/vnfm/vim_client.py +++ b/tacker/vnfm/vim_client.py @@ -64,7 +64,8 @@ class VimClient(object): vim_res = {'vim_auth': vim_auth, 'vim_id': vim_info['id'], 'vim_name': vim_info.get('name', vim_info['id']), 'vim_type': vim_info['type'], - 'tenant': vim_info['tenant_id']} + 'tenant': vim_info['tenant_id'], + 'placement_attr': vim_info.get('placement_attr', {})} return vim_res @staticmethod