From c3a8f259479f2aadf444f803078a8b3679923772 Mon Sep 17 00:00:00 2001 From: Shubham Potale Date: Fri, 13 Dec 2019 20:03:33 +0530 Subject: [PATCH] OSC support to delete and terminate vnf Added new commands ``openstack vnflcm delete`` and ``openstack vnflcm terminate``. Blueprint: support-etsi-nfv-specs Change-Id: I72ea99c2149621428a34403504f28f9e9b1a3719 --- setup.cfg | 2 + tackerclient/osc/v1/vnflcm/vnflcm.py | 147 ++++++++++++ tackerclient/tests/unit/osc/v1/test_vnflcm.py | 221 +++++++++++++++++- .../tests/unit/osc/v1/vnflcm_fakes.py | 18 ++ tackerclient/v1_0/client.py | 15 ++ 5 files changed, 401 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index bbc469f9..94edae2f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -86,6 +86,8 @@ openstack.tackerclient.v1 = vnflcm_create = tackerclient.osc.v1.vnflcm.vnflcm:CreateVnfLcm vnflcm_show = tackerclient.osc.v1.vnflcm.vnflcm:ShowVnfLcm vnflcm_instantiate = tackerclient.osc.v1.vnflcm.vnflcm:InstantiateVnfLcm + vnflcm_terminate = tackerclient.osc.v1.vnflcm.vnflcm:TerminateVnfLcm + vnflcm_delete = tackerclient.osc.v1.vnflcm.vnflcm:DeleteVnfLcm [build_releasenotes] all_files = 1 diff --git a/tackerclient/osc/v1/vnflcm/vnflcm.py b/tackerclient/osc/v1/vnflcm/vnflcm.py index 4306bc48..cfbad1a7 100644 --- a/tackerclient/osc/v1/vnflcm/vnflcm.py +++ b/tackerclient/osc/v1/vnflcm/vnflcm.py @@ -16,6 +16,7 @@ import json import logging import os +import time from osc_lib.cli import format_columns from osc_lib.command import command @@ -34,6 +35,12 @@ _mixed_case_fields = ('vnfInstanceName', 'vnfInstanceDescription', 'vnfdId', _VNF_INSTANCE = 'vnf_instance' +VNF_INSTANCE_TERMINATION_TIMEOUT = 300 + +EXTRA_WAITING_TIME = 10 + +SLEEP_TIME = 1 + def _get_columns(vnflcm_obj, action=None): column_map = { @@ -186,3 +193,143 @@ class InstantiateVnfLcm(command.Command): if not result: print((_('Instantiate request for VNF Instance %(id)s has been' ' accepted.') % {'id': parsed_args.vnf_instance})) + + +class TerminateVnfLcm(command.Command): + _description = _("Terminate a VNF instance") + + def get_parser(self, prog_name): + parser = super(TerminateVnfLcm, self).get_parser(prog_name) + parser.add_argument( + _VNF_INSTANCE, + metavar="", + help=_("VNF instance ID to terminate")) + parser.add_argument( + "--termination-type", + default='GRACEFUL', + metavar="", + choices=['GRACEFUL', 'FORCEFUL'], + help=_("Termination type can be 'GRACEFUL' or 'FORCEFUL'. " + "Default is 'GRACEFUL'")) + parser.add_argument( + '--graceful-termination-timeout', + metavar="", + type=int, + help=_('This attribute is only applicable in case of graceful ' + 'termination. It defines the time to wait for the VNF to be' + ' taken out of service before shutting down the VNF and ' + 'releasing the resources. The unit is seconds.')) + parser.add_argument( + '--D', + action='store_true', + default=False, + help=_("Delete VNF Instance subsequently after it's termination"), + ) + return parser + + def args2body(self, parsed_args): + body = {} + body['terminationType'] = parsed_args.termination_type + + if parsed_args.graceful_termination_timeout: + if parsed_args.termination_type == 'FORCEFUL': + exceptions.InvalidInput('--graceful-termination-timeout' + ' argument is invalid for "FORCEFUL"' + ' termination') + body['gracefulTerminationTimeout'] = parsed_args.\ + graceful_termination_timeout + + return body + + def take_action(self, parsed_args): + client = self.app.client_manager.tackerclient + result = client.terminate_vnf_instance(parsed_args.vnf_instance, + self.args2body(parsed_args)) + if not result: + print(_("Terminate request for VNF Instance '%(id)s' has been" + " accepted.") % {'id': parsed_args.vnf_instance}) + if parsed_args.D: + print(_("Waiting for vnf instance to be terminated before " + "deleting")) + + self._wait_until_vnf_is_terminated( + client, parsed_args.vnf_instance, + graceful_timeout=parsed_args.graceful_termination_timeout) + + result = client.delete_vnf_instance(parsed_args.vnf_instance) + if not result: + print(_("VNF Instance '%(id)s' deleted successfully") % + {'id': parsed_args.vnf_instance}) + + def _wait_until_vnf_is_terminated(self, client, vnf_instance_id, + graceful_timeout=None): + # wait until vnf instance 'instantiationState' is set to + # 'NOT_INSTANTIATED' + if graceful_timeout: + # If graceful_termination_timeout is provided, + # terminate vnf will start after this timeout period. + # Hence, it should wait for extra time of 10 seconds + # after this graceful_termination_timeout period. + timeout = graceful_timeout + EXTRA_WAITING_TIME + else: + timeout = VNF_INSTANCE_TERMINATION_TIMEOUT + + start_time = int(time.time()) + while True: + vnf_instance = client.show_vnf_instance(vnf_instance_id) + if vnf_instance['instantiationState'] == 'NOT_INSTANTIATED': + break + + if ((int(time.time()) - start_time) > timeout): + msg = _("Couldn't verify vnf instance is terminated within " + "'%(timeout)s' seconds. Unable to delete vnf instance " + "%(id)s") + raise exceptions.CommandError(msg % {'timeout': timeout, + 'id': vnf_instance_id}) + time.sleep(SLEEP_TIME) + + +class DeleteVnfLcm(command.Command): + """Vnf lcm delete + + DeleteVnfLcm class supports bulk deletion of vnf instances, and error + handling. + """ + + _description = _("Delete VNF Instance(s)") + + def get_parser(self, prog_name): + parser = super(DeleteVnfLcm, self).get_parser(prog_name) + parser.add_argument( + 'vnf_instances', + metavar="", + nargs="+", + help=_("VNF instance ID(s) to delete")) + return parser + + def take_action(self, parsed_args): + error_count = 0 + client = self.app.client_manager.tackerclient + vnf_instances = parsed_args.vnf_instances + for vnf_instance in vnf_instances: + try: + client.delete_vnf_instance(vnf_instance) + except Exception as e: + error_count += 1 + LOG.error(_("Failed to delete vnf instance with " + "ID '%(vnf)s': %(e)s"), + {'vnf': vnf_instance, 'e': e}) + + total = len(vnf_instances) + if (error_count > 0): + msg = (_("Failed to delete %(error_count)s of %(total)s " + "vnf instances.") % {'error_count': error_count, + 'total': total}) + raise exceptions.CommandError(msg) + else: + if total > 1: + print(_('All specified vnf instances are deleted ' + 'successfully')) + else: + print(_("Vnf instance '%s' deleted " + "successfully") % vnf_instances[0]) diff --git a/tackerclient/tests/unit/osc/v1/test_vnflcm.py b/tackerclient/tests/unit/osc/v1/test_vnflcm.py index f94fe525..753a3ccf 100644 --- a/tackerclient/tests/unit/osc/v1/test_vnflcm.py +++ b/tackerclient/tests/unit/osc/v1/test_vnflcm.py @@ -13,13 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. -import ddt from io import StringIO -import mock import os import sys +import ddt +import mock from oslo_utils.fixture import uuidsentinel +import six from tackerclient.common import exceptions from tackerclient.osc.v1.vnflcm import vnflcm @@ -237,3 +238,219 @@ class TestInstantiateVnfLcm(TestVnfLcm): expected_msg = "Failed to load parameter file." self.assertIn(expected_msg, ex.message) + + +@ddt.ddt +class TestTerminateVnfLcm(TestVnfLcm): + + def setUp(self): + super(TestTerminateVnfLcm, self).setUp() + self.terminate_vnf_instance = vnflcm.TerminateVnfLcm( + self.app, self.app_args, cmd_name='vnflcm terminate') + + @ddt.data({'termination_type': 'GRACEFUL', 'delete_vnf': True}, + {'termination_type': 'FORCEFUL', 'delete_vnf': False}) + @ddt.unpack + def test_take_action(self, termination_type, delete_vnf): + # argument 'delete_vnf' decides deletion of vnf instance post + # termination. + vnf_instance = vnflcm_fakes.vnf_instance_response() + arglist = ['--termination-type', termination_type, vnf_instance['id']] + + verifylist = [('termination_type', termination_type), + ('vnf_instance', vnf_instance['id'])] + + if delete_vnf: + arglist.extend(['--D']) + verifylist.extend([('D', True)]) + + if termination_type == 'GRACEFUL': + arglist.extend(['--graceful-termination-timeout', '60']) + verifylist.append(('graceful_termination_timeout', 60)) + parsed_args = self.check_parser(self.terminate_vnf_instance, arglist, + verifylist) + url = os.path.join(self.url, 'vnflcm/v1/vnf_instances', + vnf_instance['id'], 'terminate') + + with mock.patch.object(proxy_client.ClientBase, + '_handle_fault_response') as m: + self.requests_mock.register_uri('POST', url, json={}, + headers=self.header) + if delete_vnf: + self.requests_mock.register_uri( + 'GET', os.path.join(self.url, 'vnflcm/v1/vnf_instances', + vnf_instance['id']), + json=vnf_instance, headers=self.header) + self.requests_mock.register_uri( + 'DELETE', os.path.join( + self.url, 'vnflcm/v1/vnf_instances', + vnf_instance['id']), json={}, headers=self.header) + + sys.stdout = buffer = StringIO() + result = self.terminate_vnf_instance.take_action(parsed_args) + actual_message = buffer.getvalue().strip() + + expected_message = ("Terminate request for VNF Instance '%s'" + " has been accepted.") % vnf_instance['id'] + self.assertIn(expected_message, actual_message) + + if delete_vnf: + expected_message = ("VNF Instance '%s' deleted successfully" + % vnf_instance['id']) + self.assertIn(expected_message, actual_message) + + self.assertIsNone(result) + self.assertNotCalled(m) + + def test_take_action_terminate_and_delete_wait_failed(self): + vnf_instance = vnflcm_fakes.vnf_instance_response() + termination_type = 'GRACEFUL' + arglist = ['--termination-type', termination_type, '--D', + '--graceful-termination-timeout', '5', vnf_instance['id']] + + verifylist = [('termination_type', termination_type), ('D', True), + ('graceful_termination_timeout', 5), + ('vnf_instance', vnf_instance['id'])] + + parsed_args = self.check_parser(self.terminate_vnf_instance, arglist, + verifylist) + url = os.path.join(self.url, 'vnflcm/v1/vnf_instances', + vnf_instance['id'], 'terminate') + + self.requests_mock.register_uri('POST', url, json={}, + headers=self.header) + # set the instantiateState to "INSTANTIATED", so that the + # _wait_until_vnf_is_terminated will fail + vnf_instance['instantiationState'] = 'INSTANTIATED' + + self.requests_mock.register_uri( + 'GET', os.path.join(self.url, 'vnflcm/v1/vnf_instances', + vnf_instance['id']), + json=vnf_instance, headers=self.header) + + sys.stdout = buffer = StringIO() + with mock.patch.object(self.app.client_manager.tackerclient, + 'delete_vnf_instance') as mock_delete: + result = self.assertRaises( + exceptions.CommandError, + self.terminate_vnf_instance.take_action, parsed_args) + + actual_message = buffer.getvalue().strip() + + # Terminate vnf instance verification + expected_message = ("Terminate request for VNF Instance '%s'" + " has been accepted.") % vnf_instance['id'] + self.assertIn(expected_message, actual_message) + + # Verify it fails to wait for termination before delete + expected_message = ("Couldn't verify vnf instance is terminated " + "within '%(timeout)s' seconds. Unable to " + "delete vnf instance %(id)s" + % {'timeout': 15, 'id': vnf_instance['id']}) + + self.assertIn(expected_message, six.text_type(result)) + self.assertNotCalled(mock_delete) + + def test_terminate_no_options(self): + self.assertRaises(base.ParserException, self.check_parser, + self.terminate_vnf_instance, [], []) + + def test_take_action_vnf_instance_not_found(self): + vnf_instance = vnflcm_fakes.vnf_instance_response() + termination_type = 'GRACEFUL' + arglist = ['--termination-type', termination_type, '--D', + '--graceful-termination-timeout', '5', vnf_instance['id']] + + verifylist = [('termination_type', termination_type), ('D', True), + ('graceful_termination_timeout', 5), + ('vnf_instance', vnf_instance['id'])] + + parsed_args = self.check_parser(self.terminate_vnf_instance, arglist, + verifylist) + + url = os.path.join(self.url, 'vnflcm/v1/vnf_instances', + vnf_instance['id'], 'terminate') + self.requests_mock.register_uri('POST', url, headers=self.header, + status_code=404, json={}) + + self.assertRaises(exceptions.TackerClientException, + self.terminate_vnf_instance.take_action, + parsed_args) + + +class TestDeleteVnfLcm(TestVnfLcm): + + def setUp(self): + super(TestDeleteVnfLcm, self).setUp() + self.delete_vnf_instance = vnflcm.DeleteVnfLcm( + self.app, self.app_args, cmd_name='vnflcm delete') + + # Vnf Instance to delete + self.vnf_instances = vnflcm_fakes.create_vnf_instances(count=3) + + def _mock_request_url_for_delete(self, vnf_index): + url = os.path.join(self.url, 'vnflcm/v1/vnf_instances', + self.vnf_instances[vnf_index]['id']) + + json = self.vnf_instances[vnf_index] + + self.requests_mock.register_uri('GET', url, json=json, + headers=self.header) + self.requests_mock.register_uri('DELETE', url, + headers=self.header, json={}) + + def test_delete_one_vnf_instance(self): + arglist = [self.vnf_instances[0]['id']] + verifylist = [('vnf_instances', + [self.vnf_instances[0]['id']])] + + parsed_args = self.check_parser(self.delete_vnf_instance, arglist, + verifylist) + + self._mock_request_url_for_delete(0) + sys.stdout = buffer = StringIO() + result = self.delete_vnf_instance.take_action(parsed_args) + self.assertIsNone(result) + self.assertEqual(("Vnf instance '%s' deleted successfully") + % self.vnf_instances[0]['id'], + buffer.getvalue().strip()) + + def test_delete_multiple_vnf_instance(self): + arglist = [] + for vnf_pkg in self.vnf_instances: + arglist.append(vnf_pkg['id']) + verifylist = [('vnf_instances', arglist)] + parsed_args = self.check_parser(self.delete_vnf_instance, arglist, + verifylist) + for i in range(0, 3): + self._mock_request_url_for_delete(i) + sys.stdout = buffer = StringIO() + result = self.delete_vnf_instance.take_action(parsed_args) + self.assertIsNone(result) + self.assertEqual('All specified vnf instances are deleted ' + 'successfully', buffer.getvalue().strip()) + + def test_delete_multiple_vnf_instance_exception(self): + arglist = [ + self.vnf_instances[0]['id'], + 'xxxx-yyyy-zzzz', + self.vnf_instances[1]['id'], + ] + verifylist = [('vnf_instances', arglist)] + parsed_args = self.check_parser(self.delete_vnf_instance, + arglist, verifylist) + + self._mock_request_url_for_delete(0) + + url = os.path.join(self.url, 'vnflcm/v1/vnf_instances', + 'xxxx-yyyy-zzzz') + self.requests_mock.register_uri( + 'GET', url, exc=exceptions.ConnectionFailed) + + self._mock_request_url_for_delete(1) + exception = self.assertRaises(exceptions.CommandError, + self.delete_vnf_instance.take_action, + parsed_args) + + self.assertEqual('Failed to delete 1 of 3 vnf instances.', + exception.message) diff --git a/tackerclient/tests/unit/osc/v1/vnflcm_fakes.py b/tackerclient/tests/unit/osc/v1/vnflcm_fakes.py index c48cfb06..1a6d7060 100644 --- a/tackerclient/tests/unit/osc/v1/vnflcm_fakes.py +++ b/tackerclient/tests/unit/osc/v1/vnflcm_fakes.py @@ -14,6 +14,7 @@ # under the License. from oslo_utils.fixture import uuidsentinel +from oslo_utils import uuidutils def vnf_instance_response(attrs=None, instantiation_state='NOT_INSTANTIATED'): @@ -104,6 +105,9 @@ def vnf_instance_response(attrs=None, instantiation_state='NOT_INSTANTIATED'): } }) + # Overwrite default attributes. + dummy_vnf_instance.update(attrs) + return dummy_vnf_instance @@ -115,3 +119,17 @@ def get_vnflcm_data(vnf_instance): """ # return the list of data as per column order return tuple([vnf_instance[key] for key in sorted(vnf_instance.keys())]) + + +def create_vnf_instances(count=2): + """Create multiple fake vnf instances. + + :param count: The number of vnf instances to fake + :return: + A list of fake vnf instances dictionary + """ + vnf_instances = [] + for i in range(0, count): + unique_id = uuidutils.generate_uuid() + vnf_instances.append(vnf_instance_response(attrs={'id': unique_id})) + return vnf_instances diff --git a/tackerclient/v1_0/client.py b/tackerclient/v1_0/client.py index 41169998..d71854b3 100644 --- a/tackerclient/v1_0/client.py +++ b/tackerclient/v1_0/client.py @@ -800,6 +800,15 @@ class VnfLCMClient(ClientBase): return self.post((self.vnf_instance_path + "/instantiate") % vnf_id, body=body) + @APIParamsCall + def terminate_vnf_instance(self, vnf_id, body): + return self.post((self.vnf_instance_path + "/terminate") % vnf_id, + body=body) + + @APIParamsCall + def delete_vnf_instance(self, vnf_id): + return self.delete(self.vnf_instance_path % vnf_id) + class Client(object): """Unified interface to interact with multiple applications of tacker service. @@ -1058,3 +1067,9 @@ class Client(object): def instantiate_vnf_instance(self, vnf_id, body): return self.vnf_lcm_client.instantiate_vnf_instance(vnf_id, body) + + def terminate_vnf_instance(self, vnf_id, body): + return self.vnf_lcm_client.terminate_vnf_instance(vnf_id, body) + + def delete_vnf_instance(self, vnf_id): + return self.vnf_lcm_client.delete_vnf_instance(vnf_id)