diff --git a/releasenotes/notes/add-v2-rollback-api-1b53e7b9c89d5281.yaml b/releasenotes/notes/add-v2-rollback-api-1b53e7b9c89d5281.yaml new file mode 100755 index 000000000..3adea099c --- /dev/null +++ b/releasenotes/notes/add-v2-rollback-api-1b53e7b9c89d5281.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add the Version "2.0.0" of Rollback operation API + based on ETSI NFV specifications. \ No newline at end of file diff --git a/tacker/sol_refactored/api/policies/vnflcm_v2.py b/tacker/sol_refactored/api/policies/vnflcm_v2.py index 981bc7ef4..4f1274d66 100644 --- a/tacker/sol_refactored/api/policies/vnflcm_v2.py +++ b/tacker/sol_refactored/api/policies/vnflcm_v2.py @@ -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 diff --git a/tacker/sol_refactored/api/router.py b/tacker/sol_refactored/api/router.py index 242450b95..bced19106 100644 --- a/tacker/sol_refactored/api/router.py +++ b/tacker/sol_refactored/api/router.py @@ -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 diff --git a/tacker/sol_refactored/common/exceptions.py b/tacker/sol_refactored/common/exceptions.py index ddf9e349f..ef299b8f9 100644 --- a/tacker/sol_refactored/common/exceptions.py +++ b/tacker/sol_refactored/common/exceptions.py @@ -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.") diff --git a/tacker/sol_refactored/common/lcm_op_occ_utils.py b/tacker/sol_refactored/common/lcm_op_occ_utils.py index 3f36c6209..830ba78c4 100644 --- a/tacker/sol_refactored/common/lcm_op_occ_utils.py +++ b/tacker/sol_refactored/common/lcm_op_occ_utils.py @@ -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 diff --git a/tacker/sol_refactored/conductor/conductor_rpc_v2.py b/tacker/sol_refactored/conductor/conductor_rpc_v2.py index 5f5e69ce2..30b1f8904 100644 --- a/tacker/sol_refactored/conductor/conductor_rpc_v2.py +++ b/tacker/sol_refactored/conductor/conductor_rpc_v2.py @@ -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') diff --git a/tacker/sol_refactored/conductor/conductor_v2.py b/tacker/sol_refactored/conductor/conductor_v2.py index 239a9929e..b8284e92f 100644 --- a/tacker/sol_refactored/conductor/conductor_v2.py +++ b/tacker/sol_refactored/conductor/conductor_v2.py @@ -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) diff --git a/tacker/sol_refactored/conductor/vnflcm_driver_v2.py b/tacker/sol_refactored/conductor/vnflcm_driver_v2.py index e4ecf8b11..2c66b1d5f 100644 --- a/tacker/sol_refactored/conductor/vnflcm_driver_v2.py +++ b/tacker/sol_refactored/conductor/vnflcm_driver_v2.py @@ -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. diff --git a/tacker/sol_refactored/controller/vnflcm_v2.py b/tacker/sol_refactored/controller/vnflcm_v2.py index cd61e3592..6dfbb4b21 100644 --- a/tacker/sol_refactored/controller/vnflcm_v2.py +++ b/tacker/sol_refactored/controller/vnflcm_v2.py @@ -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: diff --git a/tacker/sol_refactored/infra_drivers/openstack/openstack.py b/tacker/sol_refactored/infra_drivers/openstack/openstack.py index de52d7501..9024d6935 100644 --- a/tacker/sol_refactored/infra_drivers/openstack/openstack.py +++ b/tacker/sol_refactored/infra_drivers/openstack/openstack.py @@ -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 diff --git a/tacker/sol_refactored/test-tools/cli.py b/tacker/sol_refactored/test-tools/cli.py index 56d34a780..a25007b98 100644 --- a/tacker/sol_refactored/test-tools/cli.py +++ b/tacker/sol_refactored/test-tools/cli.py @@ -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]) diff --git a/tacker/tests/unit/sol_refactored/conductor/test_conductor_v2.py b/tacker/tests/unit/sol_refactored/conductor/test_conductor_v2.py index 401f5e970..4a2b20433 100644 --- a/tacker/tests/unit/sol_refactored/conductor/test_conductor_v2.py +++ b/tacker/tests/unit/sol_refactored/conductor/test_conductor_v2.py @@ -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) diff --git a/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py b/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py index 89357308d..d554b8ef9 100644 --- a/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py +++ b/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py @@ -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)