From fbb38266d6a5efbb4eb09af30cc1ec71bb4bc5e5 Mon Sep 17 00:00:00 2001 From: Ajay Parja Date: Thu, 5 Dec 2019 07:47:47 +0000 Subject: [PATCH] Add terminate vnf instance API Implemented terminate vnf instance API. * GET /vnflcm/v1/vnf_instances/{vnf_instance_id}/terminate Co-Authored-By: tpatil Co-Authored-By: Shubham Potale Co-Authored-By: Sameer Thakur Change-Id: I69b7ef12038aa410db7e50671df2931beb761223 Blueprint: support-etsi-nfv-specs --- tacker/api/schemas/vnf_lcm.py | 11 ++ tacker/api/vnflcm/v1/controller.py | 23 ++- tacker/conductor/conductor_server.py | 14 ++ tacker/conductor/conductorrpc/vnf_lcm_rpc.py | 11 ++ tacker/objects/__init__.py | 1 + tacker/objects/fields.py | 11 ++ tacker/objects/terminate_vnf_req.py | 54 +++++++ tacker/objects/vnf_instantiated_info.py | 11 ++ tacker/policies/vnf_lcm.py | 11 ++ .../unit/conductor/test_conductor_server.py | 99 +++++++++++- .../objects/test_terminate_vnf_request.py | 51 ++++++ tacker/tests/unit/vnflcm/test_controller.py | 140 ++++++++++++++++ .../tests/unit/vnflcm/test_vnflcm_driver.py | 111 ++++++++++++- tacker/vnflcm/abstract_driver.py | 11 ++ tacker/vnflcm/vnflcm_driver.py | 153 ++++++++++++++++++ 15 files changed, 704 insertions(+), 8 deletions(-) create mode 100644 tacker/objects/terminate_vnf_req.py create mode 100644 tacker/tests/unit/objects/test_terminate_vnf_request.py diff --git a/tacker/api/schemas/vnf_lcm.py b/tacker/api/schemas/vnf_lcm.py index 3d9fafa07..9356737c1 100644 --- a/tacker/api/schemas/vnf_lcm.py +++ b/tacker/api/schemas/vnf_lcm.py @@ -199,3 +199,14 @@ instantiate = { 'required': ['flavourId'], 'additionalProperties': False, } + +terminate = { + 'type': 'object', + 'properties': { + 'terminationType': {'type': 'string', + 'enum': ['FORCEFUL', 'GRACEFUL']}, + 'gracefulTerminationTimeout': {'type': 'integer', 'minimum': 0} + }, + 'required': ['terminationType'], + 'additionalProperties': False, +} diff --git a/tacker/api/vnflcm/v1/controller.py b/tacker/api/vnflcm/v1/controller.py index ffe383a23..4648b5040 100644 --- a/tacker/api/vnflcm/v1/controller.py +++ b/tacker/api/vnflcm/v1/controller.py @@ -245,8 +245,29 @@ class VnfLcmController(wsgi.Controller): self._instantiate(context, vnf_instance, body) + @check_vnf_state(action="terminate", + instantiation_state=[fields.VnfInstanceState.INSTANTIATED], + task_state=[None]) + def _terminate(self, context, vnf_instance, request_body): + req_body = utils.convert_camelcase_to_snakecase(request_body) + terminate_vnf_req = \ + objects.TerminateVnfRequest.obj_from_primitive( + req_body, context=context) + + vnf_instance.task_state = fields.VnfInstanceTaskState.TERMINATING + vnf_instance.save() + self.rpc_api.terminate(context, vnf_instance, terminate_vnf_req) + + @wsgi.response(http_client.ACCEPTED) + @wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN, + http_client.NOT_FOUND, http_client.CONFLICT)) + @validation.schema(vnf_lcm.terminate) def terminate(self, request, id, body): - raise webob.exc.HTTPNotImplemented() + context = request.environ['tacker.context'] + context.can(vnf_lcm_policies.VNFLCM % 'terminate') + + vnf_instance = self._get_vnf_instance(context, id) + self._terminate(context, vnf_instance, body) def heal(self, request, id, body): raise webob.exc.HTTPNotImplemented() diff --git a/tacker/conductor/conductor_server.py b/tacker/conductor/conductor_server.py index c6408e53c..ce1aabb4a 100644 --- a/tacker/conductor/conductor_server.py +++ b/tacker/conductor/conductor_server.py @@ -424,6 +424,20 @@ class Conductor(manager.Manager): vnf_package.save() + def terminate(self, context, vnf_instance, terminate_vnf_req): + self.vnflcm_driver.terminate_vnf(context, vnf_instance, + terminate_vnf_req) + + 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: + LOG.error("Failed to update usage_state of vnf package %s", + vnf_package.id) + def init(args, **kwargs): CONF(args=args, project='tacker', diff --git a/tacker/conductor/conductorrpc/vnf_lcm_rpc.py b/tacker/conductor/conductorrpc/vnf_lcm_rpc.py index 7a109d88c..1a934555d 100644 --- a/tacker/conductor/conductorrpc/vnf_lcm_rpc.py +++ b/tacker/conductor/conductorrpc/vnf_lcm_rpc.py @@ -38,3 +38,14 @@ class VNFLcmRPCAPI(object): return rpc_method(context, 'instantiate', vnf_instance=vnf_instance, instantiate_vnf=instantiate_vnf) + + def terminate(self, context, vnf_instance, terminate_vnf_req, cast=True): + serializer = objects_base.TackerObjectSerializer() + + client = rpc.get_client(self.target, version_cap=None, + serializer=serializer) + cctxt = client.prepare() + rpc_method = cctxt.cast if cast else cctxt.call + return rpc_method(context, 'terminate', + vnf_instance=vnf_instance, + terminate_vnf_req=terminate_vnf_req) diff --git a/tacker/objects/__init__.py b/tacker/objects/__init__.py index d94bf6cd3..6e9278ab9 100644 --- a/tacker/objects/__init__.py +++ b/tacker/objects/__init__.py @@ -34,3 +34,4 @@ def register_all(): __import__('tacker.objects.vim_connection') __import__('tacker.objects.instantiate_vnf_req') __import__('tacker.objects.vnf_resources') + __import__('tacker.objects.terminate_vnf_req') diff --git a/tacker/objects/fields.py b/tacker/objects/fields.py index bf1f38289..a780d2595 100644 --- a/tacker/objects/fields.py +++ b/tacker/objects/fields.py @@ -166,3 +166,14 @@ class IpAddressType(BaseTackerEnum): class IpAddressTypeField(BaseEnumField): AUTO_TYPE = IpAddressType() + + +class VnfInstanceTerminationType(BaseTackerEnum): + FORCEFUL = 'FORCEFUL' + GRACEFUL = 'GRACEFUL' + + ALL = (FORCEFUL, GRACEFUL) + + +class VnfInstanceTerminationTypeField(BaseEnumField): + AUTO_TYPE = VnfInstanceTerminationType() diff --git a/tacker/objects/terminate_vnf_req.py b/tacker/objects/terminate_vnf_req.py new file mode 100644 index 000000000..85612c83c --- /dev/null +++ b/tacker/objects/terminate_vnf_req.py @@ -0,0 +1,54 @@ +# Copyright (C) 2020 NTT DATA +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging + +from tacker.objects import base +from tacker.objects import fields + +LOG = logging.getLogger(__name__) + + +@base.TackerObjectRegistry.register +class TerminateVnfRequest(base.TackerObject, base.TackerPersistentObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'termination_type': fields.VnfInstanceTerminationTypeField( + nullable=False), + 'graceful_termination_timeout': fields.IntegerField(nullable=True, + default=0) + } + + @classmethod + def obj_from_primitive(cls, primitive, context): + if 'tacker_object.name' in primitive: + obj_terminate_vnf_req = super( + TerminateVnfRequest, cls).obj_from_primitive( + primitive, context) + else: + obj_terminate_vnf_req = TerminateVnfRequest._from_dict(primitive) + + return obj_terminate_vnf_req + + @classmethod + def _from_dict(cls, data_dict): + termination_type = data_dict.get('termination_type') + graceful_termination_timeout = \ + data_dict.get('graceful_termination_timeout', 0) + + return cls(termination_type=termination_type, + graceful_termination_timeout=graceful_termination_timeout) diff --git a/tacker/objects/vnf_instantiated_info.py b/tacker/objects/vnf_instantiated_info.py index 1f76dad15..2807f79e8 100644 --- a/tacker/objects/vnf_instantiated_info.py +++ b/tacker/objects/vnf_instantiated_info.py @@ -293,6 +293,17 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat, return data + def reinitialize(self): + # Reinitialize vnf to non instantiated state. + self.ext_cp_info = [] + self.ext_virtual_link_info = [] + self.ext_managed_virtual_link_info = [] + self.vnfc_resource_info = [] + self.vnf_virtual_link_resource_info = [] + self.virtual_storage_resource_info = [] + self.instance_id = None + self.vnf_state = fields.VnfOperationalStateType.STOPPED + @base.TackerObjectRegistry.register class VnfExtCpInfo(base.TackerObject, base.TackerObjectDictCompat, diff --git a/tacker/policies/vnf_lcm.py b/tacker/policies/vnf_lcm.py index 631bf1df7..caec50577 100644 --- a/tacker/policies/vnf_lcm.py +++ b/tacker/policies/vnf_lcm.py @@ -55,6 +55,17 @@ rules = [ } ] ), + policy.DocumentedRuleDefault( + name=VNFLCM % 'terminate', + check_str=base.RULE_ADMIN_OR_OWNER, + description="Terminate a VNF instance.", + operations=[ + { + 'method': 'POST', + 'path': '/vnflcm/v1/vnf_instances/{vnfInstanceId}/terminate' + } + ] + ) ] diff --git a/tacker/tests/unit/conductor/test_conductor_server.py b/tacker/tests/unit/conductor/test_conductor_server.py index 776f8219a..5e050b36e 100644 --- a/tacker/tests/unit/conductor/test_conductor_server.py +++ b/tacker/tests/unit/conductor/test_conductor_server.py @@ -29,7 +29,7 @@ import tacker.conf from tacker import context from tacker.glance_store import store as glance_store from tacker import objects -from tacker.objects import vnf_package +from tacker.objects import fields from tacker.tests.unit.conductor import fakes from tacker.tests.unit.db.base import SqlTestCase from tacker.tests.unit.objects import fakes as fake_obj @@ -73,8 +73,8 @@ class TestConductor(SqlTestCase): 'tacker.vnflcm.vnflcm_driver.VnfLcmDriver', fake_vnflcm_driver) def _create_vnf_package(self): - vnfpkgm = vnf_package.VnfPackage(context=self.context, - **fakes.VNF_PACKAGE_DATA) + vnfpkgm = objects.VnfPackage(context=self.context, + **fakes.VNF_PACKAGE_DATA) vnfpkgm.create() return vnfpkgm @@ -251,10 +251,101 @@ class TestConductor(SqlTestCase): mock_log.error.assert_called_once_with(expected_log, vnf_package_vnfd.package_uuid) + @mock.patch.object(objects.VnfPackage, 'is_package_in_use') + def test_terminate_vnf_instance(self, mock_package_in_use): + 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() + + terminate_vnf_req = objects.TerminateVnfRequest( + termination_type=fields.VnfInstanceTerminationType.GRACEFUL) + + self.conductor.terminate(self.context, vnf_instance, + terminate_vnf_req) + + self.vnflcm_driver.terminate_vnf.assert_called_once_with( + self.context, vnf_instance, terminate_vnf_req) + mock_package_in_use.assert_called_once() + + @mock.patch.object(objects.VnfPackage, 'is_package_in_use') + def test_terminate_vnf_instance_with_usage_state_not_in_use(self, + mock_vnf_package_is_package_in_use): + 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.INSTANTIATED + vnf_instance = objects.VnfInstance(context=self.context, + **vnf_instance_data) + vnf_instance.create() + + mock_vnf_package_is_package_in_use.return_value = False + terminate_vnf_req = objects.TerminateVnfRequest( + termination_type=fields.VnfInstanceTerminationType.GRACEFUL) + + self.conductor.terminate(self.context, vnf_instance, + terminate_vnf_req) + + self.vnflcm_driver.terminate_vnf.assert_called_once_with( + self.context, vnf_instance, terminate_vnf_req) + mock_vnf_package_is_package_in_use.assert_called_once() + + @mock.patch.object(objects.VnfPackage, 'is_package_in_use') + def test_terminate_vnf_instance_with_usage_state_already_in_use(self, + mock_vnf_package_is_package_in_use): + 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.INSTANTIATED + vnf_instance = objects.VnfInstance(context=self.context, + **vnf_instance_data) + vnf_instance.create() + + mock_vnf_package_is_package_in_use.return_value = True + terminate_vnf_req = objects.TerminateVnfRequest( + termination_type=fields.VnfInstanceTerminationType.GRACEFUL) + + self.conductor.terminate(self.context, vnf_instance, + terminate_vnf_req) + + self.vnflcm_driver.terminate_vnf.assert_called_once_with( + self.context, vnf_instance, terminate_vnf_req) + mock_vnf_package_is_package_in_use.assert_called_once() + + @mock.patch.object(objects.VnfPackage, 'is_package_in_use') + @mock.patch('tacker.conductor.conductor_server.LOG') + def test_terminate_vnf_instance_failed_to_update_usage_state( + self, mock_log, mock_is_package_in_use): + 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.INSTANTIATED + vnf_instance = objects.VnfInstance(context=self.context, + **vnf_instance_data) + vnf_instance.create() + terminate_vnf_req = objects.TerminateVnfRequest( + termination_type=fields.VnfInstanceTerminationType.GRACEFUL) + mock_is_package_in_use.side_effect = Exception + self.conductor.terminate(self.context, vnf_instance, + terminate_vnf_req) + self.vnflcm_driver.terminate_vnf.assert_called_once_with( + self.context, vnf_instance, terminate_vnf_req) + expected_msg = "Failed to update usage_state of vnf package %s" + mock_log.error.assert_called_once_with(expected_msg, + vnf_package_vnfd.package_uuid) + @mock.patch.object(os, 'remove') @mock.patch.object(shutil, 'rmtree') @mock.patch.object(os.path, 'exists') - @mock.patch.object(vnf_package.VnfPackagesList, 'get_by_filters') + @mock.patch.object(objects.VnfPackagesList, 'get_by_filters') def test_run_cleanup_vnf_packages(self, mock_get_by_filter, mock_exists, mock_rmtree, mock_remove): diff --git a/tacker/tests/unit/objects/test_terminate_vnf_request.py b/tacker/tests/unit/objects/test_terminate_vnf_request.py new file mode 100644 index 000000000..16ae93efa --- /dev/null +++ b/tacker/tests/unit/objects/test_terminate_vnf_request.py @@ -0,0 +1,51 @@ +# Copyright (C) 2020 NTT DATA +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tacker import context +from tacker import objects +from tacker.tests.unit.db.base import SqlTestCase + + +class TestTerminateVnfRequest(SqlTestCase): + + def setUp(self): + super(TestTerminateVnfRequest, self).setUp() + self.context = context.get_admin_context() + + def _get_terminate_vnf_request(self): + terminate_vnf_request = { + 'termination_type': 'GRACEFUL', + 'graceful_termination_timeout': 10 + } + return terminate_vnf_request + + def test_obj_from_primitive(self): + terminate_vnf_request = self._get_terminate_vnf_request() + result = objects.TerminateVnfRequest.obj_from_primitive( + terminate_vnf_request, self.context) + self.assertTrue(isinstance(result, objects.TerminateVnfRequest)) + self.assertEqual('GRACEFUL', result.termination_type) + self.assertEqual(terminate_vnf_request['graceful_termination_timeout'], + result.graceful_termination_timeout) + + def test_obj_from_primitive_without_timeout(self): + terminate_vnf_request = self._get_terminate_vnf_request() + terminate_vnf_request.pop('graceful_termination_timeout') + + result = objects.TerminateVnfRequest.obj_from_primitive( + terminate_vnf_request, self.context) + self.assertTrue(isinstance(result, objects.TerminateVnfRequest)) + self.assertEqual('GRACEFUL', result.termination_type) + self.assertEqual(0, result.graceful_termination_timeout) diff --git a/tacker/tests/unit/vnflcm/test_controller.py b/tacker/tests/unit/vnflcm/test_controller.py index 460c4c8c1..9f170c0f6 100644 --- a/tacker/tests/unit/vnflcm/test_controller.py +++ b/tacker/tests/unit/vnflcm/test_controller.py @@ -695,3 +695,143 @@ class TestController(base.TestCase): resp = req.get_response(self.app) self.assertEqual(http_client.METHOD_NOT_ALLOWED, resp.status_code) + + @mock.patch.object(objects.VnfInstance, "get_by_id") + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(VNFLcmRPCAPI, "terminate") + @ddt.data({'terminationType': 'FORCEFUL'}, + {'terminationType': 'GRACEFUL'}, + {'terminationType': 'GRACEFUL', + 'gracefulTerminationTimeout': 10}) + def test_terminate(self, body, mock_terminate, mock_save, mock_get_by_id): + vnf_instance_obj = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + mock_get_by_id.return_value = vnf_instance_obj + + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/terminate' % uuidsentinel.vnf_instance_id) + 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.ACCEPTED, resp.status_code) + mock_terminate.assert_called_once() + + @ddt.data( + {'attribute': 'terminationType', 'value': "TEST", + 'expected_type': 'enum'}, + {'attribute': 'terminationType', 'value': 123, + 'expected_type': 'enum'}, + {'attribute': 'terminationType', 'value': True, + 'expected_type': 'enum'}, + {'attribute': 'gracefulTerminationTimeout', 'value': True, + 'expected_type': 'integer'}, + {'attribute': 'gracefulTerminationTimeout', 'value': "test", + 'expected_type': 'integer'} + ) + @ddt.unpack + def test_terminate_with_invalid_request_body( + self, attribute, value, expected_type): + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/terminate' % uuidsentinel.vnf_instance_id) + body = {'terminationType': 'GRACEFUL', + 'gracefulTerminationTimeout': 10} + body.update({attribute: value}) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + expected_message = ("Invalid input for field/attribute {attribute}. " + "Value: {value}.".format(value=value, attribute=attribute)) + + exception = self.assertRaises(exceptions.ValidationError, + self.controller.terminate, + req, constants.UUID, body=body) + self.assertIn(expected_message, exception.msg) + + def test_terminate_missing_termination_type(self): + body = {'gracefulTerminationTimeout': 10} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/terminate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + + # Call terminate API + resp = req.get_response(self.app) + self.assertEqual(http_client.BAD_REQUEST, resp.status_code) + self.assertEqual("'terminationType' is a required property", + resp.json['badRequest']['message']) + + @ddt.data('GET', 'HEAD', 'PUT', 'DELETE', 'PATCH') + def test_terminate_invalid_http_method(self, method): + # Wrong HTTP method + body = {'terminationType': 'GRACEFUL', + 'gracefulTerminationTimeout': 10} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/terminate' % uuidsentinel.vnf_instance_id) + req.body = jsonutils.dump_as_bytes(body) + 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) + + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + def test_terminate_non_existing_vnf_instance(self, mock_vnf_by_id): + body = {'terminationType': 'GRACEFUL', + 'gracefulTerminationTimeout': 10} + mock_vnf_by_id.side_effect = exceptions.VnfInstanceNotFound + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/terminate' % uuidsentinel.vnf_instance_id) + 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.NOT_FOUND, resp.status_code) + self.assertEqual("Can not find requested vnf instance: %s" % + uuidsentinel.vnf_instance_id, + resp.json['itemNotFound']['message']) + + @mock.patch.object(objects.vnf_instance, "_vnf_instance_get_by_id") + def test_terminate_incorrect_instantiation_state(self, mock_vnf_by_id): + mock_vnf_by_id.return_value = fakes.return_vnf_instance() + body = {"terminationType": "FORCEFUL"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/terminate' % uuidsentinel.vnf_instance_id) + 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.CONFLICT, resp.status_code) + expected_msg = ("Vnf instance %s in instantiation_state " + "NOT_INSTANTIATED. Cannot terminate while the vnf " + "instance is in this state.") + self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, + resp.json['conflictingRequest']['message']) + + @mock.patch.object(objects.VnfInstance, "get_by_id") + def test_terminate_incorrect_task_state(self, mock_vnf_by_id): + vnf_instance = fakes.return_vnf_instance( + instantiated_state=fields.VnfInstanceState.INSTANTIATED, + task_state=fields.VnfInstanceTaskState.TERMINATING) + mock_vnf_by_id.return_value = vnf_instance + + body = {"terminationType": "FORCEFUL"} + req = fake_request.HTTPRequest.blank( + '/vnf_instances/%s/terminate' % uuidsentinel.vnf_instance_id) + 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.CONFLICT, resp.status_code) + expected_msg = ("Vnf instance %s in task_state TERMINATING. Cannot " + "terminate while the vnf instance is in this state.") + self.assertEqual(expected_msg % uuidsentinel.vnf_instance_id, + resp.json['conflictingRequest']['message']) diff --git a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py index 7cde0d099..12d0b3ab5 100644 --- a/tacker/tests/unit/vnflcm/test_vnflcm_driver.py +++ b/tacker/tests/unit/vnflcm/test_vnflcm_driver.py @@ -23,10 +23,12 @@ from tacker.common import exceptions from tacker.common import utils from tacker import context from tacker import objects +from tacker.objects import fields from tacker.tests.unit.db import base as db_base from tacker.tests.unit.vnflcm import fakes from tacker.tests import uuidsentinel from tacker.vnflcm import vnflcm_driver +from tacker.vnfm import vim_client class InfraDriverException(Exception): @@ -256,7 +258,7 @@ class TestVnflcmDriver(db_base.SqlTestCase): self.assertEqual(expected_error % vnf_instance_obj.id, str(error)) self.assertEqual("NOT_INSTANTIATED", vnf_instance_obj.instantiation_state) - self.assertEqual(1, mock_vnf_instance_save.call_count) + self.assertEqual(2, mock_vnf_instance_save.call_count) self.assertEqual(2, self._vnf_manager.invoke.call_count) shutil.rmtree(fake_csar) @@ -298,8 +300,8 @@ class TestVnflcmDriver(db_base.SqlTestCase): self.assertEqual(expected_error % vnf_instance_obj.id, str(error)) self.assertEqual("NOT_INSTANTIATED", vnf_instance_obj.instantiation_state) - self.assertEqual(1, mock_vnf_instance_save.call_count) - self.assertEqual(3, self._vnf_manager.invoke.call_count) + self.assertEqual(3, mock_vnf_instance_save.call_count) + self.assertEqual(5, self._vnf_manager.invoke.call_count) shutil.rmtree(fake_csar) @@ -334,3 +336,106 @@ class TestVnflcmDriver(db_base.SqlTestCase): self.assertEqual(2, mock_create.call_count) self.assertEqual("INSTANTIATED", vnf_instance_obj.instantiation_state) shutil.rmtree(fake_csar) + + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + @mock.patch.object(objects.VnfResource, "destroy") + def test_terminate_vnf(self, mock_resource_destroy, mock_resource_list, + mock_vim, mock_vnf_instance_save): + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + + mock_resource_list.return_value = [fakes.return_vnf_resource()] + terminate_vnf_req = objects.TerminateVnfRequest( + termination_type=fields.VnfInstanceTerminationType.FORCEFUL) + + self._mock_vnf_manager() + driver = vnflcm_driver.VnfLcmDriver() + driver.terminate_vnf(self.context, vnf_instance, terminate_vnf_req) + self.assertEqual(2, mock_vnf_instance_save.call_count) + self.assertEqual(1, mock_resource_destroy.call_count) + self.assertEqual(3, self._vnf_manager.invoke.call_count) + + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + @mock.patch.object(objects.VnfResource, "destroy") + def test_terminate_vnf_graceful_no_timeout(self, mock_resource_destroy, + mock_resource_list, mock_vim, mock_vnf_instance_save): + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + + mock_resource_list.return_value = [fakes.return_vnf_resource()] + terminate_vnf_req = objects.TerminateVnfRequest( + termination_type=fields.VnfInstanceTerminationType.GRACEFUL) + + self._mock_vnf_manager() + driver = vnflcm_driver.VnfLcmDriver() + driver.terminate_vnf(self.context, vnf_instance, terminate_vnf_req) + self.assertEqual(2, mock_vnf_instance_save.call_count) + self.assertEqual(1, mock_resource_destroy.call_count) + + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(vim_client.VimClient, "get_vim") + def test_terminate_vnf_delete_instance_failed(self, mock_vim, + mock_vnf_instance_save): + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + terminate_vnf_req = objects.TerminateVnfRequest( + termination_type=fields.VnfInstanceTerminationType.GRACEFUL, + graceful_termination_timeout=10) + + self._mock_vnf_manager(fail_method_name='delete') + driver = vnflcm_driver.VnfLcmDriver() + error = self.assertRaises(InfraDriverException, driver.terminate_vnf, + self.context, vnf_instance, terminate_vnf_req) + self.assertEqual("delete failed", str(error)) + self.assertEqual(1, mock_vnf_instance_save.call_count) + self.assertEqual(1, self._vnf_manager.invoke.call_count) + + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(vim_client.VimClient, "get_vim") + def test_terminate_vnf_delete_wait_instance_failed(self, mock_vim, + mock_vnf_instance_save): + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + terminate_vnf_req = objects.TerminateVnfRequest( + termination_type=fields.VnfInstanceTerminationType.FORCEFUL) + + self._mock_vnf_manager(fail_method_name='delete_wait') + driver = vnflcm_driver.VnfLcmDriver() + error = self.assertRaises(InfraDriverException, driver.terminate_vnf, + self.context, vnf_instance, terminate_vnf_req) + self.assertEqual("delete_wait failed", str(error)) + self.assertEqual(2, mock_vnf_instance_save.call_count) + self.assertEqual(2, self._vnf_manager.invoke.call_count) + + @mock.patch.object(objects.VnfInstance, "save") + @mock.patch.object(vim_client.VimClient, "get_vim") + @mock.patch.object(objects.VnfResourceList, "get_by_vnf_instance_id") + def test_terminate_vnf_delete_vnf_resource_failed(self, mock_resource_list, + mock_vim, mock_vnf_instance_save): + vnf_instance = fakes.return_vnf_instance( + fields.VnfInstanceState.INSTANTIATED) + vnf_instance.instantiated_vnf_info.instance_id =\ + uuidsentinel.instance_id + terminate_vnf_req = objects.TerminateVnfRequest( + termination_type=fields.VnfInstanceTerminationType.FORCEFUL) + + mock_resource_list.return_value = [fakes.return_vnf_resource()] + self._mock_vnf_manager(fail_method_name='delete_vnf_resource') + driver = vnflcm_driver.VnfLcmDriver() + error = self.assertRaises(InfraDriverException, driver.terminate_vnf, + self.context, vnf_instance, terminate_vnf_req) + self.assertEqual("delete_vnf_resource failed", str(error)) + self.assertEqual(2, mock_vnf_instance_save.call_count) + self.assertEqual(3, self._vnf_manager.invoke.call_count) diff --git a/tacker/vnflcm/abstract_driver.py b/tacker/vnflcm/abstract_driver.py index 9dc1a7633..f03b046cd 100644 --- a/tacker/vnflcm/abstract_driver.py +++ b/tacker/vnflcm/abstract_driver.py @@ -31,3 +31,14 @@ class VnfInstanceAbstractDriver(object): :return: None """ pass + + @abc.abstractmethod + def terminate_vnf(self, context, vnf_instance, terminate_vnf_req): + """terminate vnf request. + + :param context: the request context + :param vnf_instance: object of VnfInstance + :param terminate_vnf_req: object of TerminateVnfRequest + :return: None + """ + pass diff --git a/tacker/vnflcm/vnflcm_driver.py b/tacker/vnflcm/vnflcm_driver.py index 6e278887c..2e0df6e8f 100644 --- a/tacker/vnflcm/vnflcm_driver.py +++ b/tacker/vnflcm/vnflcm_driver.py @@ -14,6 +14,10 @@ # under the License. import copy +import functools +import inspect +import six +import time from oslo_config import cfg from oslo_log import log as logging @@ -24,6 +28,8 @@ from tacker.common import log from tacker.common import driver_manager from tacker.common import exceptions +from tacker.common import safe_utils +from tacker.common import utils from tacker import objects from tacker.objects import fields from tacker.vnflcm import abstract_driver @@ -34,6 +40,84 @@ LOG = logging.getLogger(__name__) CONF = cfg.CONF +@utils.expects_func_args('vnf_instance') +def rollback_vnf_instantiated_resources(function): + """Decorator to rollback resources created during vnf instantiation""" + + def _rollback_vnf(vnflcm_driver, context, vnf_instance): + vim_info = vnflcm_utils._get_vim(context, + vnf_instance.vim_connection_info) + + vim_connection_info = objects.VimConnectionInfo.obj_from_primitive( + vim_info, context) + + LOG.info("Rollback vnf %s", vnf_instance.id) + try: + vnflcm_driver._delete_vnf_instance_resources(context, vnf_instance, + vim_connection_info) + + if vnf_instance.instantiated_vnf_info: + vnf_instance.instantiated_vnf_info.reinitialize() + + vnflcm_driver._vnf_instance_update(context, vnf_instance, + vim_connection_info=[], task_state=None) + + LOG.info("Vnf %s rollback completed successfully", vnf_instance.id) + except Exception as ex: + LOG.error("Unable to rollback vnf instance " + "%s due to error: %s", vnf_instance.id, ex) + + @functools.wraps(function) + def decorated_function(self, context, *args, **kwargs): + try: + return function(self, context, *args, **kwargs) + except Exception as exp: + with excutils.save_and_reraise_exception(): + wrapped_func = safe_utils.get_wrapped_function(function) + keyed_args = inspect.getcallargs(wrapped_func, self, context, + *args, **kwargs) + vnf_instance = keyed_args['vnf_instance'] + LOG.error("Failed to instantiate vnf %(id)s. Error: %(error)s", + {"id": vnf_instance.id, + "error": six.text_type(exp)}) + + _rollback_vnf(self, context, vnf_instance) + + return decorated_function + + +@utils.expects_func_args('vnf_instance') +def revert_to_error_task_state(function): + """Decorator to revert task_state to error on failure.""" + + @functools.wraps(function) + def decorated_function(self, context, *args, **kwargs): + try: + return function(self, context, *args, **kwargs) + except Exception: + with excutils.save_and_reraise_exception(): + wrapped_func = safe_utils.get_wrapped_function(function) + keyed_args = inspect.getcallargs(wrapped_func, self, context, + *args, **kwargs) + vnf_instance = keyed_args['vnf_instance'] + previous_task_state = vnf_instance.task_state + try: + self._vnf_instance_update(context, vnf_instance, + task_state=fields.VnfInstanceTaskState.ERROR) + LOG.info("Successfully reverted task state from " + "%(state)s to %(error)s on failure for vnf " + "instance %(id)s.", + {"state": previous_task_state, + "id": vnf_instance.id, + "error": fields.VnfInstanceTaskState.ERROR}) + except Exception as e: + LOG.warning("Failed to revert task state for vnf " + "instance %(id)s. Error: %(error)s", + {"id": vnf_instance.id, "error": e}) + + return decorated_function + + class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): def __init__(self): @@ -130,6 +214,7 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): vim_connection_info=vim_connection_info) @log.log + @rollback_vnf_instantiated_resources def instantiate_vnf(self, context, vnf_instance, instantiate_vnf_req): vim_connection_info_list = vnflcm_utils.\ @@ -151,3 +236,71 @@ class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver): self._vnf_instance_update(context, vnf_instance, instantiation_state=fields.VnfInstanceState.INSTANTIATED, task_state=None) + + @log.log + @revert_to_error_task_state + def terminate_vnf(self, context, vnf_instance, terminate_vnf_req): + + vim_info = vnflcm_utils._get_vim(context, + vnf_instance.vim_connection_info) + + vim_connection_info = objects.VimConnectionInfo.obj_from_primitive( + vim_info, context) + + LOG.info("Terminating vnf %s", vnf_instance.id) + try: + self._delete_vnf_instance_resources(context, vnf_instance, + vim_connection_info, terminate_vnf_req=terminate_vnf_req) + + vnf_instance.instantiated_vnf_info.reinitialize() + self._vnf_instance_update(context, vnf_instance, + vim_connection_info=[], task_state=None) + + LOG.info("Vnf terminated %s successfully", vnf_instance.id) + except Exception as exp: + with excutils.save_and_reraise_exception(): + LOG.error("Unable to terminate vnf '%s' instance. " + "Error: %s", vnf_instance.id, + encodeutils.exception_to_unicode(exp)) + + def _delete_vnf_instance_resources(self, context, vnf_instance, + vim_connection_info, + terminate_vnf_req=None): + + if vnf_instance.instantiated_vnf_info and \ + vnf_instance.instantiated_vnf_info.instance_id: + + instance_id = vnf_instance.instantiated_vnf_info.instance_id + access_info = vim_connection_info.access_info + + LOG.info("Deleting stack %(instance)s for vnf %(id)s ", + {"instance": instance_id, "id": vnf_instance.id}) + + if terminate_vnf_req: + if (terminate_vnf_req.termination_type == 'GRACEFUL' and + terminate_vnf_req.graceful_termination_timeout > 0): + time.sleep(terminate_vnf_req.graceful_termination_timeout) + + self._vnf_manager.invoke(vim_connection_info.vim_type, + 'delete', plugin=self, context=context, + vnf_id=instance_id, auth_attr=access_info) + + vnf_instance.instantiation_state = \ + fields.VnfInstanceState.NOT_INSTANTIATED + vnf_instance.save() + + self._vnf_manager.invoke(vim_connection_info.vim_type, + 'delete_wait', plugin=self, context=context, + vnf_id=instance_id, auth_attr=access_info) + + vnf_resources = objects.VnfResourceList.get_by_vnf_instance_id( + context, vnf_instance.id) + + for vnf_resource in vnf_resources: + self._vnf_manager.invoke(vim_connection_info.vim_type, + 'delete_vnf_instance_resource', + context=context, vnf_instance=vnf_instance, + vim_connection_info=vim_connection_info, + vnf_resource=vnf_resource) + + vnf_resource.destroy(context)