Add terminate vnf instance API
Implemented terminate vnf instance API. * GET /vnflcm/v1/vnf_instances/{vnf_instance_id}/terminate Co-Authored-By: tpatil <tushar.vitthal.patil@gmail.com> Co-Authored-By: Shubham Potale <shubham.potale@nttdata.com> Co-Authored-By: Sameer Thakur <sameer.thakur@nttdata.com> Change-Id: I69b7ef12038aa410db7e50671df2931beb761223 Blueprint: support-etsi-nfv-specs
This commit is contained in:
parent
19ff0032f0
commit
fbb38266d6
@ -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,
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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()
|
||||
|
54
tacker/objects/terminate_vnf_req.py
Normal file
54
tacker/objects/terminate_vnf_req.py
Normal file
@ -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)
|
@ -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,
|
||||
|
@ -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'
|
||||
}
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
51
tacker/tests/unit/objects/test_terminate_vnf_request.py
Normal file
51
tacker/tests/unit/objects/test_terminate_vnf_request.py
Normal file
@ -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)
|
@ -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'])
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user