support rollback operation task of v2 API

This patch implements rollback operation task defined in ETSI NFV-SOL003
v3.3.1 5.4.15.

This patch supports rollback operation of existing task API, i.e
instantiate. Note that rollback operation of terminate is not supported.

Rollback operation of a task API which is added in the future
(ex. scale) will be implemented in the same patch
of implementing the API.

Functional tests will be provided with another patch.

Implements: blueprint support-nfv-solv3-error-handling
Change-Id: I3b2924287f7b30a85c250602a3f5ecbffd449a5e
This commit is contained in:
Itsuro Oda 2021-10-11 06:03:57 +00:00 committed by Hirofumi Noguchi
parent bf51c2bc80
commit 4b5204b213
13 changed files with 212 additions and 2 deletions

View File

@ -0,0 +1,5 @@
---
features:
- |
Add the Version "2.0.0" of Rollback operation API
based on ETSI NFV specifications.

View File

@ -159,6 +159,15 @@ rules = [
'path': VNF_LCM_OP_OCCS_ID_PATH + '/retry'}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('lcm_op_occ_rollback'),
check_str=RULE_ANY,
description="Rollback VnfLcmOpOcc.",
operations=[
{'method': 'POST',
'path': VNF_LCM_OP_OCCS_ID_PATH + '/rollback'}
]
),
# NOTE: 'DELETE' is not defined in the specification. It is for test
# use since it is convenient to be able to delete under development.
# It is available when config parameter

View File

@ -46,6 +46,7 @@ class VnflcmAPIRouterV2(sol_wsgi.SolAPIRouter):
"DELETE": "subscription_delete"}),
("/vnf_lcm_op_occs", {"GET": "lcm_op_occ_list"}),
("/vnf_lcm_op_occs/{id}/retry", {"POST": "lcm_op_occ_retry"}),
("/vnf_lcm_op_occs/{id}/rollback", {"POST": "lcm_op_occ_rollback"}),
# NOTE: 'DELETE' is not defined in the specification. It is for test
# use since it is convenient to be able to delete under development.
# It is available when config parameter

View File

@ -219,3 +219,7 @@ class LcmOpOccNotFailedTemp(SolHttpError409):
class GrantRequestOrGrantNotFound(SolHttpError404):
message = _("GrantRequest or Grant for LCM operation "
"%(lcmocc_id)s not found.")
class RollbackNotSupported(SolHttpError422):
message = _("Rollback of %(op)s is not supported.")

View File

@ -56,10 +56,11 @@ def make_lcmocc_links(lcmocc, endpoint):
href=inst_utils.inst_href(lcmocc.vnfInstanceId, endpoint))
links.retry = objects.Link(
href=lcmocc_task_href(lcmocc.vnfInstanceId, 'retry', endpoint))
links.rollback = objects.Link(
href=lcmocc_task_href(lcmocc.vnfInstanceId, 'rollback', endpoint))
# TODO(oda-g): add when implemented
# links.grant
# links.cancel
# links.rollback
# links.fail
# links.vnfSnapshot

View File

@ -44,3 +44,6 @@ class VnfLcmRpcApiV2(object):
def retry_lcm_op(self, context, lcmocc_id):
self._cast_lcm_op(context, lcmocc_id, 'retry_lcm_op')
def rollback_lcm_op(self, context, lcmocc_id):
self._cast_lcm_op(context, lcmocc_id, 'rollback_lcm_op')

View File

@ -181,3 +181,55 @@ class ConductorV2(object):
# send notification COMPLETED or FAILED_TEMP
self.nfvo_client.send_lcmocc_notification(context, lcmocc, inst,
self.endpoint)
@log.log
def rollback_lcm_op(self, context, lcmocc_id):
lcmocc = lcmocc_utils.get_lcmocc(context, lcmocc_id)
self._rollback_lcm_op(context, lcmocc)
@coordinate.lock_vnf_instance('{lcmocc.vnfInstanceId}', delay=True)
def _rollback_lcm_op(self, context, lcmocc):
# just consistency check
if lcmocc.operationState != fields.LcmOperationStateType.FAILED_TEMP:
LOG.error("VnfLcmOpOcc unexpected operationState.")
return
inst = inst_utils.get_inst(context, lcmocc.vnfInstanceId)
lcmocc.operationState = fields.LcmOperationStateType.ROLLING_BACK
lcmocc.update(context)
# send notification ROLLING_BACK
self.nfvo_client.send_lcmocc_notification(context, lcmocc, inst,
self.endpoint)
try:
vnfd = self.nfvo_client.get_vnfd(context, inst.vnfdId)
grant_req, grant = lcmocc_utils.get_grant_req_and_grant(context,
lcmocc)
self.vnflcm_driver.post_grant(context, lcmocc, inst, grant_req,
grant, vnfd)
self.vnflcm_driver.rollback(context, lcmocc, inst, grant_req,
grant, vnfd)
lcmocc.operationState = fields.LcmOperationStateType.ROLLED_BACK
with context.session.begin(subtransactions=True):
# it is not necessary to update inst DB because it was not
# changed when the operationState became FAILED_TEMP.
# NOTE: inst object may be changed in driver's rollback
# method temporary but must not save it.
lcmocc.update(context)
# grant_req and grant are not necessary any more.
grant_req.delete(context)
grant.delete(context)
except Exception as ex:
LOG.exception("ROLLING_BACK %s failed", lcmocc.operation)
lcmocc.operationState = fields.LcmOperationStateType.FAILED_TEMP
self._set_lcmocc_error(lcmocc, ex)
lcmocc.update(context)
# grant_req and grant are already saved. they are not deleted
# while oprationState is FAILED_TEMP.
# send notification ROLLED_BACK or FAILED_TEMP
self.nfvo_client.send_lcmocc_notification(context, lcmocc, inst,
self.endpoint)

