From cb89433bb3f64904c3d71cc20b8114f0459c4562 Mon Sep 17 00:00:00 2001 From: "ma-ooyama.kddi.com" Date: Fri, 24 Feb 2023 04:03:56 +0000 Subject: [PATCH] Support multiple conductors onboarding In current implementation, onboarding process doesn't work when using Tacker as N-Act cluster. This patch enables all conductors in the N-Act cluster to download or delete VNF Package. The functional test for multi-conductors environment will be added in another patch. Co-Authored-By: Hitomi Koba Co-Authored-By: Yukihiro Kinjo Co-Authored-By: Xu Hongjin Implements: blueprint support-multi-conductors-onboarding Change-Id: Iabb100872d0d78bebc29378592dffba8e83710fc --- ...onductors-onboarding-29410991aceecf1d.yaml | 6 + tacker/api/vnfpkgm/v1/controller.py | 16 +- tacker/common/csar_utils.py | 9 +- tacker/conductor/conductor_server.py | 150 ++++++++---- tacker/db/db_sqlalchemy/models.py | 1 + ...fceb25a49_add_downloading_to_vnfpackage.py | 36 +++ .../alembic_migrations/versions/HEAD | 2 +- tacker/objects/vnf_package.py | 9 +- tacker/tests/unit/common/test_csar_utils.py | 7 +- .../unit/conductor/test_conductor_server.py | 229 +++++++++++------- tacker/tests/unit/objects/fakes.py | 3 +- tacker/tests/unit/vnfpkgm/test_controller.py | 4 +- 12 files changed, 327 insertions(+), 145 deletions(-) create mode 100644 releasenotes/notes/support-multi-conductors-onboarding-29410991aceecf1d.yaml create mode 100644 tacker/db/migration/alembic_migrations/versions/34cfceb25a49_add_downloading_to_vnfpackage.py diff --git a/releasenotes/notes/support-multi-conductors-onboarding-29410991aceecf1d.yaml b/releasenotes/notes/support-multi-conductors-onboarding-29410991aceecf1d.yaml new file mode 100644 index 000000000..bb95f13c4 --- /dev/null +++ b/releasenotes/notes/support-multi-conductors-onboarding-29410991aceecf1d.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + VNF package management APIs in multiple conductors environment + are supported that includes Upload VNF Package from content, + Upload VNF Package from uri and Delete VNF Package. diff --git a/tacker/api/vnfpkgm/v1/controller.py b/tacker/api/vnfpkgm/v1/controller.py index 4ce28e7b9..ec292afc1 100644 --- a/tacker/api/vnfpkgm/v1/controller.py +++ b/tacker/api/vnfpkgm/v1/controller.py @@ -223,8 +223,20 @@ class VnfPkgmController(wsgi.Controller): "operational_state": vnf_package.operational_state, "usage_state": vnf_package.usage_state}) - # Delete vnf_package - self.rpc_api.delete_vnf_package(context, vnf_package) + # Controller deletes vnf_package just from DB here. + # Related objects in Legacy DB are also deleted. + # Actual vnf_packages that is in backend storage + # or each conductors' local filesystems are deleted after. + if vnf_package.vnfd is not None: + objects.VnfdAttribute(context).delete(vnf_package.vnfd.vnfd_id) + objects.Vnfd(context).delete(vnf_package.vnfd.vnfd_id) + vnf_package.destroy(context) + + if vnf_package.onboarding_state == ( + fields.PackageOnboardingStateType.ONBOARDED): + # send rpc message for deleting actual vnf_package + # to conductors + self.rpc_api.delete_vnf_package(context, vnf_package) @wsgi.response(http_client.ACCEPTED) @wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND, diff --git a/tacker/common/csar_utils.py b/tacker/common/csar_utils.py index 2443d7cd8..72348d5ae 100644 --- a/tacker/common/csar_utils.py +++ b/tacker/common/csar_utils.py @@ -567,14 +567,9 @@ def delete_csar_data(package_uuid): csar_path = os.path.join(CONF.vnf_package.vnf_package_csar_path, package_uuid + ".zip") - try: - shutil.rmtree(csar_zip_temp_path) + shutil.rmtree(csar_zip_temp_path, ignore_errors=True) + if os.path.isfile(csar_path): os.remove(csar_path) - except OSError as exc: - exc_message = encodeutils.exception_to_unicode(exc) - msg = _('Failed to delete csar folder: ' - '%(csar_path)s, Error: %(exc)s') - LOG.error(msg, {'csar_path': csar_path, 'exc': exc_message}) class PreserveZipFilePermissions(zipfile.ZipFile): diff --git a/tacker/conductor/conductor_server.py b/tacker/conductor/conductor_server.py index d8e0bd46f..4d20bc303 100644 --- a/tacker/conductor/conductor_server.py +++ b/tacker/conductor/conductor_server.py @@ -46,6 +46,7 @@ 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 rpc from tacker.common import safe_utils from tacker.common import topics from tacker.common import utils @@ -59,6 +60,7 @@ from tacker import objects from tacker.objects import fields from tacker.objects.fields import ErrorPoint as EP from tacker.objects.vnf_lcm_subscriptions import LccnSubscriptionRequest +from tacker.objects import vnf_package as vnf_package_obj 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 @@ -115,14 +117,6 @@ def config_opts(): return [('keystone_authtoken', OPTS)] -def _delete_csar(context, vnf_package): - # Delete from glance store - glance_store.delete_csar(context, vnf_package.id, - vnf_package.location_glance_store) - - csar_utils.delete_csar_data(vnf_package.id) - - @utils.expects_func_args('vnf_package') def revert_upload_vnf_package(function): """Decorator to revert upload_vnf_package on failure.""" @@ -144,7 +138,17 @@ def revert_upload_vnf_package(function): glance_store.delete_csar(context, vnf_package.id, vnf_package.location_glance_store) - csar_utils.delete_csar_data(vnf_package.id) + target = oslo_messaging.Target( + exchange='tacker', + topic=topics.TOPIC_CONDUCTOR, + fanout=True, + version='1.0') + serializer = objects.base.TackerObjectSerializer() + client = rpc.get_client(target, version_cap=None, + serializer=serializer) + cctxt = client.prepare() + rpc_method = cctxt.cast + rpc_method(context, 'delete_csar', vnf_package=vnf_package) # Delete the vnf_deployment_flavour if created. if vnf_package.vnf_deployment_flavours: @@ -333,6 +337,28 @@ class Conductor(manager.Manager, v2_hook.ConductorV2Hook): def init_host(self): glance_store.initialize_glance_store() self._basic_config_check() + self.init_csar_files() + + def init_csar_files(self): + context = t_context.get_admin_context() + filters = {'field': 'onboarding_state', 'model': 'VnfPackage', + 'value': fields.PackageOnboardingStateType.ONBOARDED, + 'op': '=='} + vnf_packages = vnf_package_obj.VnfPackagesList.get_by_filters( + context, read_deleted='no', filters=filters) + for vnf_package in vnf_packages: + try: + location = vnf_package.location_glance_store + zip_path = os.path.join(CONF.vnf_package.vnf_package_csar_path, + vnf_package.id) + if not os.path.exists(zip_path): + 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)) + except Exception as error_msg: + raise exceptions.TackerException(message=error_msg) def _get_vnf_instance_href(self, vnf_instance_id): return '{endpoint}/vnflcm/v1/vnf_instances/{id}'.format( @@ -457,16 +483,27 @@ class Conductor(manager.Manager, v2_hook.ConductorV2Hook): vnfd_attribute.create() @revert_upload_vnf_package - def upload_vnf_package_content(self, context, vnf_package): - vnf_package.onboarding_state = ( - fields.PackageOnboardingStateType.PROCESSING) - try: - vnf_package.save() + def load_csar_data(self, context, vnf_package): + with context.session.begin(subtransactions=True): + vnf_package = vnf_package_obj.VnfPackage.get_by_id_with_lock( + context, vnf_package.id) + vnf_package.downloading += 1 + context.session.commit() - 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) + 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) + with context.session.begin(subtransactions=True): + vnf_package = vnf_package_obj.VnfPackage.get_by_id_with_lock( + context, vnf_package.id) + vnf_package.downloading -= 1 + context.session.commit() + + vnf_package = vnf_package_obj.VnfPackage.get_by_id( + context, vnf_package.id) + if (vnf_package.downloading == 0) and (vnf_package.onboarding_state + == fields.PackageOnboardingStateType.PROCESSING): self._onboard_vnf_package( context, vnf_package, @@ -479,6 +516,27 @@ class Conductor(manager.Manager, v2_hook.ConductorV2Hook): fields.PackageOperationalStateType.ENABLED) vnf_package.save() + @revert_upload_vnf_package + def upload_vnf_package_content(self, context, vnf_package): + vnf_package.onboarding_state = ( + fields.PackageOnboardingStateType.PROCESSING) + vnf_package.downloading = 0 + + target = oslo_messaging.Target( + exchange='tacker', + topic=topics.TOPIC_CONDUCTOR, + fanout=True, + version='1.0') + + serializer = objects.base.TackerObjectSerializer() + client = rpc.get_client(target, version_cap=None, + serializer=serializer) + cctxt = client.prepare() + rpc_method = cctxt.cast + + try: + vnf_package.save() + rpc_method(context, 'load_csar_data', vnf_package=vnf_package) except Exception as msg: raise Exception(msg) @@ -495,6 +553,7 @@ class Conductor(manager.Manager, v2_hook.ConductorV2Hook): vnf_package.onboarding_state = ( fields.PackageOnboardingStateType.PROCESSING) + vnf_package.downloading = 0 vnf_package.algorithm = CONF.vnf_package.hashing_algorithm vnf_package.hash = multihash @@ -502,36 +561,43 @@ class Conductor(manager.Manager, v2_hook.ConductorV2Hook): vnf_package.size = size vnf_package.save() - 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) + target = oslo_messaging.Target( + exchange='tacker', + topic=topics.TOPIC_CONDUCTOR, + fanout=True, + version='1.0') - self._onboard_vnf_package( - context, - vnf_package, - vnf_data, - flavours, - vnf_artifacts) + serializer = objects.base.TackerObjectSerializer() + client = rpc.get_client(target, version_cap=None, + serializer=serializer) + cctxt = client.prepare() + rpc_method = cctxt.cast - vnf_package.onboarding_state = ( - fields.PackageOnboardingStateType.ONBOARDED) - vnf_package.operational_state = ( - fields.PackageOperationalStateType.ENABLED) + try: + vnf_package.save() + rpc_method(context, 'load_csar_data', vnf_package=vnf_package) + except Exception as msg: + raise Exception(msg) - vnf_package.save() + def delete_csar(self, context, vnf_package): + csar_utils.delete_csar_data(vnf_package.id) def delete_vnf_package(self, context, vnf_package): - if (vnf_package.onboarding_state == - fields.PackageOnboardingStateType.ONBOARDED): + # Delete from glance store + glance_store.delete_csar(context, vnf_package.id, + vnf_package.location_glance_store) - _delete_csar(context, vnf_package) - - # TODO(h-asahina): stop using these Legacy DB - if vnf_package.vnfd is not None: - objects.VnfdAttribute(context).delete(vnf_package.vnfd.vnfd_id) - objects.Vnfd(context).delete(vnf_package.vnfd.vnfd_id) - - vnf_package.destroy(context) + target = oslo_messaging.Target( + exchange='tacker', + topic=topics.TOPIC_CONDUCTOR, + fanout=True, + version='1.0') + serializer = objects.base.TackerObjectSerializer() + client = rpc.get_client(target, version_cap=None, + serializer=serializer) + cctxt = client.prepare() + rpc_method = cctxt.cast + rpc_method(context, 'delete_csar', vnf_package=vnf_package) def get_vnf_package_vnfd(self, context, vnf_package): csar_path = os.path.join(CONF.vnf_package.vnf_package_csar_path, diff --git a/tacker/db/db_sqlalchemy/models.py b/tacker/db/db_sqlalchemy/models.py index a6ab269f1..fa0741437 100644 --- a/tacker/db/db_sqlalchemy/models.py +++ b/tacker/db/db_sqlalchemy/models.py @@ -171,6 +171,7 @@ class VnfPackage(model_base.BASE, models.SoftDeleteMixin, hash = sa.Column(sa.String(128), nullable=True) location_glance_store = sa.Column(sa.Text(), nullable=True) size = sa.Column(sa.BigInteger, nullable=False, default=0) + downloading = sa.Column(sa.Integer, nullable=False, default=0) _metadata = orm.relationship( VnfPackageUserData, diff --git a/tacker/db/migration/alembic_migrations/versions/34cfceb25a49_add_downloading_to_vnfpackage.py b/tacker/db/migration/alembic_migrations/versions/34cfceb25a49_add_downloading_to_vnfpackage.py new file mode 100644 index 000000000..425a9c7f3 --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/34cfceb25a49_add_downloading_to_vnfpackage.py @@ -0,0 +1,36 @@ +# Copyright 2023 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""add_downloading_to_vnfPackage + +Revision ID: 34cfceb25a49 +Revises: 2531c976c0f1 +Create Date: 2023-03-08 01:56:40.134793 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '34cfceb25a49' +down_revision = '2531c976c0f1' + + +def upgrade(active_plugins=None, options=None): + op.add_column('vnf_packages', sa.Column('downloading', sa.Integer(), + nullable=False, + server_default='0')) diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index 6cedf4dc8..603928232 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -2531c976c0f1 +34cfceb25a49 \ No newline at end of file diff --git a/tacker/objects/vnf_package.py b/tacker/objects/vnf_package.py index 287dfed2f..56ac44163 100644 --- a/tacker/objects/vnf_package.py +++ b/tacker/objects/vnf_package.py @@ -380,7 +380,8 @@ class VnfPackage(base.TackerObject, base.TackerPersistentObject, 'vnfd': fields.ObjectField('VnfPackageVnfd', nullable=True), 'size': fields.IntegerField(nullable=False, default=0), 'vnf_artifacts': fields.ObjectField('VnfPackageArtifactInfoList', - nullable=True) + nullable=True), + 'downloading': fields.IntegerField(nullable=False, default=0) } def __init__(self, context=None, **kwargs): @@ -569,6 +570,12 @@ class VnfPackage(base.TackerObject, base.TackerPersistentObject, return cls._from_db_object(context, cls(), db_vnf_package, expected_attrs=expected_attrs) + @base.remotable_classmethod + def get_by_id_with_lock(cls, context, id): + vnf_package = context.session.query(models.VnfPackage).filter( + models.VnfPackage.id == id).with_for_update().one() + return vnf_package + @base.remotable def destroy(self, context): if not self.obj_attr_is_set('id'): diff --git a/tacker/tests/unit/common/test_csar_utils.py b/tacker/tests/unit/common/test_csar_utils.py index 7255a4a1b..3e083e5b6 100644 --- a/tacker/tests/unit/common/test_csar_utils.py +++ b/tacker/tests/unit/common/test_csar_utils.py @@ -384,9 +384,14 @@ class TestCSARUtils(testtools.TestCase): self.assertEqual("No VNF flavours are available", exc.format_message()) + @mock.patch.object(os.path, 'isfile') + @mock.patch.object(os.path, 'exists') @mock.patch.object(os, 'remove') @mock.patch.object(shutil, 'rmtree') - def test_delete_csar_data(self, mock_rmtree, mock_remove): + def test_delete_csar_data(self, mock_rmtree, + mock_remove, mock_exists, mock_isfile): + mock_exists.return_value = True + mock_isfile.return_value = True csar_utils.delete_csar_data(constants.UUID) mock_rmtree.assert_called() mock_remove.assert_called() diff --git a/tacker/tests/unit/conductor/test_conductor_server.py b/tacker/tests/unit/conductor/test_conductor_server.py index 6666bffb6..f7276a8f3 100644 --- a/tacker/tests/unit/conductor/test_conductor_server.py +++ b/tacker/tests/unit/conductor/test_conductor_server.py @@ -37,13 +37,12 @@ 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.rpc import BackingOffClient from tacker.conductor import conductor_server import tacker.conf from tacker import context from tacker import context as t_context -from tacker.db.db_sqlalchemy import api from tacker.db.db_sqlalchemy import models -from tacker.db.vnfm import vnfm_db from tacker.glance_store import store as glance_store from tacker import objects from tacker.objects import fields @@ -106,6 +105,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): self.vnfd_pkg_data = self._create_vnfd_pkg_data() cfg.CONF.set_override('retry_wait', 0, group='vnf_lcm') self.addCleanup(cfg.CONF.clear_override, 'retry_wait', group='vnf_lcm') + self.cctxt_mock = mock.MagicMock() def _mock_vnfm_plugin(self): self.vnfm_plugin = mock.Mock(wraps=FakeVNFMPlugin()) @@ -204,35 +204,15 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): "Authorization") self.assertEqual("Bearer " + expected_token, actual_auth) - @mock.patch.object(conductor_server.Conductor, '_onboard_vnf_package') - @mock.patch.object(conductor_server, 'revert_upload_vnf_package') - @mock.patch.object(csar_utils, 'load_csar_data') - @mock.patch.object(glance_store, 'load_csar') - def test_upload_vnf_package_content(self, mock_load_csar, - mock_load_csar_data, - mock_revert, mock_onboard): - mock_load_csar_data.return_value = (mock.ANY, mock.ANY, mock.ANY) - mock_load_csar.return_value = '/var/lib/tacker/5f5d99c6-844a-4c3' \ - '1-9e6d-ab21b87dcfff.zip' - self.conductor.upload_vnf_package_content( - self.context, self.vnf_package) - mock_load_csar.assert_called() - mock_load_csar_data.assert_called() - mock_onboard.assert_called() - - @mock.patch.object(conductor_server.Conductor, '_onboard_vnf_package') + @mock.patch.object(BackingOffClient, 'prepare') @mock.patch.object(glance_store, 'store_csar') @mock.patch.object(conductor_server, 'revert_upload_vnf_package') - @mock.patch.object(csar_utils, 'load_csar_data') - @mock.patch.object(glance_store, 'load_csar') - def test_upload_vnf_package_from_uri(self, mock_load_csar, - mock_load_csar_data, - mock_revert, mock_store, - mock_onboard): + def test_upload_vnf_package_from_uri(self, + mock_revert, + mock_store, + mock_prepare): + mock_prepare.return_value = self.cctxt_mock address_information = "http://test.zip" - mock_load_csar_data.return_value = (mock.ANY, mock.ANY, mock.ANY) - mock_load_csar.return_value = '/var/lib/tacker/5f5d99c6-844a' \ - '-4c31-9e6d-ab21b87dcfff.zip' mock_store.return_value = 'location', 0, 'checksum',\ 'multihash', 'loc_meta' self.conductor.upload_vnf_package_from_uri(self.context, @@ -240,58 +220,54 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): address_information, user_name=None, password=None) - mock_load_csar.assert_called() - mock_load_csar_data.assert_called() mock_store.assert_called() - mock_onboard.assert_called() + mock_prepare.assert_called() + self.cctxt_mock.cast.assert_called_once_with( + self.context, 'load_csar_data', vnf_package=self.vnf_package) self.assertEqual('multihash', self.vnf_package.hash) self.assertEqual('location', self.vnf_package.location_glance_store) + @mock.patch.object(csar_utils, 'load_csar_data') + @mock.patch.object(glance_store, 'load_csar') + @mock.patch.object(objects.vnf_package.VnfPackagesList, 'get_by_filters') + def test_init_csar_files(self, mock_pkg_list, + mock_load_csar, mock_load_csar_data): + fake_csar = os.path.join(self.temp_dir, self.vnf_package.id) + self.vnf_package.location = fake_csar + mock_pkg_list.return_value = [self.vnf_package] + mock_load_csar.return_value = True + mock_load_csar_data.return_value = (mock.ANY, mock.ANY, mock.ANY) + + self.conductor.init_csar_files() + + mock_load_csar.assert_called() + mock_load_csar_data.assert_called() + + @mock.patch.object(csar_utils, 'load_csar_data') + @mock.patch.object(glance_store, 'load_csar') + @mock.patch.object(objects.vnf_package.VnfPackagesList, 'get_by_filters') + def test_init_csar_files_exception(self, mock_pkg_list, + mock_load_csar, mock_load_csar_data): + fake_csar = os.path.join(self.temp_dir, self.vnf_package.id) + self.vnf_package.location = fake_csar + mock_pkg_list.return_value = [self.vnf_package] + mock_load_csar.return_value = True + mock_load_csar_data.side_effect = exceptions.InvalidCSAR + + self.assertRaises(exceptions.TackerException, + self.conductor.init_csar_files) + + @mock.patch.object(BackingOffClient, 'prepare') @mock.patch.object(glance_store, 'delete_csar') - def test_delete_vnf_package(self, mock_delete_csar): + def test_delete_vnf_package(self, mock_delete_csar, mock_prepare): self.vnf_package.onboarding_state = 'ONBOARDED' - vnfd_id = uuidsentinel.vnfd_id - - # create VnfPackageVnfd - vnf_pack_vnfd_data = fake_obj.get_vnf_package_vnfd_data( - self.vnf_package.id, vnfd_id) - vnf_pack_vnfd = objects.VnfPackageVnfd( - self.context, **vnf_pack_vnfd_data) - vnf_pack_vnfd.create() - self.vnf_package.vnfd = vnf_pack_vnfd - - # create Legacy Vnfd - vnfd = objects.Vnfd(self.context) - vnfd.id = vnfd_id - vnfd.tenant_id = uuidsentinel.tenant_id - vnfd.name = 'dummy_name' - vnfd.create() - - # create Legacy VnfdAttribute - vnfd_attr = objects.VnfdAttribute(self.context) - vnfd_attr.id = uuidsentinel.vnfd_attr_id - vnfd_attr.vnfd_id = vnfd_id - vnfd_attr.key = 'dummy_key' - vnfd_attr.create() - - # check Vnfd and VnfdAttribute were created - self.assertIsNotNone( - api.model_query(self.context, vnfm_db.VNFDAttribute) - .filter_by(vnfd_id=vnfd_id).first()) - self.assertIsNotNone( - api.model_query(self.context, vnfm_db.VNFD) - .filter_by(id=vnfd_id).first()) - + mock_prepare.return_value = self.cctxt_mock self.conductor.delete_vnf_package(self.context, self.vnf_package) - # check Vnfd and VnfdAttribute were deleted mock_delete_csar.assert_called() - self.assertIsNone( - api.model_query(self.context, vnfm_db.VNFDAttribute) - .filter_by(vnfd_id=vnfd_id).first()) - self.assertIsNone( - api.model_query(self.context, vnfm_db.VNFD) - .filter_by(id=vnfd_id).first()) + mock_prepare.assert_called() + self.cctxt_mock.cast.assert_called_once_with( + self.context, 'delete_csar', vnf_package=self.vnf_package) def test_get_vnf_package_vnfd_with_tosca_meta_file_in_csar(self): fake_csar = fakes.create_fake_csar_dir(self.vnf_package.id, @@ -3497,37 +3473,112 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase): self.vnfd_pkg_data, vnfd_id) + @mock.patch.object(BackingOffClient, 'prepare') + @mock.patch.object(objects.vnf_package.VnfPackage, 'save') + @mock.patch.object(glance_store, 'delete_csar') @mock.patch.object(csar_utils, 'load_csar_data') @mock.patch.object(glance_store, 'load_csar') - @mock.patch.object(glance_store, 'delete_csar') - def test_revert_upload_vnf_package_in_upload_vnf_package_content(self, - mock_load_csar, - mock_load_csar_data, - mock_delete_csar): - mock_load_csar_data.return_value = (mock.ANY, mock.ANY, mock.ANY) + def test_revert_upload_vnf_package_in_load_csar_data(self, + mock_load_csar, + mock_load_csar_data, + mock_delete_csar, + mock_save, + mock_prepare): + self.vnf_package.onboarding_state = "PROCESSING" + self.vnf_package.save() mock_load_csar.return_value = '/var/lib/tacker/5f5d99c6-844a-4c3' \ '1-9e6d-ab21b87dcfff.zip' - mock_delete_csar.return_value = "" - self.assertRaisesRegex(Exception, - "not enough values to unpack", - self.conductor.upload_vnf_package_content, - self.context, self.vnf_package) + mock_load_csar_data.side_effect = exceptions.InvalidCSAR + mock_prepare.return_value = self.cctxt_mock - @mock.patch.object(conductor_server, 'revert_upload_vnf_package') + self.assertRaises(exceptions.InvalidCSAR, + self.conductor.load_csar_data, + self.context, self.vnf_package) + mock_delete_csar.assert_called() + mock_prepare.assert_called() + self.cctxt_mock.cast.assert_called_once_with( + self.context, 'delete_csar', vnf_package=self.vnf_package) + self.assertEqual(self.vnf_package.onboarding_state, "CREATED") + + @mock.patch.object(conductor_server.Conductor, '_onboard_vnf_package') @mock.patch.object(csar_utils, 'load_csar_data') @mock.patch.object(glance_store, 'load_csar') - def test_onboard_vnf_package_through_upload_vnf_package_content(self, - mock_load_csar, - mock_load_csar_data, - mock_revert): + def test_load_csar_data(self, mock_load_csar, + mock_load_csar_data, mock_onboard): + self.vnf_package.onboarding_state = "PROCESSING" + self.vnf_package.save() vnf_data = fakes.get_vnf_package_vnfd() vnf_data = self._rename_vnfdata_keys(vnf_data) mock_load_csar_data.return_value = (vnf_data, [], []) mock_load_csar.return_value = '/var/lib/tacker/5f5d99c6-844a-4c3' \ '1-9e6d-ab21b87dcfff.zip' - fake_context = context.Context(tenant_id=uuidsentinel.tenant_id) + self.conductor.load_csar_data(self.context, self.vnf_package) + mock_load_csar.assert_called() + mock_load_csar_data.assert_called() + mock_onboard.assert_called() + + @mock.patch.object(conductor_server.Conductor, '_onboard_vnf_package') + @mock.patch.object(csar_utils, 'load_csar_data') + @mock.patch.object(glance_store, 'load_csar') + def test_load_csar_data_with_onboarding_state_created(self, mock_load_csar, + mock_load_csar_data, mock_onboard): + self.vnf_package.onboarding_state = "PROCESSING" + self.vnf_package.save() + vnf_data = fakes.get_vnf_package_vnfd() + vnf_data = self._rename_vnfdata_keys(vnf_data) + mock_load_csar_data.return_value = (vnf_data, [], []) + + def f(): + vnf_pkg = objects.VnfPackage.get_by_id( + self.context, self.vnf_package.id) + vnf_pkg.onboarding_state = "CREATED" + vnf_pkg.save() + return (mock.ANY, mock.ANY, mock.ANY) + + mock_load_csar_data.return_value = f() + mock_load_csar.return_value = '/var/lib/tacker/5f5d99c6-844a-4c3' \ + '1-9e6d-ab21b87dcfff.zip' + self.conductor.load_csar_data(self.context, self.vnf_package) + + mock_load_csar.assert_called() + mock_load_csar_data.assert_called() + vnf_pkg = objects.VnfPackage.get_by_id( + self.context, self.vnf_package.id) + self.assertEqual(vnf_pkg.onboarding_state, "CREATED") + mock_onboard.assert_not_called() + + @mock.patch.object(conductor_server.Conductor, '_onboard_vnf_package') + @mock.patch.object(csar_utils, 'load_csar_data') + @mock.patch.object(glance_store, 'load_csar') + def test_load_csar_data_downloading_not_completed(self, mock_load_csar, + mock_load_csar_data, mock_onboard): + self.vnf_package.onboarding_state = "PROCESSING" + self.vnf_package.downloading = 1 + self.vnf_package.save() + vnf_data = fakes.get_vnf_package_vnfd() + vnf_data = self._rename_vnfdata_keys(vnf_data) + mock_load_csar_data.return_value = (vnf_data, [], []) + mock_load_csar.return_value = '/var/lib/tacker/5f5d99c6-844a-4c3' \ + '1-9e6d-ab21b87dcfff.zip' + self.conductor.load_csar_data(self.context, self.vnf_package) + mock_load_csar.assert_called() + mock_load_csar_data.assert_called() + mock_onboard.assert_not_called() + vnf_pkg = objects.VnfPackage.get_by_id( + self.context, self.vnf_package.id) + self.assertEqual(vnf_pkg.onboarding_state, "PROCESSING") + self.assertEqual(vnf_pkg.downloading, 1) + + @mock.patch.object(BackingOffClient, 'prepare') + def test_onboard_vnf_package_through_upload_vnf_package_content( + self, mock_prepare): + mock_prepare.return_value = self.cctxt_mock self.conductor.upload_vnf_package_content( - fake_context, self.vnf_package) + self.context, self.vnf_package) + mock_prepare.assert_called() + self.cctxt_mock.cast.assert_called_once_with( + self.context, 'load_csar_data', + vnf_package=self.vnf_package) def _rename_vnfdata_keys(self, vnf_data): vnf_data["descriptor_id"] = vnf_data.pop("id") diff --git a/tacker/tests/unit/objects/fakes.py b/tacker/tests/unit/objects/fakes.py index 932af2e11..e33833888 100644 --- a/tacker/tests/unit/objects/fakes.py +++ b/tacker/tests/unit/objects/fakes.py @@ -31,7 +31,8 @@ vnf_package_data = {'algorithm': None, 'hash': None, 'created_at': datetime.datetime( 2019, 8, 8, 0, 0, 0, tzinfo=iso8601.UTC), 'deleted': False, - 'size': 0 + 'size': 0, + "downloading": 0 } software_image = { diff --git a/tacker/tests/unit/vnfpkgm/test_controller.py b/tacker/tests/unit/vnfpkgm/test_controller.py index 138816e72..e2676adb6 100644 --- a/tacker/tests/unit/vnfpkgm/test_controller.py +++ b/tacker/tests/unit/vnfpkgm/test_controller.py @@ -647,9 +647,11 @@ class TestController(base.TestCase): if expected_result_link is not None: self.assertEqual(expected_result_link, res_dict.headers['Link']) + @mock.patch.object(vnf_package.VnfPackage, "destroy") @mock.patch.object(vnf_package.VnfPackage, "get_by_id") @mock.patch.object(VNFPackageRPCAPI, "delete_vnf_package") - def test_delete_with_204_status(self, mock_delete_rpc, mock_vnf_by_id): + def test_delete_with_204_status(self, mock_delete_rpc, + mock_vnf_by_id, mock_vnf_pack_destroy): vnfpkg_updates = {'operational_state': 'DISABLED'} mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj( vnf_package_updates=vnfpkg_updates)