Support for grant request with Synchronous response

Supported grant for the following sequences:
   Instantiation
   Healing
   Scaling
   Termination

Implements: blueprint support-vnfm-operations
Spec: https://specs.openstack.org/openstack/tacker-specs/specs/victoria/support-sol003-vnfm-operations.html

Change-Id: I47a6a995dc25256eb1bd10f2e03a1f810fdb3f69
changes/95/747895/18
Aldinson Esto 2 years ago
parent 49be782dc2
commit ffe4c40921
  1. 2
      tacker/api/vnflcm/v1/controller.py
  2. 631
      tacker/conductor/conductor_server.py
  3. 16
      tacker/db/db_sqlalchemy/models.py
  4. 52
      tacker/db/migration/alembic_migrations/versions/2c5211036579_add_placement_table.py
  5. 2
      tacker/db/migration/alembic_migrations/versions/HEAD
  6. 53
      tacker/db/vnfm/vnfm_db.py
  7. 4
      tacker/objects/__init__.py
  8. 287
      tacker/objects/grant.py
  9. 407
      tacker/objects/grant_request.py
  10. 12
      tacker/plugins/common/constants.py
  11. 0
      tacker/tests/contrib/post_test_hook.sh
  12. 0
      tacker/tests/etc/samples/etsi/nfv/common/Files/images/cirros-0.4.0-x86_64-disk.img
  13. 0
      tacker/tests/etc/samples/etsi/nfv/vnflcm1/Definitions/helloworld3_df_simple.yaml
  14. 0
      tacker/tests/etc/samples/etsi/nfv/vnflcm1/Definitions/helloworld3_top.vnfd.yaml
  15. 0
      tacker/tests/etc/samples/etsi/nfv/vnflcm1/Definitions/helloworld3_types.yaml
  16. 0
      tacker/tests/etc/samples/etsi/nfv/vnflcm1/TOSCA-Metadata/TOSCA.meta
  17. 0
      tacker/tests/etc/samples/etsi/nfv/vnflcm2/Definitions/helloworld3_df_simple.yaml
  18. 0
      tacker/tests/etc/samples/etsi/nfv/vnflcm2/Definitions/helloworld3_top.vnfd.yaml
  19. 0
      tacker/tests/etc/samples/etsi/nfv/vnflcm2/Definitions/helloworld3_types.yaml
  20. 0
      tacker/tests/etc/samples/etsi/nfv/vnflcm2/TOSCA-Metadata/TOSCA.meta
  21. 0
      tacker/tests/etc/samples/etsi/nfv/vnflcm4/TOSCA-Metadata/TOSCA.meta
  22. 1445
      tacker/tests/unit/conductor/test_conductor_server.py
  23. 77
      tacker/tests/unit/db/utils.py
  24. 3
      tacker/tests/unit/objects/fakes.py
  25. 4
      tacker/tests/unit/objects/test_vnf_instance.py
  26. 14
      tacker/tests/unit/objects/test_vnf_lcm_op_occs.py
  27. 4
      tacker/tests/unit/vnflcm/fakes.py
  28. 88
      tacker/tests/unit/vnflcm/test_vnflcm_driver.py
  29. 98
      tacker/tests/unit/vnfm/infra_drivers/openstack/data/etsi_nfv/tosca_vnfd.yaml
  30. 27
      tacker/tests/unit/vnfm/infra_drivers/openstack/data/hot_grant.yaml
  31. 31
      tacker/tests/unit/vnfm/infra_drivers/openstack/data/hot_scale_grant.yaml
  32. 31
      tacker/tests/unit/vnfm/infra_drivers/openstack/data/hot_scale_initial.yaml
  33. 57
      tacker/tests/unit/vnfm/infra_drivers/openstack/data/hot_scale_nest_grant.yaml
  34. 27
      tacker/tests/unit/vnfm/infra_drivers/openstack/data/hot_scale_nest_initial.yaml
  35. 361
      tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py
  36. 166
      tacker/tests/unit/vnfm/test_plugin.py
  37. 8
      tacker/vnflcm/utils.py
  38. 55
      tacker/vnflcm/vnflcm_driver.py
  39. 24
      tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py
  40. 347
      tacker/vnfm/infra_drivers/openstack/openstack.py
  41. 27
      tacker/vnfm/infra_drivers/scale_driver.py

@ -455,7 +455,7 @@ class VnfLcmController(wsgi.Controller):
'vnfInstance': {
'href': self._get_vnf_instance_href(vnf_instance)}}}
# call sendNotification
# call send_notification
self.rpc_api.send_notification(context, notification)
result = self._view_builder.create(vnf_instance)