View File

@ -104,6 +104,15 @@ class VnfLcmDriverV2(object):
self._exec_mgmt_driver_script(operation,
flavour_id, req, inst, grant_req, grant, vnfd)
def rollback(self, context, lcmocc, inst, grant_req, grant, vnfd):
method = getattr(self,
"%s_%s" % (lcmocc.operation.lower(), 'rollback'),
None)
if method:
method(context, lcmocc, inst, grant_req, grant, vnfd)
else:
raise sol_ex.RollbackNotSupported(op=lcmocc.operation)
def _get_link_ports(self, inst_req):
names = []
if inst_req.obj_attr_is_set('extVirtualLinks'):
@ -281,6 +290,17 @@ class VnfLcmDriverV2(object):
inst.instantiationState = 'INSTANTIATED'
lcmocc_utils.make_instantiate_lcmocc(lcmocc, inst)
def instantiate_rollback(self, context, lcmocc, inst, grant_req,
grant, vnfd):
req = lcmocc.operationParams
vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo)
if vim_info.vimType == 'ETSINFV.OPENSTACK_KEYSTONE.V_3':
driver = openstack.Openstack()
driver.instantiate_rollback(req, inst, grant_req, grant, vnfd)
else:
# only support openstack at the moment
raise sol_ex.SolException(sol_detail='not support vim type')
def terminate_grant(self, context, lcmocc, inst, vnfd):
# grant exchange
# NOTE: the api_version of NFVO supposes 1.4.0 at the moment.

View File

@ -344,6 +344,21 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController):
return sol_wsgi.SolResponse(202, None)
def lcm_op_occ_rollback(self, request, id):
context = request.context
lcmocc = lcmocc_utils.get_lcmocc(context, id)
return self._lcm_op_occ_rollback(context, lcmocc)
@coordinate.lock_vnf_instance('{lcmocc.vnfInstanceId}')
def _lcm_op_occ_rollback(self, context, lcmocc):
if lcmocc.operationState != v2fields.LcmOperationStateType.FAILED_TEMP:
raise sol_ex.LcmOpOccNotFailedTemp(lcmocc_id=lcmocc.id)
self.conductor_rpc.rollback_lcm_op(context, lcmocc.id)
return sol_wsgi.SolResponse(202, None)
def lcm_op_occ_delete(self, request, id):
# not allowed to delete on the specification
if not CONF.v2_vnfm.test_enable_lcm_op_occ_delete:

View File

@ -522,6 +522,14 @@ class Openstack(object):
inst.instantiatedVnfInfo = inst_vnf_info
def instantiate_rollback(self, req, inst, grant_req, grant, vnfd):
vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo)
heat_client = heat_utils.HeatClient(vim_info)
stack_name = heat_utils.get_stack_name(inst)
status, _ = heat_client.get_status(stack_name)
if status is not None:
heat_client.delete_stack(stack_name)
def terminate(self, req, inst, grant_req, grant, vnfd):
if req.terminationType == 'GRACEFUL':
timeout = CONF.v2_vnfm.default_graceful_termination_timeout

View File

