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
changes/15/816115/8
Itsuro Oda 11 months ago
parent e501198b0f
commit bf51c2bc80
  1. 5
      releasenotes/notes/add-v2-retry-api-34667d944db1f54c.yaml
  2. 78
      tacker/db/migration/alembic_migrations/versions/3ff50553e9d3_add_grant_and_grant_request.py
  3. 2
      tacker/db/migration/alembic_migrations/versions/HEAD
  4. 9
      tacker/sol_refactored/api/policies/vnflcm_v2.py
  5. 1
      tacker/sol_refactored/api/router.py
  6. 9
      tacker/sol_refactored/common/exceptions.py
  7. 32
      tacker/sol_refactored/common/lcm_op_occ_utils.py
  8. 10
      tacker/sol_refactored/conductor/conductor_rpc_v2.py
  9. 82
      tacker/sol_refactored/conductor/conductor_v2.py
  10. 133
      tacker/sol_refactored/conductor/vnflcm_driver_v2.py
  11. 23
      tacker/sol_refactored/controller/vnflcm_v2.py
  12. 24
      tacker/sol_refactored/db/api.py
  13. 54
      tacker/sol_refactored/db/sqlalchemy/models.py
  14. 22
      tacker/sol_refactored/infra_drivers/openstack/heat_utils.py
  15. 15
      tacker/sol_refactored/infra_drivers/openstack/openstack.py
  16. 6
      tacker/sol_refactored/objects/__init__.py
  17. 15
      tacker/sol_refactored/objects/base.py
  18. 3
      tacker/sol_refactored/objects/v1/grant_request.py
  19. 80
      tacker/sol_refactored/test-tools/cli.py
  20. 2
      tacker/tests/functional/sol_v2/base_v2.py
  21. 266
      tacker/tests/unit/sol_refactored/conductor/test_conductor_v2.py
  22. 97
      tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py

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

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

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

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

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

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

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

@ -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,28 +102,81 @@ 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)
self.vnflcm_driver.process(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)
lcmocc.operationState = fields.LcmOperationStateType.COMPLETED
# update inst and lcmocc at the same time
with context.session.begin(subtransactions=True):
inst.update(context)
lcmocc.update(context)
except Exception as ex:
LOG.exception("PROCESSING %s failed", lcmocc.operation)
lcmocc.operationState = fields.LcmOperationStateType.FAILED_TEMP
self._set_lcmocc_error(lcmocc, ex)
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)
# perform postamble LCM script
end_method = self._get_lcm_op_method(lcmocc.operation, 'end')
end_method(context, lcmocc, inst, grant_req, grant, vnfd)
@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,

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

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

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

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

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

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

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

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

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

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

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

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

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