Merge "OSC support to delete and terminate vnf"
This commit is contained in:
commit
a5466fe493
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue