From bf51c2bc800f6511a7756eeccddd7d18b7bc8b69 Mon Sep 17 00:00:00 2001 From: Itsuro Oda Date: Mon, 1 Nov 2021 00:34:46 +0000 Subject: [PATCH] support retry operation task of v2 API This patch implements retry operation task defined in ETSI NFV-SOL003 v3.3.1 5.4.14. Basically retry operation is not task API specific. Thus retry operation of a task API which is added in the future (ex. scale) will be supported naturally. Functional tests will be provided with another patch. Implements: blueprint support-nfv-solv3-error-handling Change-Id: I2717540f042882363cce3d1a0da22c71789a36cb --- .../add-v2-retry-api-34667d944db1f54c.yaml | 5 + ...ff50553e9d3_add_grant_and_grant_request.py | 78 +++++ .../alembic_migrations/versions/HEAD | 2 +- .../sol_refactored/api/policies/vnflcm_v2.py | 9 + tacker/sol_refactored/api/router.py | 1 + tacker/sol_refactored/common/exceptions.py | 9 + .../sol_refactored/common/lcm_op_occ_utils.py | 32 ++- .../conductor/conductor_rpc_v2.py | 10 +- .../sol_refactored/conductor/conductor_v2.py | 88 ++++-- .../conductor/vnflcm_driver_v2.py | 133 +++++---- tacker/sol_refactored/controller/vnflcm_v2.py | 23 +- tacker/sol_refactored/db/api.py | 24 -- tacker/sol_refactored/db/sqlalchemy/models.py | 54 ++++ .../infra_drivers/openstack/heat_utils.py | 22 +- .../infra_drivers/openstack/openstack.py | 15 +- tacker/sol_refactored/objects/__init__.py | 6 +- tacker/sol_refactored/objects/base.py | 15 +- .../objects/v1/grant_request.py | 3 +- tacker/sol_refactored/test-tools/cli.py | 80 +++--- tacker/tests/functional/sol_v2/base_v2.py | 2 +- .../conductor/test_conductor_v2.py | 266 ++++++++++++++++++ .../controller/test_vnflcm_v2.py | 97 ++++++- 22 files changed, 806 insertions(+), 168 deletions(-) create mode 100755 releasenotes/notes/add-v2-retry-api-34667d944db1f54c.yaml create mode 100644 tacker/db/migration/alembic_migrations/versions/3ff50553e9d3_add_grant_and_grant_request.py delete mode 100644 tacker/sol_refactored/db/api.py create mode 100644 tacker/tests/unit/sol_refactored/conductor/test_conductor_v2.py diff --git a/releasenotes/notes/add-v2-retry-api-34667d944db1f54c.yaml b/releasenotes/notes/add-v2-retry-api-34667d944db1f54c.yaml new file mode 100755 index 000000000..894c869cf --- /dev/null +++ b/releasenotes/notes/add-v2-retry-api-34667d944db1f54c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add the Version "2.0.0" of Retry operation API + based on ETSI NFV specifications. \ No newline at end of file diff --git a/tacker/db/migration/alembic_migrations/versions/3ff50553e9d3_add_grant_and_grant_request.py b/tacker/db/migration/alembic_migrations/versions/3ff50553e9d3_add_grant_and_grant_request.py new file mode 100644 index 000000000..15aced9eb --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/3ff50553e9d3_add_grant_and_grant_request.py @@ -0,0 +1,78 @@ +# Copyright 2021 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""add grant and grant request + +Revision ID: 3ff50553e9d3 +Revises: 70df18f71ba2 +Create Date: 2021-10-07 03:57:25.430532 + +""" + +# flake8: noqa: E402 + +# revision identifiers, used by Alembic. +revision = '3ff50553e9d3' +down_revision = '70df18f71ba2' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(active_plugins=None, options=None): + op.create_table('GrantV1', + sa.Column('id', sa.String(length=255), nullable=False), + sa.Column('vnfInstanceId', sa.String(length=255), nullable=False), + sa.Column('vnfLcmOpOccId', sa.String(length=255), nullable=False), + sa.Column('vimConnectionInfo', sa.JSON(), nullable=True), + sa.Column('zones', sa.JSON(), nullable=True), + sa.Column('zoneGroups', sa.JSON(), nullable=True), + sa.Column('addResources', sa.JSON(), nullable=True), + sa.Column('tempResources', sa.JSON(), nullable=True), + sa.Column('removeResources', sa.JSON(), nullable=True), + sa.Column('updateResources', sa.JSON(), nullable=True), + sa.Column('vimAssets', sa.JSON(), nullable=True), + sa.Column('extVirtualLinks', sa.JSON(), nullable=True), + sa.Column('extManagedVirtualLinks', sa.JSON(), nullable=True), + sa.Column('additionalParams', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + + op.create_table('GrantRequestV1', + sa.Column('vnfInstanceId', sa.String(length=255), nullable=False), + sa.Column('vnfLcmOpOccId', sa.String(length=255), nullable=False), + sa.Column('vnfdId', sa.String(length=255), nullable=False), + sa.Column('dstVnfdId', sa.String(length=255), nullable=True), + sa.Column('flavourId', sa.String(length=255), nullable=True), + sa.Column('operation', + sa.Enum('INSTANTIATE', 'SCALE', 'SCALE_TO_LEVEL', + 'CHANGE_FLAVOUR', 'TERMINATE', 'HEAL', 'OPERATE', + 'CHANGE_EXT_CONN', 'CREATE_SNAPSHOT', + 'REVERT_TO_SNAPSHOT', 'CHANGE_VNFPKG'), + nullable=False), + sa.Column('isAutomaticInvocation', sa.Boolean(), nullable=False), + sa.Column('instantiationLevelId', sa.String(length=255), + nullable=True), + sa.Column('addResources', sa.JSON(), nullable=True), + sa.Column('tempResources', sa.JSON(), nullable=True), + sa.Column('removeResources', sa.JSON(), nullable=True), + sa.Column('updateResources', sa.JSON(), nullable=True), + sa.Column('placementConstraints', sa.JSON(), nullable=True), + sa.Column('vimConstraints', sa.JSON(), nullable=True), + sa.Column('additionalParams', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('vnfLcmOpOccId'), + mysql_engine='InnoDB' + ) diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index c60df71fd..b819caf90 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -70df18f71ba2 +3ff50553e9d3 diff --git a/tacker/sol_refactored/api/policies/vnflcm_v2.py b/tacker/sol_refactored/api/policies/vnflcm_v2.py index adccd38ea..981bc7ef4 100644 --- a/tacker/sol_refactored/api/policies/vnflcm_v2.py +++ b/tacker/sol_refactored/api/policies/vnflcm_v2.py @@ -150,6 +150,15 @@ rules = [ 'path': VNF_LCM_OP_OCCS_ID_PATH} ] ), + policy.DocumentedRuleDefault( + name=POLICY_NAME.format('lcm_op_occ_retry'), + check_str=RULE_ANY, + description="Retry VnfLcmOpOcc.", + operations=[ + {'method': 'POST', + 'path': VNF_LCM_OP_OCCS_ID_PATH + '/retry'} + ] + ), # 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 0c44acd92..242450b95 100644 --- a/tacker/sol_refactored/api/router.py +++ b/tacker/sol_refactored/api/router.py @@ -45,6 +45,7 @@ class VnflcmAPIRouterV2(sol_wsgi.SolAPIRouter): ("/subscriptions/{id}", {"GET": "subscription_show", "DELETE": "subscription_delete"}), ("/vnf_lcm_op_occs", {"GET": "lcm_op_occ_list"}), + ("/vnf_lcm_op_occs/{id}/retry", {"POST": "lcm_op_occ_retry"}), # 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 5a79134c7..ddf9e349f 100644 --- a/tacker/sol_refactored/common/exceptions.py +++ b/tacker/sol_refactored/common/exceptions.py @@ -210,3 +210,12 @@ class ResponseTooBig(SolHttpError400): class LocalNfvoGrantFailed(SolHttpError403): title = 'Grant failed' # detail set in the code + + +class LcmOpOccNotFailedTemp(SolHttpError409): + message = _("LCM operation %(lcmocc_id)s not FAILED_TEMP.") + + +class GrantRequestOrGrantNotFound(SolHttpError404): + message = _("GrantRequest or Grant for LCM operation " + "%(lcmocc_id)s not found.") diff --git a/tacker/sol_refactored/common/lcm_op_occ_utils.py b/tacker/sol_refactored/common/lcm_op_occ_utils.py index df8367679..3f36c6209 100644 --- a/tacker/sol_refactored/common/lcm_op_occ_utils.py +++ b/tacker/sol_refactored/common/lcm_op_occ_utils.py @@ -44,15 +44,21 @@ def lcmocc_href(lcmocc_id, endpoint): return "{}/v2/vnflcm/vnf_lcm_op_occs/{}".format(endpoint, lcmocc_id) +def lcmocc_task_href(lcmocc_id, task, endpoint): + return "{}/v2/vnflcm/vnf_lcm_op_occs/{}/{}".format(endpoint, lcmocc_id, + task) + + def make_lcmocc_links(lcmocc, endpoint): links = objects.VnfLcmOpOccV2_Links() links.self = objects.Link(href=lcmocc_href(lcmocc.id, endpoint)) links.vnfInstance = objects.Link( href=inst_utils.inst_href(lcmocc.vnfInstanceId, endpoint)) + links.retry = objects.Link( + href=lcmocc_task_href(lcmocc.vnfInstanceId, 'retry', endpoint)) # TODO(oda-g): add when implemented # links.grant # links.cancel - # links.retry # links.rollback # links.fail # links.vnfSnapshot @@ -179,3 +185,27 @@ def make_instantiate_lcmocc(lcmocc, inst): def make_terminate_lcmocc(lcmocc, inst): _make_instantiate_lcmocc(lcmocc, inst, 'REMOVED') + + +def get_grant_req_and_grant(context, lcmocc): + grant_reqs = objects.GrantRequestV1.get_by_filter(context, + vnfLcmOpOccId=lcmocc.id) + grant = objects.GrantV1.get_by_id(context, lcmocc.grantId) + if not grant_reqs or grant is None: + raise sol_ex.GrantRequestOrGrantNotFound(lcmocc_id=lcmocc.id) + + # len(grant_reqs) == 1 because vnfLcmOpOccId is primary key. + return grant_reqs[0], grant + + +def check_lcmocc_in_progress(context, inst_id): + # if the controller or conductor executes an operation for the vnf + # instance (i.e. operationState is ...ING), other operation for + # the same vnf instance is exculed by the coordinator. + # check here is existence of lcmocc for the vnf instance with + # FAILED_TEMP operationState. + lcmoccs = objects.VnfLcmOpOccV2.get_by_filter( + context, vnfInstanceId=inst_id, + operationState=fields.LcmOperationStateType.FAILED_TEMP) + if lcmoccs: + raise sol_ex.OtherOperationInProgress(inst_id=inst_id) diff --git a/tacker/sol_refactored/conductor/conductor_rpc_v2.py b/tacker/sol_refactored/conductor/conductor_rpc_v2.py index a79b12589..5f5e69ce2 100644 --- a/tacker/sol_refactored/conductor/conductor_rpc_v2.py +++ b/tacker/sol_refactored/conductor/conductor_rpc_v2.py @@ -31,10 +31,16 @@ class VnfLcmRpcApiV2(object): fanout=False, version='1.0') - def start_lcm_op(self, context, lcmocc_id): + def _cast_lcm_op(self, context, lcmocc_id, method): serializer = objects_base.TackerObjectSerializer() client = rpc.get_client(self.target, version_cap=None, serializer=serializer) cctxt = client.prepare() - cctxt.cast(context, 'start_lcm_op', lcmocc_id=lcmocc_id) + cctxt.cast(context, method, lcmocc_id=lcmocc_id) + + def start_lcm_op(self, context, lcmocc_id): + self._cast_lcm_op(context, lcmocc_id, 'start_lcm_op') + + def retry_lcm_op(self, context, lcmocc_id): + self._cast_lcm_op(context, lcmocc_id, 'retry_lcm_op') diff --git a/tacker/sol_refactored/conductor/conductor_v2.py b/tacker/sol_refactored/conductor/conductor_v2.py index 7e6cd7220..239a9929e 100644 --- a/tacker/sol_refactored/conductor/conductor_v2.py +++ b/tacker/sol_refactored/conductor/conductor_v2.py @@ -39,10 +39,6 @@ class ConductorV2(object): self.endpoint = CONF.v2_vnfm.endpoint self.nfvo_client = nfvo_client.NfvoClient() - def _get_lcm_op_method(self, op, postfix): - method = getattr(self.vnflcm_driver, "%s_%s" % (op.lower(), postfix)) - return method - def _set_lcmocc_error(self, lcmocc, ex): if isinstance(ex, sol_ex.SolException): problem_details = ex.make_problem_details() @@ -84,10 +80,13 @@ class ConductorV2(object): # NOTE: perform grant exchange mainly but also perform # something to do at STATING phase ex. request check. - grant_method = self._get_lcm_op_method(lcmocc.operation, 'grant') - grant_req, grant = grant_method(context, lcmocc, inst, vnfd) + grant_req, grant = self.vnflcm_driver.grant(context, lcmocc, + inst, vnfd) + self.vnflcm_driver.post_grant(context, lcmocc, inst, grant_req, + grant, vnfd) lcmocc.operationState = fields.LcmOperationStateType.PROCESSING + lcmocc.grantId = grant.id lcmocc.update(context) except Exception as ex: LOG.exception("STARTING %s failed", lcmocc.operation) @@ -103,17 +102,8 @@ class ConductorV2(object): return try: - # perform preamble LCM script - start_method = self._get_lcm_op_method(lcmocc.operation, 'start') - start_method(context, lcmocc, inst, grant_req, grant, vnfd) - - process_method = self._get_lcm_op_method(lcmocc.operation, - 'process') - process_method(context, lcmocc, inst, grant_req, grant, vnfd) - - # perform postamble LCM script - end_method = self._get_lcm_op_method(lcmocc.operation, 'end') - end_method(context, lcmocc, inst, grant_req, grant, vnfd) + self.vnflcm_driver.process(context, lcmocc, inst, grant_req, + grant, vnfd) lcmocc.operationState = fields.LcmOperationStateType.COMPLETED # update inst and lcmocc at the same time @@ -124,7 +114,69 @@ class ConductorV2(object): LOG.exception("PROCESSING %s failed", lcmocc.operation) lcmocc.operationState = fields.LcmOperationStateType.FAILED_TEMP self._set_lcmocc_error(lcmocc, ex) - lcmocc.update(context) + with context.session.begin(subtransactions=True): + # save grant_req and grant to be used when retry + # NOTE: grant_req is saved because it is necessary to interpret + # the contents of grant. Though grant can be gotten from NFVO, + # it is saved here with grant_req so that it is not necessary + # to communicate with NFVO when retry. They are saved temporary + # and will be deleted when operationState becomes an end state + # (COMPLETED/FAILED/ROLLED_BACK). + grant_req.create(context) + grant.create(context) + lcmocc.update(context) + + # send notification COMPLETED or FAILED_TEMP + self.nfvo_client.send_lcmocc_notification(context, lcmocc, inst, + self.endpoint) + + @log.log + def retry_lcm_op(self, context, lcmocc_id): + lcmocc = lcmocc_utils.get_lcmocc(context, lcmocc_id) + + self._retry_lcm_op(context, lcmocc) + + @coordinate.lock_vnf_instance('{lcmocc.vnfInstanceId}', delay=True) + def _retry_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.PROCESSING + lcmocc.update(context) + # send notification PROCESSING + self.nfvo_client.send_lcmocc_notification(context, lcmocc, inst, + self.endpoint) + + try: + vnfd = self.nfvo_client.get_vnfd(context, inst.vnfdId, + all_contents=True) + 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.process(context, lcmocc, inst, grant_req, + grant, vnfd) + + lcmocc.operationState = fields.LcmOperationStateType.COMPLETED + lcmocc.error = None # clear error + # update inst and lcmocc at the same time + with context.session.begin(subtransactions=True): + inst.update(context) + 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("PROCESSING %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 COMPLETED or FAILED_TEMP self.nfvo_client.send_lcmocc_notification(context, lcmocc, inst, diff --git a/tacker/sol_refactored/conductor/vnflcm_driver_v2.py b/tacker/sol_refactored/conductor/vnflcm_driver_v2.py index c350bad3d..a779e3dd2 100644 --- a/tacker/sol_refactored/conductor/vnflcm_driver_v2.py +++ b/tacker/sol_refactored/conductor/vnflcm_driver_v2.py @@ -28,6 +28,7 @@ from tacker.sol_refactored.common import vnf_instance_utils as inst_utils from tacker.sol_refactored.infra_drivers.openstack import openstack from tacker.sol_refactored.nfvo import nfvo_client from tacker.sol_refactored import objects +from tacker.sol_refactored.objects.v2 import fields as v2fields LOG = logging.getLogger(__name__) @@ -41,6 +42,68 @@ class VnfLcmDriverV2(object): self.endpoint = CONF.v2_vnfm.endpoint self.nfvo_client = nfvo_client.NfvoClient() + def grant(self, context, lcmocc, inst, vnfd): + method = getattr(self, "%s_%s" % (lcmocc.operation.lower(), 'grant')) + return method(context, lcmocc, inst, vnfd) + + def post_grant(self, context, lcmocc, inst, grant_req, grant, vnfd): + method = getattr(self, + "%s_%s" % (lcmocc.operation.lower(), 'post_grant'), + None) + if method: + method(context, lcmocc, inst, grant_req, grant, vnfd) + + def _exec_mgmt_driver_script(self, operation, flavour_id, req, inst, + grant_req, grant, vnfd): + script = vnfd.get_interface_script(flavour_id, operation) + if script is None: + return + + tmp_csar_dir = vnfd.make_tmp_csar_dir() + script_dict = { + 'operation': operation, + 'request': req.to_dict(), + 'vnf_instance': inst.to_dict(), + 'grant_request': grant_req.to_dict(), + 'grant_response': grant.to_dict(), + 'tmp_csar_dir': tmp_csar_dir + } + # script is relative path to Definitions/xxx.yaml + script_path = os.path.join(tmp_csar_dir, "Definitions", script) + + out = subprocess.run(["python3", script_path], + input=pickle.dumps(script_dict), + capture_output=True) + + vnfd.remove_tmp_csar_dir(tmp_csar_dir) + + if out.returncode != 0: + LOG.debug("execute %s failed: %s", operation, out.stderr) + msg = "{} failed: {}".format(operation, out.stderr) + raise sol_ex.MgmtDriverExecutionFailed(sol_detail=msg) + + LOG.debug("execute %s of %s success.", operation, script) + + def process(self, context, lcmocc, inst, grant_req, grant, vnfd): + # perform preamble LCM script + req = lcmocc.operationParams + operation = "%s_%s" % (lcmocc.operation.lower(), 'start') + if lcmocc.operation == v2fields.LcmOperationType.INSTANTIATE: + flavour_id = req.flavourId + else: + flavour_id = inst.instantiatedVnfInfo.flavourId + self._exec_mgmt_driver_script(operation, + flavour_id, req, inst, grant_req, grant, vnfd) + + # main process + method = getattr(self, "%s_%s" % (lcmocc.operation.lower(), 'process')) + method(context, lcmocc, inst, grant_req, grant, vnfd) + + # perform postamble LCM script + operation = "%s_%s" % (lcmocc.operation.lower(), 'end') + self._exec_mgmt_driver_script(operation, + flavour_id, req, inst, grant_req, grant, vnfd) + def _get_link_ports(self, inst_req): names = [] if inst_req.obj_attr_is_set('extVirtualLinks'): @@ -167,19 +230,24 @@ class VnfLcmDriverV2(object): href=inst_utils.inst_href(inst.id, self.endpoint))) # NOTE: if not granted, 403 error raised. - grant_res = self.nfvo_client.grant(context, grant_req) + grant = self.nfvo_client.grant(context, grant_req) + return grant_req, grant + + def instantiate_post_grant(self, context, lcmocc, inst, grant_req, + grant, vnfd): # set inst vimConnectionInfo + req = lcmocc.operationParams vim_infos = {} if req.obj_attr_is_set('vimConnectionInfo'): vim_infos = req.vimConnectionInfo - if grant_res.obj_attr_is_set('vimConnectionInfo'): + if grant.obj_attr_is_set('vimConnectionInfo'): # if NFVO returns vimConnectionInfo use it. # As the controller does for req.vimConnectionInfo, if accessInfo # or interfaceInfo is not specified, get them from VIM DB. # vimId must be in VIM DB. - res_vim_infos = grant_res.vimConnectioninfo + res_vim_infos = grant.vimConnectioninfo for key, res_vim_info in res_vim_infos.items(): if not (res_vim_info.obj_attr_is_set('accessInfo') and res_vim_info.obj_attr_is_set('interfaceInfo')): @@ -199,8 +267,6 @@ class VnfLcmDriverV2(object): inst.vimConnectionInfo = vim_infos - return grant_req, grant_res - def instantiate_process(self, context, lcmocc, inst, grant_req, grant, vnfd): req = lcmocc.operationParams @@ -215,49 +281,6 @@ class VnfLcmDriverV2(object): inst.instantiationState = 'INSTANTIATED' lcmocc_utils.make_instantiate_lcmocc(lcmocc, inst) - def _exec_mgmt_driver_script(self, operation, flavour_id, req, inst, - grant_req, grant, vnfd): - script = vnfd.get_interface_script(flavour_id, operation) - if script is None: - return - - tmp_csar_dir = vnfd.make_tmp_csar_dir() - script_dict = { - 'operation': operation, - 'request': req.to_dict(), - 'vnf_instance': inst.to_dict(), - 'grant_request': grant_req.to_dict(), - 'grant_response': grant.to_dict(), - 'tmp_csar_dir': tmp_csar_dir - } - # script is relative path to Definitions/xxx.yaml - script_path = os.path.join(tmp_csar_dir, "Definitions", script) - - out = subprocess.run(["python3", script_path], - input=pickle.dumps(script_dict), - capture_output=True) - - vnfd.remove_tmp_csar_dir(tmp_csar_dir) - - if out.returncode != 0: - LOG.debug("execute %s failed: %s", operation, out.stderr) - msg = "{} failed: {}".format(operation, out.stderr) - raise sol_ex.MgmtDriverExecutionFailed(sol_detail=msg) - - LOG.debug("execute %s of %s success.", operation, script) - - def instantiate_start(self, context, lcmocc, inst, grant_req, - grant, vnfd): - req = lcmocc.operationParams - self._exec_mgmt_driver_script('instantiate_start', - req.flavourId, req, inst, grant_req, grant, vnfd) - - def instantiate_end(self, context, lcmocc, inst, grant_req, - grant, vnfd): - req = lcmocc.operationParams - self._exec_mgmt_driver_script('instantiate_end', - req.flavourId, req, inst, grant_req, grant, vnfd) - def terminate_grant(self, context, lcmocc, inst, vnfd): # grant exchange # NOTE: the api_version of NFVO supposes 1.4.0 at the moment. @@ -375,17 +398,3 @@ class VnfLcmDriverV2(object): # reset vimConnectionInfo inst.vimConnectionInfo = {} - - def terminate_start(self, context, lcmocc, inst, grant_req, - grant, vnfd): - req = lcmocc.operationParams - flavour_id = inst.instantiatedVnfInfo.flavourId - self._exec_mgmt_driver_script('terminate_start', - flavour_id, req, inst, grant_req, grant, vnfd) - - def terminate_end(self, context, lcmocc, inst, grant_req, - grant, vnfd): - req = lcmocc.operationParams - flavour_id = inst.instantiatedVnfInfo.flavourId - self._exec_mgmt_driver_script('terminate_end', - flavour_id, req, inst, grant_req, grant, vnfd) diff --git a/tacker/sol_refactored/controller/vnflcm_v2.py b/tacker/sol_refactored/controller/vnflcm_v2.py index 7fe231110..cd61e3592 100644 --- a/tacker/sol_refactored/controller/vnflcm_v2.py +++ b/tacker/sol_refactored/controller/vnflcm_v2.py @@ -129,11 +129,13 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): @coordinate.lock_vnf_instance('{id}') def delete(self, request, id): context = request.context - inst = inst_utils.get_inst(request.context, id) + inst = inst_utils.get_inst(context, id) if inst.instantiationState != 'NOT_INSTANTIATED': raise sol_ex.VnfInstanceIsInstantiated(inst_id=id) + lcmocc_utils.check_lcmocc_in_progress(context, id) + inst.delete(context) # NOTE: inst record in DB deleted but inst object still @@ -151,6 +153,8 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): if inst.instantiationState != 'NOT_INSTANTIATED': raise sol_ex.VnfInstanceIsInstantiated(inst_id=id) + lcmocc_utils.check_lcmocc_in_progress(context, id) + now = datetime.utcnow() lcmocc = objects.VnfLcmOpOccV2( id=uuidutils.generate_uuid(), @@ -192,6 +196,8 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): if inst.instantiationState != 'INSTANTIATED': raise sol_ex.VnfInstanceIsNotInstantiated(inst_id=id) + lcmocc_utils.check_lcmocc_in_progress(context, id) + now = datetime.utcnow() lcmocc = objects.VnfLcmOpOccV2( id=uuidutils.generate_uuid(), @@ -323,6 +329,21 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController): return sol_wsgi.SolResponse(200, resp_body) + def lcm_op_occ_retry(self, request, id): + context = request.context + lcmocc = lcmocc_utils.get_lcmocc(context, id) + + return self._lcm_op_occ_retry(context, lcmocc) + + @coordinate.lock_vnf_instance('{lcmocc.vnfInstanceId}') + def _lcm_op_occ_retry(self, context, lcmocc): + if lcmocc.operationState != v2fields.LcmOperationStateType.FAILED_TEMP: + raise sol_ex.LcmOpOccNotFailedTemp(lcmocc_id=lcmocc.id) + + self.conductor_rpc.retry_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/db/api.py b/tacker/sol_refactored/db/api.py deleted file mode 100644 index 5ca0d1820..000000000 --- a/tacker/sol_refactored/db/api.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (C) 2021 Nippon Telegraph and Telephone Corporation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from oslo_config import cfg -from oslo_db.sqlalchemy import enginefacade - - -context_manager = enginefacade.transaction_context() - - -def configure(): - context_manager.configure(sqlite_fk=True, **cfg.CONF.database) diff --git a/tacker/sol_refactored/db/sqlalchemy/models.py b/tacker/sol_refactored/db/sqlalchemy/models.py index c17e3139b..331496699 100644 --- a/tacker/sol_refactored/db/sqlalchemy/models.py +++ b/tacker/sol_refactored/db/sqlalchemy/models.py @@ -98,3 +98,57 @@ class VnfLcmOpOccV2(model_base.BASE): changedExtConnectivity = sa.Column(sa.JSON(), nullable=True) modificationsTriggeredByVnfPkgChange = sa.Column(sa.JSON(), nullable=True) vnfSnapshotInfoId = sa.Column(sa.String(255), nullable=True) + + +class GrantV1(model_base.BASE): + """Type: Grant + + NFV-SOL 003 + - v3.3.1 9.5.2.3 (API version: 1.4.0) + """ + + __tablename__ = 'GrantV1' + id = sa.Column(sa.String(255), nullable=False, primary_key=True) + vnfInstanceId = sa.Column(sa.String(255), nullable=False) + vnfLcmOpOccId = sa.Column(sa.String(255), nullable=False) + vimConnectionInfo = sa.Column(sa.JSON(), nullable=True) + zones = sa.Column(sa.JSON(), nullable=True) + zoneGroups = sa.Column(sa.JSON(), nullable=True) + addResources = sa.Column(sa.JSON(), nullable=True) + tempResources = sa.Column(sa.JSON(), nullable=True) + removeResources = sa.Column(sa.JSON(), nullable=True) + updateResources = sa.Column(sa.JSON(), nullable=True) + vimAssets = sa.Column(sa.JSON(), nullable=True) + extVirtualLinks = sa.Column(sa.JSON(), nullable=True) + extManagedVirtualLinks = sa.Column(sa.JSON(), nullable=True) + additionalParams = sa.Column(sa.JSON(), nullable=True) + + +class GrantRequestV1(model_base.BASE): + """Type: GrantRequest + + NFV-SOL 003 + - v3.3.1 9.5.2.2 (API version: 1.4.0) + """ + + __tablename__ = 'GrantRequestV1' + vnfInstanceId = sa.Column(sa.String(255), nullable=False) + vnfLcmOpOccId = sa.Column(sa.String(255), nullable=False, primary_key=True) + vnfdId = sa.Column(sa.String(255), nullable=False) + dstVnfdId = sa.Column(sa.String(255), nullable=True) + flavourId = sa.Column(sa.String(255), nullable=True) + operation = sa.Column(sa.Enum( + 'INSTANTIATE', 'SCALE', 'SCALE_TO_LEVEL', 'CHANGE_FLAVOUR', + 'TERMINATE', 'HEAL', 'OPERATE', 'CHANGE_EXT_CONN', + 'CREATE_SNAPSHOT', 'REVERT_TO_SNAPSHOT', 'CHANGE_VNFPKG', + create_constraint=True, validate_strings=True), + nullable=False) + isAutomaticInvocation = sa.Column(sa.Boolean, nullable=False) + instantiationLevelId = sa.Column(sa.String(255), nullable=True) + addResources = sa.Column(sa.JSON(), nullable=True) + tempResources = sa.Column(sa.JSON(), nullable=True) + removeResources = sa.Column(sa.JSON(), nullable=True) + updateResources = sa.Column(sa.JSON(), nullable=True) + placementConstraints = sa.Column(sa.JSON(), nullable=True) + vimConstraints = sa.Column(sa.JSON(), nullable=True) + additionalParams = sa.Column(sa.JSON(), nullable=True) diff --git a/tacker/sol_refactored/infra_drivers/openstack/heat_utils.py b/tacker/sol_refactored/infra_drivers/openstack/heat_utils.py index 905110497..72158c153 100644 --- a/tacker/sol_refactored/infra_drivers/openstack/heat_utils.py +++ b/tacker/sol_refactored/infra_drivers/openstack/heat_utils.py @@ -40,16 +40,30 @@ class HeatClient(object): self.client = http_client.HttpClient(auth, service_type='orchestration') - def create_stack(self, fields): + def create_stack(self, fields, wait=True): path = "stacks" resp, body = self.client.do_request(path, "POST", expected_status=[201], body=fields) - def delete_stack(self, stack_name): + if wait: + self.wait_stack_create(fields["stack_name"]) + + def update_stack(self, stack_name, fields, wait=True): + path = "stacks/{}".format(stack_name) + resp, body = self.client.do_request(path, "PATCH", + expected_status=[202], body=fields) + + if wait: + self.wait_stack_update(stack_name) + + def delete_stack(self, stack_name, wait=True): path = "stacks/{}".format(stack_name) resp, body = self.client.do_request(path, "DELETE", expected_status=[204, 404]) + if wait: + self.wait_stack_delete(stack_name) + def get_status(self, stack_name): path = "stacks/{}".format(stack_name) resp, body = self.client.do_request(path, "GET", @@ -100,6 +114,10 @@ class HeatClient(object): self._wait_completion(stack_name, "Stack create", "CREATE_COMPLETE", "CREATE_IN_PROGRESS", "CREATE_FAILED") + def wait_stack_update(self, stack_name): + self._wait_completion(stack_name, "Stack update", + "UPDATE_COMPLETE", "UPDATE_IN_PROGRESS", "UPDATE_FAILED") + def wait_stack_delete(self, stack_name): self._wait_completion(stack_name, "Stack delete", "DELETE_COMPLETE", "DELETE_IN_PROGRESS", "DELETE_FAILED", diff --git a/tacker/sol_refactored/infra_drivers/openstack/openstack.py b/tacker/sol_refactored/infra_drivers/openstack/openstack.py index 7956151cd..de52d7501 100644 --- a/tacker/sol_refactored/infra_drivers/openstack/openstack.py +++ b/tacker/sol_refactored/infra_drivers/openstack/openstack.py @@ -46,14 +46,16 @@ class Openstack(object): LOG.debug("stack fields: %s", fields) - # create stack + stack_name = fields['stack_name'] + + # create or update stack vim_info = inst_utils.select_vim_info(inst.vimConnectionInfo) heat_client = heat_utils.HeatClient(vim_info) - heat_client.create_stack(fields) - - # wait stack created - stack_name = fields['stack_name'] - heat_client.wait_stack_create(stack_name) + status, _ = heat_client.get_status(stack_name) + if status is None: + heat_client.create_stack(fields) + else: + heat_client.update_stack(stack_name, fields) # get stack resource heat_reses = heat_client.get_resources(stack_name) @@ -532,4 +534,3 @@ class Openstack(object): heat_client = heat_utils.HeatClient(vim_info) stack_name = heat_utils.get_stack_name(inst) heat_client.delete_stack(stack_name) - heat_client.wait_stack_delete(stack_name) diff --git a/tacker/sol_refactored/objects/__init__.py b/tacker/sol_refactored/objects/__init__.py index 80748cd1e..da739f739 100644 --- a/tacker/sol_refactored/objects/__init__.py +++ b/tacker/sol_refactored/objects/__init__.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -from tacker.sol_refactored.db import api as db_api from tacker.sol_refactored.objects.common import fields # noqa # NOTE: You may scratch your head as you see code that imports @@ -24,7 +23,7 @@ from tacker.sol_refactored.objects.common import fields # noqa # the object. -def register_all(init_db=True): +def register_all(): # NOTE: You must make sure your object gets imported in this # function in order for it to be registered by services that may # need to receive it via RPC. @@ -120,6 +119,3 @@ def register_all(init_db=True): __import__(objects_root + '.v2.vnf_snapshot') __import__(objects_root + '.v2.vnf_state_snapshot_info') __import__(objects_root + '.v2.vnf_virtual_link_resource_info') - - if init_db: - db_api.configure() diff --git a/tacker/sol_refactored/objects/base.py b/tacker/sol_refactored/objects/base.py index 7dcb40684..df6f80023 100644 --- a/tacker/sol_refactored/objects/base.py +++ b/tacker/sol_refactored/objects/base.py @@ -25,7 +25,7 @@ from oslo_utils import versionutils from oslo_versionedobjects import base as ovoo_base from oslo_versionedobjects import exception as ovoo_exc -from tacker.sol_refactored.db import api as db_api +from tacker.db import api as db_api from tacker.sol_refactored.db.sqlalchemy import models from tacker.sol_refactored import objects from tacker.sol_refactored.objects import fields as obj_fields @@ -221,6 +221,8 @@ class TackerObject(ovoo_base.VersionedObject): for name, field in self.fields.items(): if not self.obj_attr_is_set(name): continue + if getattr(self, name) is None: + continue if isinstance(field, obj_fields.ObjectField): obj[name] = getattr(self, name).to_dict() elif isinstance(field, obj_fields.ListOfObjectsField): @@ -378,6 +380,14 @@ class TackerPersistentObject(TackerObject): result = query.all() return [cls.from_db_obj(item) for item in result] + @classmethod + @db_api.context_manager.reader + def get_by_filter(cls, context, *args, **kwargs): + model_cls = getattr(models, cls.__name__) + query = context.session.query(model_cls).filter_by(**kwargs) + result = query.all() + return [cls.from_db_obj(item) for item in result] + @classmethod def from_db_obj(cls, db_obj): inst = cls() @@ -417,6 +427,9 @@ class TackerPersistentObject(TackerObject): name_ = get_model_field(name) if not self.obj_attr_is_set(name): continue + if getattr(self, name) is None: + obj[name_] = None + continue if isinstance(field, obj_fields.ObjectField): obj[name_] = getattr(self, name).to_json() elif isinstance(field, obj_fields.ListOfObjectsField): diff --git a/tacker/sol_refactored/objects/v1/grant_request.py b/tacker/sol_refactored/objects/v1/grant_request.py index 0d4ff00a3..3bf4da833 100644 --- a/tacker/sol_refactored/objects/v1/grant_request.py +++ b/tacker/sol_refactored/objects/v1/grant_request.py @@ -21,7 +21,8 @@ from tacker.sol_refactored.objects.v1 import fields as v1fields # NFV-SOL 003 # - v3.3.1 9.5.2.2 (API version: 1.4.0) @base.TackerObjectRegistry.register -class GrantRequestV1(base.TackerObject, base.TackerObjectDictCompat): +class GrantRequestV1(base.TackerPersistentObject, + base.TackerObjectDictCompat): # Version 1.0: Initial version VERSION = '1.0' diff --git a/tacker/sol_refactored/test-tools/cli.py b/tacker/sol_refactored/test-tools/cli.py index 5b938c4ea..56d34a780 100644 --- a/tacker/sol_refactored/test-tools/cli.py +++ b/tacker/sol_refactored/test-tools/cli.py @@ -89,6 +89,11 @@ class Client(object): path, "POST", body=req_body, version="2.0.0") self.print(resp, body) + def retry(self, id): + path = self.path + '/' + id + '/retry' + resp, body = self.client.do_request(path, "POST", version="2.0.0") + self.print(resp, body) + def usage(): print("usage: cli resource action [arg...]") @@ -105,67 +110,64 @@ def usage(): print(" lcmocc list [body(path of content)]") print(" lcmocc show {id}") print(" lcmocc delete {id}") + print(" lcmocc retry {id}") os._exit(1) +def get_body(arg): + with open(arg) as fp: + return json.load(fp) + + if __name__ == '__main__': if len(sys.argv) < 3: usage() resource = sys.argv[1] action = sys.argv[2] - if resource not in ["inst", "subsc", "lcmocc"]: - usage() + if resource == "inst": if action not in ["create", "list", "show", "delete", "inst", "term"]: usage() - if resource == "subsc": - if action not in ["create", "list", "show", "delete"]: - usage() - if resource == "lcmocc": - if action not in ["list", "show", "delete"]: - usage() - if action in ["create", "show", "delete"]: - if len(sys.argv) != 4: - usage() - arg1 = sys.argv[3] - elif action in ["inst", "term"]: - if len(sys.argv) != 5: - usage() - arg1 = sys.argv[3] - arg2 = sys.argv[4] - else: # list - arg1 = None - if len(sys.argv) == 4: - arg1 = sys.argv[3] - elif len(sys.argv) != 3: - usage() - - if resource == "inst": client = Client("/vnflcm/v2/vnf_instances") elif resource == "subsc": + if action not in ["create", "list", "show", "delete"]: + usage() client = Client("/vnflcm/v2/subscriptions") elif resource == "lcmocc": + if action not in ["list", "show", "delete", "retry"]: + usage() client = Client("/vnflcm/v2/vnf_lcm_op_occs") + else: + usage() if action == "create": - with open(arg1) as fp: - body = json.load(fp) - client.create(body) + if len(sys.argv) != 4: + usage() + client.create(get_body(sys.argv[3])) elif action == "list": body = None - if arg1 is not None: - with open(arg1) as fp: - body = json.load(fp) + if len(sys.argv) == 4: + body = get_body(sys.argv[3]) + elif len(sys.argv) != 3: + usage() client.list(body) elif action == "show": - client.show(arg1) + if len(sys.argv) != 4: + usage() + client.show(sys.argv[3]) elif action == "delete": - client.delete(arg1) + if len(sys.argv) != 4: + usage() + client.delete(sys.argv[3]) elif action == "inst": - with open(arg2) as fp: - body = json.load(fp) - client.inst(arg1, body) + if len(sys.argv) != 5: + usage() + client.inst(sys.argv[3], get_body(sys.argv[4])) elif action == "term": - with open(arg2) as fp: - body = json.load(fp) - client.term(arg1, body) + if len(sys.argv) != 5: + usage() + client.term(sys.argv[3], get_body(sys.argv[4])) + elif action == "retry": + if len(sys.argv) != 4: + usage() + client.retry(sys.argv[3]) diff --git a/tacker/tests/functional/sol_v2/base_v2.py b/tacker/tests/functional/sol_v2/base_v2.py index a421845a2..d875a18fe 100644 --- a/tacker/tests/functional/sol_v2/base_v2.py +++ b/tacker/tests/functional/sol_v2/base_v2.py @@ -47,7 +47,7 @@ class BaseSolV2Test(base.BaseTestCase): cfg.CONF(args=['--config-file', '/etc/tacker/tacker.conf'], project='tacker', version='%%prog %s' % version.version_info.release_string()) - objects.register_all(False) + objects.register_all() vim_info = cls.get_vim_info() cls.auth_url = vim_info.interfaceInfo['endpoint'] diff --git a/tacker/tests/unit/sol_refactored/conductor/test_conductor_v2.py b/tacker/tests/unit/sol_refactored/conductor/test_conductor_v2.py new file mode 100644 index 000000000..401f5e970 --- /dev/null +++ b/tacker/tests/unit/sol_refactored/conductor/test_conductor_v2.py @@ -0,0 +1,266 @@ +# Copyright (C) 2021 Nippon Telegraph and Telephone Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from datetime import datetime +from unittest import mock + +from oslo_utils import uuidutils + +from tacker import context +from tacker.sol_refactored.common import exceptions as sol_ex +from tacker.sol_refactored.common import lcm_op_occ_utils as lcmocc_utils +from tacker.sol_refactored.conductor import conductor_v2 +from tacker.sol_refactored.conductor import vnflcm_driver_v2 +from tacker.sol_refactored.nfvo import nfvo_client +from tacker.sol_refactored import objects +from tacker.sol_refactored.objects.v2 import fields +from tacker.tests.unit.db import base as db_base + + +class TestConductorV2(db_base.SqlTestCase): + + def setUp(self): + super(TestConductorV2, self).setUp() + objects.register_all() + self.conductor = conductor_v2.ConductorV2() + self.context = context.get_admin_context() + + def _create_inst_and_lcmocc(self, + op_state=fields.LcmOperationStateType.STARTING): + inst = objects.VnfInstanceV2( + # required fields + id=uuidutils.generate_uuid(), + vnfdId=uuidutils.generate_uuid(), + vnfProvider='provider', + vnfProductName='product name', + vnfSoftwareVersion='software version', + vnfdVersion='vnfd version', + instantiationState='NOT_INSTANTIATED' + ) + + req = {"flavourId": "simple"} # instantiate request + lcmocc = objects.VnfLcmOpOccV2( + # required fields + id=uuidutils.generate_uuid(), + operationState=op_state, + stateEnteredTime=datetime.utcnow(), + startTime=datetime.utcnow(), + vnfInstanceId=inst.id, + operation=fields.LcmOperationType.INSTANTIATE, + isAutomaticInvocation=False, + isCancelPending=False, + operationParams=req) + + inst.create(self.context) + lcmocc.create(self.context) + + return lcmocc + + def _make_grant_req_and_grant(self, lcmocc): + grant_req = objects.GrantRequestV1( + # required fields + vnfInstanceId=lcmocc.vnfInstanceId, + vnfLcmOpOccId=lcmocc.id, + vnfdId=uuidutils.generate_uuid(), + operation=lcmocc.operation, + isAutomaticInvocation=lcmocc.isAutomaticInvocation + ) + grant = objects.GrantV1( + # required fields + id=uuidutils.generate_uuid(), + vnfInstanceId=lcmocc.vnfInstanceId, + vnfLcmOpOccId=lcmocc.id + ) + + return grant_req, grant + + def _create_grant_req_and_grant(self, lcmocc): + grant_req, grant = self._make_grant_req_and_grant(lcmocc) + grant_req.create(self.context) + grant.create(self.context) + lcmocc.grantId = grant.id + lcmocc.update(self.context) + + @mock.patch.object(nfvo_client.NfvoClient, 'send_lcmocc_notification') + @mock.patch.object(nfvo_client.NfvoClient, 'get_vnfd') + @mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'grant') + @mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'post_grant') + @mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'process') + def test_start_lcm_op_completed(self, mocked_process, mocked_post_grant, + mocked_grant, mocked_get_vnfd, mocked_send_lcmocc_notification): + # prepare + lcmocc = self._create_inst_and_lcmocc() + mocked_get_vnfd.return_value = mock.Mock() + mocked_grant.return_value = self._make_grant_req_and_grant(lcmocc) + + op_state = [] + + def _store_state(context, lcmocc, inst, endpoint): + op_state.append(lcmocc.operationState) + + mocked_send_lcmocc_notification.side_effect = _store_state + + # run start_lcm_op + self.conductor.start_lcm_op(self.context, lcmocc.id) + + # check operationState transition + self.assertEqual(3, mocked_send_lcmocc_notification.call_count) + self.assertEqual(fields.LcmOperationStateType.STARTING, op_state[0]) + self.assertEqual(fields.LcmOperationStateType.PROCESSING, op_state[1]) + self.assertEqual(fields.LcmOperationStateType.COMPLETED, op_state[2]) + + @mock.patch.object(nfvo_client.NfvoClient, 'send_lcmocc_notification') + @mock.patch.object(nfvo_client.NfvoClient, 'get_vnfd') + @mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'grant') + def test_start_lcm_op_rolled_back(self, + mocked_grant, mocked_get_vnfd, mocked_send_lcmocc_notification): + # prepare + lcmocc = self._create_inst_and_lcmocc() + mocked_get_vnfd.return_value = mock.Mock() + ex = sol_ex.LocalNfvoGrantFailed(sol_detail="unit test") + mocked_grant.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 start_lcm_op + self.conductor.start_lcm_op(self.context, lcmocc.id) + + # check operationState transition + self.assertEqual(2, mocked_send_lcmocc_notification.call_count) + self.assertEqual(fields.LcmOperationStateType.STARTING, op_state[0]) + self.assertEqual(fields.LcmOperationStateType.ROLLED_BACK, 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()) + + @mock.patch.object(nfvo_client.NfvoClient, 'send_lcmocc_notification') + @mock.patch.object(nfvo_client.NfvoClient, 'get_vnfd') + @mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'grant') + @mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'post_grant') + @mock.patch.object(vnflcm_driver_v2.VnfLcmDriverV2, 'process') + def test_start_lcm_op_failed_temp(self, mocked_process, mocked_post_grant, + mocked_grant, mocked_get_vnfd, mocked_send_lcmocc_notification): + # prepare + lcmocc = self._create_inst_and_lcmocc() + mocked_get_vnfd.return_value = mock.Mock() + mocked_grant.return_value = self._make_grant_req_and_grant(lcmocc) + ex = sol_ex.StackOperationFailed(sol_detail="unit test", + sol_title="stack failed") + mocked_process.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 start_lcm_op + self.conductor.start_lcm_op(self.context, lcmocc.id) + + # check operationState transition + self.assertEqual(3, mocked_send_lcmocc_notification.call_count) + self.assertEqual(fields.LcmOperationStateType.STARTING, op_state[0]) + self.assertEqual(fields.LcmOperationStateType.PROCESSING, op_state[1]) + self.assertEqual(fields.LcmOperationStateType.FAILED_TEMP, op_state[2]) + + # 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 are saved to DB + # 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, 'process') + def test_retry_lcm_op_completed(self, mocked_process, 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 retry_lcm_op + self.conductor.retry_lcm_op(self.context, lcmocc.id) + + # check operationState transition + self.assertEqual(2, mocked_send_lcmocc_notification.call_count) + self.assertEqual(fields.LcmOperationStateType.PROCESSING, op_state[0]) + self.assertEqual(fields.LcmOperationStateType.COMPLETED, 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, 'process') + def test_retry_lcm_op_failed_temp(self, mocked_process, 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_process.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 retry_lcm_op + self.conductor.retry_lcm_op(self.context, lcmocc.id) + + # check operationState transition + self.assertEqual(2, mocked_send_lcmocc_notification.call_count) + self.assertEqual(fields.LcmOperationStateType.PROCESSING, 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 93867f068..89357308d 100644 --- a/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py +++ b/tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from datetime import datetime from unittest import mock from oslo_utils import uuidutils @@ -23,6 +24,7 @@ from tacker.sol_refactored.common import exceptions as sol_ex from tacker.sol_refactored.controller import vnflcm_v2 from tacker.sol_refactored.nfvo import nfvo_client from tacker.sol_refactored import objects +from tacker.sol_refactored.objects.v2 import fields from tacker.tests.unit.db import base as db_base @@ -30,11 +32,42 @@ class TestVnflcmV2(db_base.SqlTestCase): def setUp(self): super(TestVnflcmV2, self).setUp() - objects.register_all(False) + objects.register_all() self.controller = vnflcm_v2.VnfLcmControllerV2() + self.context = context.get_admin_context() + self.context.api_version = api_version.APIVersion("2.0.0") self.request = mock.Mock() - self.request.context = context.get_admin_context() - self.request.context.api_version = api_version.APIVersion("2.0.0") + self.request.context = self.context + + def _create_inst_and_lcmocc(self, inst_state, op_state): + inst = objects.VnfInstanceV2( + # required fields + id=uuidutils.generate_uuid(), + vnfdId=uuidutils.generate_uuid(), + vnfProvider='provider', + vnfProductName='product name', + vnfSoftwareVersion='software version', + vnfdVersion='vnfd version', + instantiationState=inst_state + ) + + req = {"flavourId": "simple"} # instantate request + lcmocc = objects.VnfLcmOpOccV2( + # required fields + id=uuidutils.generate_uuid(), + operationState=op_state, + stateEnteredTime=datetime.utcnow(), + startTime=datetime.utcnow(), + vnfInstanceId=inst.id, + operation=fields.LcmOperationType.INSTANTIATE, + isAutomaticInvocation=False, + isCancelPending=False, + operationParams=req) + + inst.create(self.context) + lcmocc.create(self.context) + + return inst.id, lcmocc.id @mock.patch.object(nfvo_client.NfvoClient, 'get_vnf_package_info_vnfd') def test_create_pkg_disabled(self, mocked_get_vnf_package_info_vnfd): @@ -57,6 +90,56 @@ class TestVnflcmV2(db_base.SqlTestCase): self.assertRaises(sol_ex.VnfdIdNotEnabled, self.controller.create, request=self.request, body=body) + def test_delete_instantiated(self): + inst_id, _ = self._create_inst_and_lcmocc('INSTANTIATED', + fields.LcmOperationStateType.COMPLETED) + + self.assertRaises(sol_ex.VnfInstanceIsInstantiated, + self.controller.delete, request=self.request, id=inst_id) + + def test_delete_lcmocc_in_progress(self): + inst_id, _ = self._create_inst_and_lcmocc('NOT_INSTANTIATED', + fields.LcmOperationStateType.FAILED_TEMP) + + self.assertRaises(sol_ex.OtherOperationInProgress, + self.controller.delete, request=self.request, id=inst_id) + + def test_instantiate_instantiated(self): + inst_id, _ = self._create_inst_and_lcmocc('INSTANTIATED', + fields.LcmOperationStateType.COMPLETED) + body = {"flavourId": "small"} + + self.assertRaises(sol_ex.VnfInstanceIsInstantiated, + self.controller.instantiate, request=self.request, id=inst_id, + body=body) + + def test_instantiate_lcmocc_in_progress(self): + inst_id, _ = self._create_inst_and_lcmocc('NOT_INSTANTIATED', + fields.LcmOperationStateType.FAILED_TEMP) + body = {"flavourId": "small"} + + self.assertRaises(sol_ex.OtherOperationInProgress, + self.controller.instantiate, request=self.request, id=inst_id, + body=body) + + def test_terminate_not_instantiated(self): + inst_id, _ = self._create_inst_and_lcmocc('NOT_INSTANTIATED', + fields.LcmOperationStateType.COMPLETED) + body = {"terminationType": "FORCEFUL"} + + self.assertRaises(sol_ex.VnfInstanceIsNotInstantiated, + self.controller.terminate, request=self.request, id=inst_id, + body=body) + + def test_terminate_lcmocc_in_progress(self): + inst_id, _ = self._create_inst_and_lcmocc('INSTANTIATED', + fields.LcmOperationStateType.FAILED_TEMP) + body = {"terminationType": "FORCEFUL"} + + self.assertRaises(sol_ex.OtherOperationInProgress, + self.controller.terminate, request=self.request, id=inst_id, + body=body) + def test_invalid_subscripion(self): body = { "callbackUri": "http://127.0.0.1:6789/notification", @@ -92,3 +175,11 @@ class TestVnflcmV2(db_base.SqlTestCase): body=body) self.assertEqual("'TLS_CERT' is not supported at the moment.", ex.detail) + + def test_retry_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_retry, request=self.request, + id=lcmocc_id)