@ -94,6 +94,10 @@ class Client(object):
resp, body = self.client.do_request(path, "POST", version="2.0.0")
self.print(resp, body)
def rollback(self, id):
path = self.path + '/' + id + '/rollback'
resp, body = self.client.do_request(path, "POST", version="2.0.0")
def usage():
print("usage: cli resource action [arg...]")
@ -111,6 +115,7 @@ def usage():
print(" lcmocc show {id}")
print(" lcmocc delete {id}")
print(" lcmocc retry {id}")
print(" lcmocc rollback {id}")
os._exit(1)
@ -134,7 +139,7 @@ if __name__ == '__main__':
usage()
client = Client("/vnflcm/v2/subscriptions")
elif resource == "lcmocc":
if action not in ["list", "show", "delete", "retry"]:
if action not in ["list", "show", "delete", "retry", "rollback"]:
usage()
client = Client("/vnflcm/v2/vnf_lcm_op_occs")
else:
@ -171,3 +176,7 @@ if __name__ == '__main__':
if len(sys.argv) != 4:
usage()
client.retry(sys.argv[3])
elif action == "rollback":
if len(sys.argv) != 4:
usage()
client.rollback(sys.argv[3])

View File

@ -264,3 +264,78 @@ class TestConductorV2(db_base.SqlTestCase):
# check grant_req and grant remain
# it's OK if no exception raised
lcmocc_utils.get_grant_req_and_grant(self.context, lcmocc)
@mock.patch.object(nfvo_client.NfvoClient, 'send_lcmocc_notification')
@mock.patch.object(nfvo_client.NfvoClient, 'get_vnfd')
@mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'post_grant')
@mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'rollback')
def test_rollback_lcm_op_rolled_back(self, mocked_rollback,
mocked_post_grant, mocked_get_vnfd,
mocked_send_lcmocc_notification):
# prepare
lcmocc = self._create_inst_and_lcmocc(
op_state=fields.LcmOperationStateType.FAILED_TEMP)
self._create_grant_req_and_grant(lcmocc)
mocked_get_vnfd.return_value = mock.Mock()
op_state = []
def _store_state(context, lcmocc, inst, endpoint):
op_state.append(lcmocc.operationState)
mocked_send_lcmocc_notification.side_effect = _store_state
# run rollback_lcm_op
self.conductor.rollback_lcm_op(self.context, lcmocc.id)
# check operationState transition
self.assertEqual(2, mocked_send_lcmocc_notification.call_count)
self.assertEqual(fields.LcmOperationStateType.ROLLING_BACK,
op_state[0])
self.assertEqual(fields.LcmOperationStateType.ROLLED_BACK, op_state[1])
# check grant_req and grant are deleted
self.assertRaises(sol_ex.GrantRequestOrGrantNotFound,
lcmocc_utils.get_grant_req_and_grant, self.context, lcmocc)
@mock.patch.object(nfvo_client.NfvoClient, 'send_lcmocc_notification')
@mock.patch.object(nfvo_client.NfvoClient, 'get_vnfd')
@mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'post_grant')
@mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'rollback')
def test_rollback_lcm_op_failed_temp(self, mocked_rollback,
mocked_post_grant, mocked_get_vnfd,
mocked_send_lcmocc_notification):
# prepare
lcmocc = self._create_inst_and_lcmocc(
op_state=fields.LcmOperationStateType.FAILED_TEMP)
self._create_grant_req_and_grant(lcmocc)
mocked_get_vnfd.return_value = mock.Mock()
ex = sol_ex.StackOperationFailed(sol_detail="unit test",
sol_title="stack failed")
mocked_rollback.side_effect = ex
op_state = []
def _store_state(context, lcmocc, inst, endpoint):
op_state.append(lcmocc.operationState)
mocked_send_lcmocc_notification.side_effect = _store_state
# run rollback_lcm_op
self.conductor.rollback_lcm_op(self.context, lcmocc.id)
# check operationState transition
self.assertEqual(2, mocked_send_lcmocc_notification.call_count)
self.assertEqual(fields.LcmOperationStateType.ROLLING_BACK,
op_state[0])
self.assertEqual(fields.LcmOperationStateType.FAILED_TEMP, op_state[1])
# check lcmocc.error
# get lcmocc from DB to be sure lcmocc saved to DB
lcmocc = lcmocc_utils.get_lcmocc(self.context, lcmocc.id)
expected = ex.make_problem_details()
self.assertEqual(expected, lcmocc.error.to_dict())
# check grant_req and grant remain
# it's OK if no exception raised
lcmocc_utils.get_grant_req_and_grant(self.context, lcmocc)

View File

@ -183,3 +183,11 @@ class TestVnflcmV2(db_base.SqlTestCase):
self.assertRaises(sol_ex.LcmOpOccNotFailedTemp,
self.controller.lcm_op_occ_retry, request=self.request,
id=lcmocc_id)
def test_rollback_not_failed_temp(self):
_, lcmocc_id = self._create_inst_and_lcmocc('INSTANTIATED',
fields.LcmOperationStateType.COMPLETED)
self.assertRaises(sol_ex.LcmOpOccNotFailedTemp,
self.controller.lcm_op_occ_rollback, request=self.request,
id=lcmocc_id)