@ -25,7 +25,6 @@ import time
import traceback
import yaml
from glance_store import exceptions as store_exceptions
from oslo_config import cfg
from oslo_log import log as logging
@ -51,6 +50,7 @@ from tacker.common import utils
import tacker.conf
from tacker import context as t_context
from tacker.db.common_services import common_services_db
from tacker.db.db_sqlalchemy import models
from tacker.db.nfvo import nfvo_db
from tacker.db.vnfm import vnfm_db
from tacker.extensions import nfvo
@ -66,6 +66,7 @@ from tacker import service as tacker_service
from tacker import version
from tacker.vnflcm import utils as vnflcm_utils
from tacker.vnflcm import vnflcm_driver
from tacker.vnfm import nfvo_client
from tacker.vnfm import plugin
CONF = tacker.conf.CONF
@ -230,6 +231,65 @@ def revert_update_lcm(function):
return decorated_function
@utils.expects_func_args('vnf_instance', 'vnf_lcm_op_occ_id')
def grant_error_common(function):
"""Decorator to revert upload_vnf_package on failure."""
@functools.wraps(function)
def decorated_function(self, context, *args, **kwargs):
try:
return function(self, context, *args, **kwargs)
except Exception:
with excutils.save_and_reraise_exception():
wrapped_func = safe_utils.get_wrapped_function(function)
keyed_args = inspect.getcallargs(wrapped_func, self, context,
*args, **kwargs)
context = keyed_args['context']
vnf_instance = keyed_args['vnf_instance']
vnf_lcm_op_occ_id = keyed_args['vnf_lcm_op_occ_id']
try:
vnf_lcm_op_occs = objects.VnfLcmOpOcc.get_by_id(
context, vnf_lcm_op_occ_id)
timestamp = datetime.datetime.utcnow()
vnf_lcm_op_occs.operation_state = 'ROLLED_BACK'
vnf_lcm_op_occs.state_entered_time = timestamp
vnf_lcm_op_occs.save()
except Exception as e:
LOG.warning("Failed to update vnf_lcm_op_occ for vnf "
"instance %(id)s. Error: %(error)s",
{"id": vnf_instance.id, "error": e})
try:
notification = {}
notification['notificationType'] = \
'VnfLcmOperationOccurrenceNotification'
notification['vnfInstanceId'] = vnf_instance.id
notification['notificationStatus'] = 'RESULT'
notification['operation'] = vnf_lcm_op_occs.operation
notification['operationState'] = 'ROLLED_BACK'
notification['isAutomaticInvocation'] = \
vnf_lcm_op_occs.is_automatic_invocation
notification['vnfLcmOpOccId'] = vnf_lcm_op_occ_id
insta_url = CONF.vnf_lcm.endpoint_url + \
"/vnflcm/v1/vnf_instances/" + \
vnf_instance.id
vnflcm_url = CONF.vnf_lcm.endpoint_url + \
"/vnflcm/v1/vnf_lcm_op_occs/" + \
vnf_lcm_op_occ_id
notification['_links'] = {}
notification['_links']['vnfInstance'] = {}
notification['_links']['vnfInstance']['href'] = insta_url
notification['_links']['vnfLcmOpOcc'] = {}
notification['_links']['vnfLcmOpOcc']['href'] = vnflcm_url
self.send_notification(context, notification)
except Exception as e:
LOG.warning("Failed notification for vnf "
"instance %(id)s. Error: %(error)s",
{"id": vnf_instance.id, "error": e})
return decorated_function
class Conductor(manager.Manager):
def __init__(self, host, conf=None):
if conf:
@ -661,9 +721,10 @@ class Conductor(manager.Manager):
vim_connection_info = objects.VimConnectionInfo.\
obj_from_primitive(vim_info, context)
vnflcm_utils._build_instantiated_vnf_info(vnfd_dict,
instantiate_vnf_req, vnf_instance,
vim_id=vim_connection_info.vim_id)
if not vnf_instance.instantiated_vnf_info.instance_id:
vnflcm_utils._build_instantiated_vnf_info(
vnfd_dict, instantiate_vnf_req, vnf_instance,
vim_id=vim_connection_info.vim_id)
if vnf_instance.instantiated_vnf_info.instance_id:
self.vnf_manager.invoke(vim_connection_info.vim_type,
@ -761,6 +822,522 @@ class Conductor(manager.Manager):
{'zip': csar_path, 'folder': csar_zip_temp_path,
'uuid': vnf_pack.id})
def _grant(self, context, grant_request):
LOG.info(
"grant start grant_request[%s]" %
grant_request.to_request_body())
response = nfvo_client.GrantRequest().grants(
json=grant_request.to_request_body())
res_body = response.json()
res_dict = utils.convert_camelcase_to_snakecase(res_body)
LOG.info("grant end res_body[%s]" % res_dict)
grant_obj = objects.Grant.obj_from_primitive(
res_dict, context=context)
if len(grant_request.add_resources) != len(grant_obj.add_resources):
msg = "grant add resource error"
raise exceptions.ValidationError(detail=msg)
if len(
grant_request.remove_resources) != len(
grant_obj.remove_resources):
msg = "grant remove resource error"
raise exceptions.ValidationError(detail=msg)
self._check_res_add_remove_rsc(context, grant_request, grant_obj)
return grant_obj
def _check_res_add_remove_rsc(self, context, grant_request, grant_obj):
for add_resource in grant_request.add_resources:
match_flg = False
for rsc in grant_obj.add_resources:
if add_resource.id == rsc.resource_definition_id:
match_flg = True
break
if not match_flg:
msg = "grant add resource error"
raise exceptions.ValidationError(detail=msg)
for remove_resource in grant_request.remove_resources:
match_flg = False
for rsc in grant_obj.remove_resources:
if remove_resource.id == rsc.resource_definition_id:
match_flg = True
break
if not match_flg:
msg = "grant remove resource error"
raise exceptions.ValidationError(detail=msg)
@grant_error_common
def _instantiate_grant(self,
context,
vnf_instance,
vnf_dict,
instantiate_vnf_request,
vnf_lcm_op_occ_id):
vnfd_dict = vnflcm_utils._get_vnfd_dict(context,
vnf_dict['vnfd']['id'],
instantiate_vnf_request.flavour_id)
inst_level = instantiate_vnf_request.instantiation_level_id
vnf_instance.instantiated_vnf_info = objects.InstantiatedVnfInfo(
flavour_id=instantiate_vnf_request.flavour_id,
instantiation_level_id=inst_level,
vnf_instance_id=vnf_instance.id)
vnf_instance.instantiated_vnf_info.reinitialize()
vnflcm_utils._build_instantiated_vnf_info(vnfd_dict,
instantiate_vnf_request, vnf_instance, '')
if not self._get_grant_execute():
return
add_resources = []
vnf_inf = vnf_instance.instantiated_vnf_info
for vnfc_resource in vnf_inf.vnfc_resource_info:
resource = objects.ResourceDefinition()
resource.id = vnfc_resource.id
resource.type = constants.TYPE_COMPUTE
resource.vdu_id = vnfc_resource.vdu_id
resource.resource_template_id = vnfc_resource.vdu_id
add_resources.append(resource)
for vl_resource in vnf_inf.vnf_virtual_link_resource_info:
resource = objects.ResourceDefinition()
resource.id = vl_resource.id
resource.type = constants.TYPE_VL
resource.resource_template_id = \
vl_resource.vnf_virtual_link_desc_id
add_resources.append(resource)
for cp_resource in vl_resource.vnf_link_ports:
for vnfc_resource in vnf_inf.vnfc_resource_info:
for vnfc_cp_resource in vnfc_resource.vnfc_cp_info:
if cp_resource.cp_instance_id == vnfc_cp_resource.id:
resource = objects.ResourceDefinition()
resource.id = cp_resource.id
resource.type = constants.TYPE_LINKPORT
resource.vdu_id = vnfc_resource.vdu_id
resource.resource_template_id = \
vnfc_cp_resource.cpd_id
add_resources.append(resource)
for storage_resource in vnf_inf.virtual_storage_resource_info:
for vnfc_resource in vnf_inf.vnfc_resource_info:
if storage_resource.id in vnfc_resource.storage_resource_ids:
resource = objects.ResourceDefinition()
resource.id = storage_resource.id
resource.type = constants.TYPE_STORAGE
resource.vdu_id = vnfc_resource.vdu_id
resource.resource_template_id = \
storage_resource.virtual_storage_desc_id
add_resources.append(resource)
p_c_list = []
placement_obj_list = []
topo_temp = vnfd_dict.get('topology_template', {})
for policy in topo_temp.get('policies', []):
for policy_name, policy_dict in policy.items():
key_type = 'tosca.policies.nfv.AntiAffinityRule'
if policy_dict['type'] == key_type:
placement_constraint = objects.PlacementConstraint()
placement_constraint.affinity_or_anti_affinity = \
'ANTI_AFFINITY'
placement_constraint.scope = 'ZONE'
placement_constraint.resource = []
placement_constraint.fallback_best_effort = True
for target in policy_dict.get('targets', []):
if target in topo_temp.get('groups', []):
for member in topo_temp['groups']['members']:
for vnfc_rsc in vnf_inf.vnfc_resource_info:
if member == vnfc_rsc.vdu_id:
resource = \
objects.ConstraintResourceRef()
resource.id_type = 'GRANT'
resource.resource_id = vnfc_rsc.id
p_rsc = \
placement_constraint.resource
p_rsc.append(resource)
break
else:
for vnfc_rsc in vnf_inf.vnfc_resource_info:
if target == vnfc_rsc.vdu_id:
resource = \
objects.ConstraintResourceRef()
resource.id_type = 'GRANT'
resource.resource_id = vnfc_rsc.id
p_rsc = placement_constraint.resource
p_rsc.append(resource)
break
p_c_list.append(placement_constraint)
placement_obj = models.PlacementConstraint()
placement_obj.id = uuidutils.generate_uuid()
placement_obj.vnf_instance_id = vnf_instance.id
placement_obj.affinity_or_anti_affinity = \
placement_constraint.affinity_or_anti_affinity
placement_obj.scope = placement_constraint.scope
placement_obj.server_group_name = policy_name
p_c_dict = placement_constraint.to_dict()
res_dict = p_c_dict.get('resource', {})
res_json = json.dumps(res_dict)
placement_obj.resource = res_json
placement_obj.created_at = timeutils.utcnow()
placement_obj.deleted_at = datetime.datetime.min
placement_obj_list.append(placement_obj)
g_request = self._make_grant_request(context,
vnf_instance,
vnf_lcm_op_occ_id,
'INSTANTIATE',
False,
add_resources=add_resources,
placement_constraints=p_c_list)
vnf_dict['placement_obj_list'] = placement_obj_list
vnf_dict['grant'] = self._grant(context, g_request)
def _get_placement(self, context, vnf_instance):
return self.vnfm_plugin.get_placement_constraint(context,
vnf_instance.id)
@grant_error_common
def _scale_grant(
self,
context,
vnf_dict,
vnf_instance,
scale_vnf_request,
vnf_lcm_op_occ_id):
# Check if vnf is in instantiated state.
vnf_instance = objects.VnfInstance.get_by_id(context,
vnf_instance.id)
if vnf_instance.instantiation_state == \
fields.VnfInstanceState.NOT_INSTANTIATED:
LOG.error("Scale action cannot be performed on vnf %(id)s "
"which is in %(state)s state.",
{"id": vnf_instance.id,
"state": vnf_instance.instantiation_state})
raise Exception("Scale action cannot be performed on vnf")
vim_info = vnflcm_utils._get_vim(
context, vnf_instance.vim_connection_info)
vim_connection_info = objects.VimConnectionInfo.obj_from_primitive(
vim_info, context)
if scale_vnf_request.type == 'SCALE_IN':
vnf_dict['action'] = 'in'
reverse = scale_vnf_request.additional_params.get('is_reverse')
region_name = vim_connection_info.access_info.get('region_name')
scale_id_list, scale_name_list, grp_id, res_num = \
self.vnf_manager.invoke(vim_connection_info.vim_type,
'get_scale_in_ids',
plugin=self,
context=context,
vnf_dict=vnf_dict,
is_reverse=reverse,
auth_attr=vim_connection_info.access_info,
region_name=region_name,
number_of_steps=scale_vnf_request.number_of_steps)
vnf_dict['res_num'] = res_num
else:
scale_id_list = []
if not self._get_grant_execute():
return None, []
placement_obj_list = self.vnfm_plugin.get_placement_constraint(
context, vnf_instance.id)
self.vnf_manager.invoke(
vim_connection_info.vim_type,
'get_grant_resource',
plugin=self,
vnf_instance=vnf_instance,
vnf_info=vnf_dict,
scale_vnf_request=scale_vnf_request,
placement_obj_list=placement_obj_list,
vim_connection_info=vim_connection_info,
del_list=scale_id_list
)
vnf_dict['placement_obj_list'] = placement_obj_list
grant_request = self._make_grant_request(
context,
vnf_instance,
vnf_lcm_op_occ_id,
'SCALE',
False,
add_resources=vnf_dict['addResources'],
remove_resources=vnf_dict['removeResources'],
placement_constraints=vnf_dict['placement_constraint_list'])
vnf_dict['grant'] = self._grant(context, grant_request)
@grant_error_common
def _heal_grant(self,
context,
vnf_instance,
vnf_dict,
heal_vnf_request,
vnf_lcm_op_occ_id):
vnf_inf = vnf_instance.instantiated_vnf_info
if not self._get_grant_execute():
return
placement_obj_list = self._get_placement(context, vnf_instance)
vim_info = vnflcm_utils._get_vim(context,
vnf_instance.vim_connection_info)
vim_connection_info = \
objects.VimConnectionInfo.obj_from_primitive(vim_info, context)
cinder_list = self.vnf_manager.invoke(vim_connection_info.vim_type,
'get_cinder_list',
vnf_info=vnf_dict)
vnf_instantiated_info_after = copy.deepcopy(vnf_inf)
del_cre_vdu_list = []
add_resources = []
rm_resources = []
affinity_list = []
for vnfc_resource in vnf_instantiated_info_after.vnfc_resource_info:
vnfc_key = vnfc_resource.compute_resource.resource_id
if not heal_vnf_request.vnfc_instance_id or \
vnfc_key in heal_vnf_request.vnfc_instance_id:
if vnfc_resource.vdu_id not in del_cre_vdu_list:
del_cre_vdu_list.append(vnfc_resource.vdu_id)
for vnfc_resource in vnf_instantiated_info_after.vnfc_resource_info:
if vnfc_resource.vdu_id in del_cre_vdu_list:
resource = objects.ResourceDefinition()
resource.id = vnfc_resource.id
resource.type = constants.TYPE_COMPUTE
resource.vdu_id = vnfc_resource.vdu_id
resource.resource_template_id = vnfc_resource.vdu_id
vim_id = vnfc_resource.compute_resource.vim_connection_id
rsc_id = vnfc_resource.compute_resource.resource_id
vnfc_rh = objects.ResourceHandle(
vim_connection_id=vim_id,
resource_id=rsc_id)
resource.resource = vnfc_rh
rm_resources.append(resource)
add_uuid = uuidutils.generate_uuid()
resource = objects.ResourceDefinition()
resource.id = add_uuid
resource.type = constants.TYPE_COMPUTE
resource.vdu_id = vnfc_resource.vdu_id
resource.resource_template_id = vnfc_resource.vdu_id
add_resources.append(resource)
key_id = vnfc_resource.compute_resource.resource_id
for placement_obj in placement_obj_list:
resource_dict = jsonutils.loads(placement_obj.resource)
set_flg = False
for resource in resource_dict:
if resource.get('resource_id') == key_id:
resource['id_type'] = 'GRANT'
resource['resource_id'] = add_uuid
g_name = placement_obj.server_group_name
affinity_list.append(g_name)
set_flg = True
res_json = jsonutils.dump_as_bytes(resource_dict)
placement_obj.resource = res_json
break
if set_flg:
break
vnfc_resource.id = add_uuid
vnfc_resource.compute_resource = objects.ResourceHandle()
st_info = vnf_instantiated_info_after.virtual_storage_resource_info
for storage_resource in st_info:
if storage_resource.virtual_storage_desc_id in cinder_list:
for vnfc_resource in vnf_inf.vnfc_resource_info:
id_list = vnfc_resource.storage_resource_ids
if storage_resource.id in id_list:
resource = objects.ResourceDefinition()
resource.id = storage_resource.id
resource.type = constants.TYPE_STORAGE
resource.vdu_id = vnfc_resource.vdu_id
resource.resource_template_id = \
storage_resource.virtual_storage_desc_id
st_rh = objects.ResourceHandle()
st_rh.vim_connection_id = \
storage_resource.storage_resource.vim_connection_id
st_rh.resource_id = \
storage_resource.storage_resource.resource_id
resource.resource = st_rh
rm_resources.append(resource)
add_uuid = uuidutils.generate_uuid()
resource = objects.ResourceDefinition()
resource = objects.ResourceDefinition()
resource.id = add_uuid
resource.type = constants.TYPE_STORAGE
resource.vdu_id = vnfc_resource.vdu_id
resource.resource_template_id = \
storage_resource.virtual_storage_desc_id
add_resources.append(resource)
storage_resource.id = add_uuid
storage_resource.storage_resource = \
objects.ResourceHandle()
p_c_list = []
for placement_obj in placement_obj_list:
p_constraint = objects.PlacementConstraint()
p_constraint.affinity_or_anti_affinity = \
placement_obj.affinity_or_anti_affinity
p_constraint.scope = placement_obj.scope
resource_dict = jsonutils.loads(placement_obj.resource)
p_constraint.resource = []
for rsc in resource_dict:
rsc_obj = objects.ConstraintResourceRef()
rsc_obj.id_type = rsc.get('id_type')
rsc_obj.resource_id = rsc.get('resource_id')
p_constraint.resource.append(rsc_obj)
p_constraint.fallback_best_effort = True
p_c_list.append(p_constraint)
g_request = self._make_grant_request(context,
vnf_instance,
vnf_lcm_op_occ_id,
'HEAL',
False,
add_resources=add_resources,
remove_resources=rm_resources,
placement_constraints=p_c_list)
vnf_dict['placement_obj_list'] = placement_obj_list
vnf_dict['grant'] = self._grant(context, g_request)
vnf_dict['vnf_instantiated_info_after'] = vnf_instantiated_info_after
@grant_error_common
def _terminate_grant(self, context, vnf_instance, vnf_lcm_op_occ_id):
vnf_inf = vnf_instance.instantiated_vnf_info
if not self._get_grant_execute():
return
rm_resources = []
for vnfc_resource in vnf_inf.vnfc_resource_info:
resource = objects.ResourceDefinition()
resource.id = vnfc_resource.id
resource.type = constants.TYPE_COMPUTE
resource.vdu_id = vnfc_resource.vdu_id
resource.resource_template_id = vnfc_resource.vdu_id
vim_id = vnfc_resource.compute_resource.vim_connection_id
rsc_id = vnfc_resource.compute_resource.resource_id
vnfc_rh = objects.ResourceHandle(
vim_connection_id=vim_id,
resource_id=rsc_id)
resource.resource = vnfc_rh
rm_resources.append(resource)
for vl_resource in vnf_inf.vnf_virtual_link_resource_info:
resource = objects.ResourceDefinition()
resource.id = vl_resource.id
resource.type = constants.TYPE_VL
resource.resource_template_id = \
vl_resource.vnf_virtual_link_desc_id
vim_id = vl_resource.network_resource.vim_connection_id
rsc_id = vl_resource.network_resource.resource_id
vl_rh = objects.ResourceHandle(
vim_connection_id=vim_id,
resource_id=rsc_id)
resource.resource = vl_rh
rm_resources.append(resource)
for cp_resource in vl_resource.vnf_link_ports:
for vnfc_resource in vnf_inf.vnfc_resource_info:
for vnfc_cp_resource in vnfc_resource.vnfc_cp_info:
if cp_resource.cp_instance_id == vnfc_cp_resource.id:
resource = objects.ResourceDefinition()
resource.id = cp_resource.id
resource.type = constants.TYPE_LINKPORT
resource.vdu_id = vnfc_resource.vdu_id
resource.resource_template_id = \
vnfc_cp_resource.cpd_id
vim_id = \
cp_resource.resource_handle.vim_connection_id
rsc_id = cp_resource.resource_handle.resource_id
cp_rh = objects.ResourceHandle(
vim_connection_id=vim_id,
resource_id=rsc_id)
resource.resource = cp_rh
rm_resources.append(resource)
for storage_resource in vnf_inf.virtual_storage_resource_info:
for vnfc_resource in vnf_inf.vnfc_resource_info:
if storage_resource.id in vnfc_resource.storage_resource_ids:
resource = objects.ResourceDefinition()
resource.id = storage_resource.id
resource.type = constants.TYPE_STORAGE
resource.vdu_id = vnfc_resource.vdu_id
resource.resource_template_id = \
storage_resource.virtual_storage_desc_id
vim_id = \
storage_resource.storage_resource.vim_connection_id
rsc_id = storage_resource.storage_resource.resource_id
st_rh = objects.ResourceHandle(
vim_connection_id=vim_id,
resource_id=rsc_id)
resource.resource = st_rh
rm_resources.append(resource)
grant_request = self._make_grant_request(context,
vnf_instance,
vnf_lcm_op_occ_id,
'TERMINATE',
False,
remove_resources=rm_resources)
self._grant(context, grant_request)
def _get_grant_execute(self):
try:
nfvo_client.GrantRequest().validate()
except nfvo_client.UndefinedExternalSettingException:
return False
return True
def _make_grant_request(self, context, vnf_instance,
vnf_lcm_op_occ_id, operation,
is_automatic_invocation,
add_resources=[],
remove_resources=[],
placement_constraints=[]):
grant_request = objects.GrantRequest()
grant_request.vnf_instance_id = vnf_instance.id
grant_request.vnf_lcm_op_occ_id = vnf_lcm_op_occ_id
grant_request.vnfd_id = vnf_instance.vnfd_id
grant_request.flavour_id = \
vnf_instance.instantiated_vnf_info.flavour_id
grant_request.operation = operation
grant_request.is_automatic_invocation = is_automatic_invocation
vnflcm_url = CONF.vnf_lcm.endpoint_url + \
"/vnflcm/v1/vnf_lcm_op_occs/" + vnf_lcm_op_occ_id
insta_url = CONF.vnf_lcm.endpoint_url + \
"/vnflcm/v1/vnf_instances/" + vnf_instance.id
link_vnflcm = objects.Link(href=vnflcm_url)
link_insta = objects.Link(href=insta_url)
link = objects.Links(vnf_lcm_op_occ=link_vnflcm,
vnf_instance=link_insta)
grant_request._links = link
if add_resources:
grant_request.add_resources = add_resources
if remove_resources:
grant_request.remove_resources = remove_resources
if placement_constraints:
grant_request.placement_constraints = placement_constraints
return grant_request
def _create_placement(self, context, vnf_dict):
p_list = vnf_dict.get('placement_obj_list', [])
if len(p_list) == 0:
return
self.vnfm_plugin.create_placement_constraint(context,
p_list)
def _update_placement(self, context, vnf_dict, vnf_instance):
self.vnfm_plugin.update_placement_constraint(context,
vnf_dict,
vnf_instance)
def _delete_placement(self, context, vnf_instance_id):
self.vnfm_plugin.delete_placement_constraint(context,
vnf_instance_id)
@log.log
def _get_vnf_notify(self, context, id):
try:
@ -790,6 +1367,7 @@ class Conductor(manager.Manager):
is_automatic_invocation = \
kwargs.get('is_automatic_invocation', False)
error = kwargs.get('error', None)
# Used for timing control when a failure occurs
if old_vnf_instance:
vnf_instance_id = old_vnf_instance.id
@ -918,7 +1496,7 @@ class Conductor(manager.Manager):
self.__set_auth_subscription(line)
for num in range(CONF.vnf_lcm.retry_num):
LOG.info("send notify[%s]" % json.dumps(notification))
LOG.warn("send notify[%s]" % json.dumps(notification))
auth_client = auth.auth_manager.get_auth_client(
notification['subscriptionId'])
response = auth_client.post(
@ -965,6 +1543,12 @@ class Conductor(manager.Manager):
instantiate_vnf,
vnf_lcm_op_occs_id):
self._instantiate_grant(context,
vnf_instance,
vnf_dict,
instantiate_vnf,
vnf_lcm_op_occs_id)
try:
# Update vnf_lcm_op_occs table and send notification "PROCESSING"
self._send_lcm_op_occ_notification(
@ -993,6 +1577,8 @@ class Conductor(manager.Manager):
instantiation_state=fields.VnfInstanceState.
INSTANTIATED, task_state=None)
self._create_placement(context, vnf_dict)
# Update vnf_lcm_op_occs table and send notification "COMPLETED"
self._send_lcm_op_occ_notification(
context=context,
@ -1024,6 +1610,11 @@ class Conductor(manager.Manager):
@coordination.synchronized('{vnf_instance[id]}')
def terminate(self, context, vnf_lcm_op_occs_id,
vnf_instance, terminate_vnf_req, vnf_dict):
self._terminate_grant(context,
vnf_instance,
vnf_lcm_op_occs_id)
try:
old_vnf_instance = copy.deepcopy(vnf_instance)
@ -1042,6 +1633,9 @@ class Conductor(manager.Manager):
self.vnflcm_driver.terminate_vnf(context, vnf_instance,
terminate_vnf_req)
self._delete_placement(context, vnf_instance.id)
self._change_vnf_status(context, vnf_instance.id,
_PENDING_STATUS, 'INACTIVE')
@ -1086,6 +1680,12 @@ class Conductor(manager.Manager):
heal_vnf_request,
vnf_lcm_op_occs_id):
self._heal_grant(context,
vnf_instance,
vnf_dict,
heal_vnf_request,
vnf_lcm_op_occs_id)
try:
old_vnf_instance = copy.deepcopy(vnf_instance)
@ -1117,6 +1717,8 @@ class Conductor(manager.Manager):
self.vnflcm_driver._vnf_instance_update(context, vnf_instance,
task_state=None)
self._update_placement(context, vnf_dict, vnf_instance)
# update vnf_lcm_op_occs and send notification "COMPLETED"
self._send_lcm_op_occ_notification(
context=context,
@ -1150,16 +1752,15 @@ class Conductor(manager.Manager):
@coordination.synchronized('{vnf_instance[id]}')
def scale(self, context, vnf_info, vnf_instance, scale_vnf_request):
# Check if vnf is in instantiated state.
vnf_instance = objects.VnfInstance.get_by_id(context,
vnf_instance.id)
if vnf_instance.instantiation_state == \
fields.VnfInstanceState.NOT_INSTANTIATED:
LOG.error("Scale action cannot be performed on vnf %(id)s "
"which is in %(state)s state.",
{"id": vnf_instance.id,
"state": vnf_instance.instantiation_state})
return
vnf_lcm_op_occ = vnf_info['vnf_lcm_op_occ']
vnf_lcm_op_occ_id = vnf_lcm_op_occ.id
self._scale_grant(
context,
vnf_info,
vnf_instance,
scale_vnf_request,
vnf_lcm_op_occ_id)
self.vnflcm_driver.scale_vnf(
context, vnf_info, vnf_instance, scale_vnf_request)

@ -301,7 +301,7 @@ class VnfLcmOpOccs(model_base.BASE, models.SoftDeleteMixin,
"""VNF LCM OP OCCS Fields"""
__tablename__ = 'vnf_lcm_op_occs'
id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
id = sa.Column(sa.String(16), primary_key=True)
vnf_instance_id = sa.Column(sa.String(36),
sa.ForeignKey('vnf_instances.id'),
nullable=False)
@ -316,3 +316,17 @@ class VnfLcmOpOccs(model_base.BASE, models.SoftDeleteMixin,
resource_changes = sa.Column(sa.JSON(), nullable=True)
changed_info = sa.Column(sa.JSON(), nullable=True)
error_point = sa.Column(sa.Integer, nullable=False)
class PlacementConstraint(model_base.BASE, models.SoftDeleteMixin,
models.TimestampMixin, models_v1.HasId):
"""Represents a Vnf Placement Constraint."""
__tablename__ = 'placement_constraint'
vnf_instance_id = sa.Column(sa.String(36),
sa.ForeignKey('vnf_instances.id'),
nullable=False)
affinity_or_anti_affinity = sa.Column(sa.String(255), nullable=False)
scope = sa.Column(sa.String(255), nullable=False)
server_group_name = sa.Column(sa.String(255), nullable=False)
resource = sa.Column(sa.JSON(), nullable=True)

@ -0,0 +1,52 @@
# Copyright 2020 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 placement table
Revision ID: 2c5211036579
Revises: ee98bbc0789d
Create Date: 2020-09-11 20:47:46.345771
"""
# flake8: noqa: E402
# revision identifiers, used by Alembic.
revision = '2c5211036579'
down_revision = 'ee98bbc0789d'
from alembic import op
import sqlalchemy as sa
from sqlalchemy import Boolean
from tacker.db import types
def upgrade(active_plugins=None, options=None):
op.create_table(
'placement_constraint',
sa.Column('id', types.Uuid(length=36), nullable=False),
sa.Column('vnf_instance_id', types.Uuid(length=36), nullable=False),
sa.Column('affinity_or_anti_affinity',
sa.String(length=255), nullable=False),
sa.Column('scope', sa.String(length=255), nullable=False),
sa.Column('server_group_name', sa.String(length=255), nullable=False),
sa.Column('resource', sa.JSON(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', Boolean, default=False),
sa.PrimaryKeyConstraint('id'),
mysql_engine='InnoDB'
)

@ -18,6 +18,7 @@ from datetime import datetime
from oslo_db.exception import DBDuplicateEntry
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
@ -29,9 +30,11 @@ from sqlalchemy import schema
from tacker._i18n import _
from tacker.api.v1 import attributes
from tacker.common import exceptions
import tacker.conf
from tacker import context as t_context
from tacker.db.common_services import common_services_db_plugin
from tacker.db import db_base
from tacker.db.db_sqlalchemy import models
from tacker.db import model_base
from tacker.db import models_v1
from tacker.db.nfvo import ns_db
@ -40,6 +43,8 @@ from tacker.extensions import vnfm
from tacker import manager
from tacker.plugins.common import constants
CONF = tacker.conf.CONF
LOG = logging.getLogger(__name__)
_ACTIVE_UPDATE = (constants.ACTIVE, constants.PENDING_UPDATE,
constants.PENDING_HEAL)
@ -812,3 +817,51 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin):
constants.ERROR]
return self._mark_vnf_status(
vnf_id, exclude_status, constants.DEAD)
def create_placement_constraint(self, context, placement_obj_list):
context.session.add_all(placement_obj_list)
def get_placement_constraint(self, context, vnf_instance_id):
placement_constraint = (
self._model_query(context, models.PlacementConstraint).filter(
models.PlacementConstraint.vnf_instance_id == vnf_instance_id).
filter(models.PlacementConstraint.deleted == 0).all())
return placement_constraint
def update_placement_constraint_heal(self, context,
vnf_info,
vnf_instance):
if not vnf_info.get('grant'):
return
placement_obj_list = vnf_info['placement_obj_list']
inst_info = vnf_instance.instantiated_vnf_info
for vnfc in inst_info.vnfc_resource_info:
for placement_obj in placement_obj_list:
rsc_dict = jsonutils.loads(placement_obj.resource)
for rsc in rsc_dict:
if vnfc.id == rsc.get('resource_id') and\
rsc.get('id_type') == 'GRANT':
rsc['id_type'] = 'RES_MGMT'
rsc['resource_id'] = vnfc.\
compute_resource.resource_id
rsc['vim_connection_id'] = vnfc.\
compute_resource.vim_connection_id
placement_obj.resource = jsonutils.dumps(rsc_dict)
self.update_placement_constraint(context, placement_obj)
def delete_placement_constraint(self, context, vnf_instance_id):
(self._model_query(context, models.PlacementConstraint).
filter(
models.PlacementConstraint.vnf_instance_id == vnf_instance_id).
filter(models.PlacementConstraint.deleted == 0).
update({'deleted': 0, 'deleted_at': timeutils.utcnow()}))
def update_placement_constraint(self, context, placement_obj):
(self._model_query(
context,
models.PlacementConstraint).filter(
models.PlacementConstraint.id == placement_obj.id).
filter(models.PlacementConstraint.deleted == 0).
update({
'resource': placement_obj.resource,
'updated_at': timeutils.utcnow()}))

@ -42,3 +42,7 @@ def register_all():
__import__('tacker.objects.vnf_artifact')
__import__('tacker.objects.vnf_lcm_subscriptions')
__import__('tacker.objects.scale_vnf_request')
__import__('tacker.objects.grant')
__import__('tacker.objects.grant_request')
__import__('tacker.objects.vnfd')
__import__('tacker.objects.vnfd_attribute')

@ -0,0 +1,287 @@
# 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 tacker import objects
from tacker.objects import base
from tacker.objects import fields
@base.TackerObjectRegistry.register
class Grant(base.TackerObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'id': fields.StringField(nullable=False),
'vnf_instance_id': fields.StringField(nullable=False),
'vnf_lcm_op_occ_id': fields.StringField(nullable=False),
'vim_connections': fields.ListOfObjectsField(
'VimConnectionInfo', nullable=True, default=[]),
'zones': fields.ListOfObjectsField(
'ZoneInfo', nullable=True, default=[]),
'add_resources': fields.ListOfObjectsField(
'GrantInfo', nullable=True, default=[]),
'remove_resources': fields.ListOfObjectsField(
'GrantInfo', nullable=True, default=[]),
'vim_assets': fields.ObjectField(
'VimAssets', nullable=True)
}
@classmethod
def obj_from_primitive(cls, primitive, context):
if 'tacker_object.name' in primitive:
obj_grant = super(
Grant, cls).obj_from_primitive(primitive, context)
else:
if 'vim_connections' in primitive.keys():
obj_data = [objects.VimConnectionInfo._from_dict(
vim_conn) for vim_conn in primitive.get(
'vim_connections', [])]
primitive.update({'vim_connections': obj_data})
if 'zones' in primitive.keys():
obj_data = [ZoneInfo._from_dict(
zone) for zone in primitive.get(
'zones', [])]
primitive.update({'zones': obj_data})
if 'add_resources' in primitive.keys():
obj_data = [GrantInfo._from_dict(
add_rsc) for add_rsc in primitive.get(
'add_resources', [])]
primitive.update({'add_resources': obj_data})
if 'remove_resources' in primitive.keys():
obj_data = [GrantInfo._from_dict(
remove_rsc) for remove_rsc in primitive.get(
'remove_resources', [])]
primitive.update({'remove_resources': obj_data})
if 'vim_assets' in primitive.keys():
obj_data = VimAssets.obj_from_primitive(
primitive.get('vim_assets'), context)
primitive.update({'vim_assets': obj_data})
obj_grant = Grant._from_dict(primitive)
return obj_grant
@classmethod
def _from_dict(cls, data_dict):
id = data_dict.get('id')
vnf_instance_id = data_dict.get('vnf_instance_id')
vnf_lcm_op_occ_id = data_dict.get('vnf_lcm_op_occ_id')
vim_connections = data_dict.get('vim_connections', [])
zones = data_dict.get('zones', [])
add_resources = data_dict.get('add_resources', [])
remove_resources = data_dict.get('remove_resources', [])
vim_assets = data_dict.get('vim_assets')
obj = cls(
id=id,
vnf_instance_id=vnf_instance_id,
vnf_lcm_op_occ_id=vnf_lcm_op_occ_id,
vim_connections=vim_connections,
zones=zones,
add_resources=add_resources,
remove_resources=remove_resources,
vim_assets=vim_assets)
return obj
@base.TackerObjectRegistry.register
class ZoneInfo(base.TackerObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'id': fields.StringField(nullable=False),
'zone_id': fields.StringField(nullable=False),
'vim_connection_id': fields.StringField(nullable=True)
}
@classmethod
def obj_from_primitive(cls, primitive, context):
if 'tacker_object.name' in primitive:
obj_zone_info = super(
ZoneInfo, cls).obj_from_primitive(primitive, context)
else:
obj_zone_info = ZoneInfo._from_dict(primitive)
return obj_zone_info
@classmethod
def _from_dict(cls, data_dict):
id = data_dict.get('id')
zone_id = data_dict.get('zone_id')
vim_connection_id = data_dict.get('vim_connection_id')
obj = cls(
id=id,
zone_id=zone_id,
vim_connection_id=vim_connection_id)
return obj
@base.TackerObjectRegistry.register
class GrantInfo(base.TackerObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'resource_definition_id': fields.StringField(nullable=False),
'vim_connection_id': fields.StringField(nullable=True),
'zone_id': fields.StringField(nullable=True)
}
@classmethod
def obj_from_primitive(cls, primitive, context):
if 'tacker_object.name' in primitive:
obj_grant_info = super(
GrantInfo, cls).obj_from_primitive(primitive, context)
else:
obj_grant_info = GrantInfo._from_dict(primitive)
return obj_grant_info
@classmethod
def _from_dict(cls, data_dict):
resource_definition_id = data_dict.get('resource_definition_id')
vim_connection_id = data_dict.get('vim_connection_id')
zone_id = data_dict.get('zone_id')
obj = cls(
resource_definition_id=resource_definition_id,
vim_connection_id=vim_connection_id,
zone_id=zone_id)
return obj
@base.TackerObjectRegistry.register
class VimAssets(base.TackerObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'compute_resource_flavours': fields.ListOfObjectsField(
'VimComputeResourceFlavour', nullable=True, default=[]),
'software_images': fields.ListOfObjectsField(
'VimSoftwareImage', nullable=True, default=[])
}
@classmethod
def obj_from_primitive(cls, primitive, context):
if 'tacker_object.name' in primitive:
obj_vim_assets = super(
VimAssets, cls).obj_from_primitive(primitive, context)
else:
if 'compute_resource_flavours' in primitive.keys():
obj_data = [VimComputeResourceFlavour._from_dict(
flavour) for flavour in primitive.get(
'compute_resource_flavours', [])]
primitive.update({'compute_resource_flavours': obj_data})
if 'software_images' in primitive.keys():
obj_data = [VimSoftwareImage._from_dict(
img) for img in primitive.get(
'software_images', [])]
primitive.update({'software_images': obj_data})
obj_vim_assets = VimAssets._from_dict(primitive)
return obj_vim_assets
@classmethod
def _from_dict(cls, data_dict):
compute_resource_flavours = data_dict.get(
'compute_resource_flavours', [])
software_images = data_dict.get('software_images', [])
obj = cls(
compute_resource_flavours=compute_resource_flavours,
software_images=software_images)
return obj
@base.TackerObjectRegistry.register
class VimComputeResourceFlavour(base.TackerObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'vim_connection_id': fields.StringField(nullable=True),
'vnfd_virtual_compute_desc_id': fields.StringField(nullable=False),
'vim_flavour_id': fields.StringField(nullable=False)
}
@classmethod
def obj_from_primitive(cls, primitive, context):
if 'tacker_object.name' in primitive:
obj_flavour = super(
VimComputeResourceFlavour,
cls).obj_from_primitive(
primitive,
context)
else:
obj_flavour = VimComputeResourceFlavour._from_dict(primitive)
return obj_flavour
@classmethod
def _from_dict(cls, data_dict):
vim_connection_id = data_dict.get('vim_connection_id')
vnfd_virtual_compute_desc_id = data_dict.get(
'vnfd_virtual_compute_desc_id')
vim_flavour_id = data_dict.get('vim_flavour_id')
obj = cls(
vim_connection_id=vim_connection_id,
vnfd_virtual_compute_desc_id=vnfd_virtual_compute_desc_id,
vim_flavour_id=vim_flavour_id)
return obj
@base.TackerObjectRegistry.register
class VimSoftwareImage(base.TackerObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'vim_connection_id': fields.StringField(nullable=True),
'vnfd_software_image_id': fields.StringField(nullable=False),
'vim_software_image_id': fields.StringField(nullable=False)
}
@classmethod
def obj_from_primitive(cls, primitive, context):
if 'tacker_object.name' in primitive:
obj_img = super(
VimSoftwareImage, cls).obj_from_primitive(primitive, context)
else:
obj_img = VimSoftwareImage._from_dict(primitive)
return obj_img
@classmethod
def _from_dict(cls, data_dict):
vim_connection_id = data_dict.get('vim_connection_id')
vnfd_software_image_id = data_dict.get('vnfd_software_image_id')
vim_software_image_id = data_dict.get('vim_software_image_id')
obj = cls(
vim_connection_id=vim_connection_id,
vnfd_software_image_id=vnfd_software_image_id,
vim_software_image_id=vim_software_image_id)
return obj

@ -0,0 +1,407 @@
# 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_serialization import jsonutils
from tacker.common import utils
from tacker.objects import base
from tacker.objects import fields
@base.TackerObjectRegistry.register
class GrantRequest(base.TackerObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'vnf_instance_id': fields.StringField(nullable=False),
'vnf_lcm_op_occ_id': fields.StringField(nullable=False),
'vnfd_id': fields.StringField(nullable=False),
'flavour_id': fields.StringField(nullable=True),
'operation': fields.StringField(nullable=False),
'is_automatic_invocation': fields.BooleanField(nullable=False,
default=False),
'add_resources': fields.ListOfObjectsField(
'ResourceDefinition', nullable=True, default=[]),
'remove_resources': fields.ListOfObjectsField(
'ResourceDefinition', nullable=True, default=[]),
'placement_constraints': fields.ListOfObjectsField(
'PlacementConstraint', nullable=True, default=[]),
'_links': fields.ObjectField(
'Links', nullable=False)
}
@classmethod
def obj_from_primitive(cls, primitive, context):
if 'tacker_object.name' in primitive:
obj_grant_req = super(
GrantRequest, cls).obj_from_primitive(primitive, context)
else:
if 'add_resources' in primitive.keys():
obj_data = [ResourceDefinition._from_dict(
add_rsc) for add_rsc in primitive.get(
'add_resources', [])]
primitive.update({'add_resources': obj_data})
if 'remove_resources' in primitive.keys():
obj_data = [ResourceDefinition._from_dict(
remove_rsc) for remove_rsc in primitive.get(
'remove_resources', [])]
primitive.update({'add_resources': obj_data})
if 'placement_constraints' in primitive.keys():
obj_data = [PlacementConstraint._from_dict(
place) for place in primitive.get(
'placement_constraints', [])]
primitive.update({'add_resources': obj_data})
obj_grant_req = GrantRequest._from_dict(primitive)
return obj_grant_req
@classmethod
def _from_dict(cls, data_dict):
vnf_instance_id = data_dict.get('vnf_instance_id')
vnf_lcm_op_occ_id = data_dict.get('vnf_lcm_op_occ_id')
vnfd_id = data_dict.get('vnfd_id')
flavour_id = data_dict.get('flavour_id')
operation = data_dict.get('operation')
is_automatic_invocation = data_dict.get('is_automatic_invocation')
add_resources = data_dict.get('add_resources', [])
remove_resources = data_dict.get('remove_resources', [])
placement_constraints = data_dict.get('placement_constraints', [])
links = data_dict.get('_links')
obj = cls(
vnf_instance_id=vnf_instance_id,
vnf_lcm_op_occ_id=vnf_lcm_op_occ_id,