Merge "OSC support to delete and terminate vnf"

This commit is contained in:
Zuul 2020-04-01 10:54:57 +00:00 committed by Gerrit Code Review
commit a5466fe493
5 changed files with 401 additions and 2 deletions

View File

@ -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

View File

@ -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="<vnf-instance>",
help=_("VNF instance ID to terminate"))
parser.add_argument(
"--termination-type",
default='GRACEFUL',
metavar="<termination-type>",
choices=['GRACEFUL', 'FORCEFUL'],
help=_("Termination type can be 'GRACEFUL' or 'FORCEFUL'. "
"Default is 'GRACEFUL'"))
parser.add_argument(
'--graceful-termination-timeout',
metavar="<graceful-termination-timeout>",
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="<vnf-instance>",
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])

View File

@ -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)

View File

@ -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

View File

@ -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)