Browse Source

Support for Change External VNF Connectivity

This feature will enable the client to use this API to perform the
following operations:

    - allow client to change the external connectivity of a VNF
      instance
    - VNFM support to change port/network
    - VNFM also supports to change ip address/mac address/
      allowed_address_pair

The operations provided through additional attributes are below:
    - Grant(POST)
    - VNF instances (GET)
    - Individual VNF Instances (GET)
    - Notification Endpoint (POST)

Modified .zuul.yaml to disable image_volume_cache, since cache
Volume remains during Terminate implementation and responds to
Heat Stack deletion failure without erasing volume type.

Implements: blueprint support-change-external-connectivity
Spec: https://specs.openstack.org/openstack/tacker-specs/specs/wallaby/support-change-external-VNF-connectivity-operation.html
Change-Id: Ie6b8b2b46b6de24e2d2d0a8bccef87bbdfa39191
changes/13/780213/16
Aldinson Esto 1 year ago
parent
commit
b77c469939
  1. 3
      .zuul.yaml
  2. 69
      api-ref/source/v1/samples/vnflcm/change-ext-conn-request.json
  3. 75
      api-ref/source/v1/vnflcm.inc
  4. 11
      tacker/api/schemas/vnf_lcm.py
  5. 4
      tacker/api/views/vnf_lcm.py
  6. 44
      tacker/api/vnflcm/v1/controller.py
  7. 5
      tacker/api/vnflcm/v1/router.py
  8. 10
      tacker/common/exceptions.py
  9. 260
      tacker/conductor/conductor_server.py
  10. 22
      tacker/conductor/conductorrpc/vnf_lcm_rpc.py
  11. 4
      tacker/extensions/vnfm.py
  12. 1
      tacker/objects/__init__.py
  13. 68
      tacker/objects/change_ext_conn_req.py
  14. 3
      tacker/objects/fields.py
  15. 23
      tacker/objects/grant.py
  16. 19
      tacker/objects/grant_request.py
  17. 3
      tacker/objects/vnf_lcm_op_occs.py
  18. 1
      tacker/plugins/common/constants.py
  19. 12
      tacker/policies/vnf_lcm.py
  20. 4
      tacker/releasenotes/notes/change_external_vnf_connectivity-444c580a01479f33.yaml
  21. 14
      tacker/tests/etc/samples/etsi/nfv/test_inst_terminate_vnf_with_vnflcmnoop/Scripts/vnflcm_noop.py
  22. 2
      tacker/tests/etc/samples/etsi/nfv/test_inst_terminate_vnf_with_vnflcmnoop/TOSCA-Metadata/TOSCA.meta
  23. 38
      tacker/tests/functional/sol/vnflcm/base.py
  24. 56
      tacker/tests/functional/sol/vnflcm/fake_vnflcm.py
  25. 185
      tacker/tests/functional/sol/vnflcm/test_vnf_instance_with_user_data.py
  26. 25
      tacker/tests/functional/sol_separated_nfvo/vnflcm/fake_grant.py
  27. 211
      tacker/tests/functional/sol_separated_nfvo/vnflcm/test_vnf_instance_with_user_data_nfvo_separate.py
  28. 248
      tacker/tests/unit/conductor/fakes.py
  29. 432
      tacker/tests/unit/conductor/test_conductor_server.py
  30. 178
      tacker/tests/unit/objects/test_change_ext_conn_req.py
  31. 251
      tacker/tests/unit/objects/test_grant.py
  32. 66
      tacker/tests/unit/vnflcm/fakes.py
  33. 149
      tacker/tests/unit/vnflcm/test_controller.py
  34. 353
      tacker/tests/unit/vnflcm/test_vnflcm_driver.py
  35. 230
      tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py
  36. 123
      tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py
  37. 161
      tacker/vnflcm/utils.py
  38. 98
      tacker/vnflcm/vnflcm_driver.py
  39. 32
      tacker/vnfm/infra_drivers/abstract_driver.py
  40. 12
      tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py
  41. 12
      tacker/vnfm/infra_drivers/noop.py
  42. 113
      tacker/vnfm/infra_drivers/openstack/openstack.py
  43. 14
      tacker/vnfm/mgmt_drivers/vnflcm_abstract_driver.py
  44. 14
      tacker/vnfm/mgmt_drivers/vnflcm_noop.py

3
.zuul.yaml

@ -121,6 +121,9 @@
$NEUTRON_DHCP_CONF:
DEFAULT:
enable_isolated_metadata: True
$CINDER_CONF:
lvmdriver-1:
image_volume_cache_enabled: False
devstack_plugins:
heat: https://opendev.org/openstack/heat
networking-sfc: https://opendev.org/openstack/networking-sfc

69
api-ref/source/v1/samples/vnflcm/change-ext-conn-request.json

@ -0,0 +1,69 @@
{
"extVirtualLinks": [
{
"id": "ext-vl-uuid-VL1",
"resourceId": "neutron-network-uuid_VL1",
"extCps": [
{
"cpdId": "CP1",
"cpConfig": [
{
"cpProtocolData": [
{
"layerProtocol": "IP_OVER_ETHERNET",
"ipOverEthernet": {
"ipAddresses": [
{
"type": "IPV4",
"numDynamicAddresses": 1,
"subnetId": "subnet-uuid"
}
]
}
}
]
}
]
},
{
"cpdId": "CP2",
"cpConfig": [
{
"cpProtocolData": [
{
"layerProtocol": "IP_OVER_ETHERNET",
"ipOverEthernet": {
"ipAddresses": [
{
"type": "IPV4",
"fixedAddresses": [
"10.0.0.1"
],
"subnetId": "subnet-uuid"
}
]
}
}
]
}
]
}
]
}
],
"vimConnectionInfo": [
{
"id": "vim-uuid",
"vimType": "ETSINFV.OPENSTACK_KEYSTONE.v_2",
"vimConnectionId": "dummy-vimid",
"interfaceInfo": {
"key1": "value1",
"key2": "value2"
},
"accessInfo": {
"key1": "value1",
"key2": "value2"
}
}
]
}

75
api-ref/source/v1/vnflcm.inc

@ -674,6 +674,81 @@ Request Example
.. literalinclude:: samples/vnflcm/modify-vnf-instance-request.json
:language: javascript
Change External VNF Connectivity
================================
.. rest_method:: POST /vnflcm/v1/vnf_instances/{vnfInstanceId}/change_ext_conn
The POST method changes the external connectivity of a VNF instance.
This task resource represents the "Change external VNF connectivity" operation.
The client can use this resource to change the external connectivity of a VNF instance.
Response Codes
--------------
.. rest_status_code:: success status.yaml
- 202
.. rest_status_code:: error status.yaml
- 400
- 401
- 403
- 404
- 409
Request Parameters
------------------
.. rest_parameters:: parameters_vnflcm.yaml
- vnfInstanceId: vnf_instance_id
- extVirtualLinks: ext_virtual_links
- id: ext_virtual_links_id
- vimConnectionId: vim_connection_id
- resourceId: ext_virtual_links_resource_id
- extCps: ext_cps
- cpdId: cpd_id
- cpConfig: cp_config
- cpInstanceId: cp_instance_id
- linkPortId: link_port_id
- cpProtocolData: cp_protocol_data
- layerProtocol: layer_protocol
- ipOverEthernet: ip_over_ethernet
- macAddress: mac_address
- ipAddresses: ip_addresses
- type: ip_address_type
- fixedAddresses: fixed_addresses
- numDynamicAddresses: num_dynamic_addresses
- subnetId: subnet_id
- extLinkPorts: ext_link_ports
- id: ext_link_port_id
- resourceHandle: ext_link_port_resource_handle
- vimConnectionId: vim_connection_id
- resourceId: resource_handle_resource_id
- vimLevelResourceType: resource_handle_vim_level_resource_type
- vimConnectionInfo: vnf_instance_vim_connection_info
- id: vim_connection_info_id
- vimId: vim_connection_info_vim_id
- vimType: vim_connection_info_vim_type
- interfaceInfo: vim_connection_info_interface_info
- endpoint: vim_connection_info_interface_info_endpoint
- accessInfo: vim_connection_info_access_info
- username: vim_connection_info_access_info_username
- password: vim_connection_info_access_info_password
- region: vim_connection_info_access_info_region
- tenant: vim_connection_info_access_info_tenant
- additionalParams: vnf_instance_additional_params
Request Example
---------------
.. literalinclude:: samples/vnflcm/change-ext-conn-request.json
:language: javascript
Show VNF LCM operation occurrence
=================================

11
tacker/api/schemas/vnf_lcm.py

@ -274,3 +274,14 @@ scale = {
'required': ['type', 'aspectId'],
'additionalProperties': True,
}
change_ext_conn = {
'type': 'object',
'properties': {
'extVirtualLinks': _extVirtualLinkData,
'vimConnectionInfo': _vimConnectionInfo,
'additionalParams': parameter_types.keyvalue_pairs,
},
'required': ['extVirtualLinks'],
'additionalProperties': True,
}

4
tacker/api/views/vnf_lcm.py

@ -60,6 +60,10 @@ class ViewBuilder(base.BaseViewBuilder):
"heal": {
"href": '/vnflcm/v1/vnf_instances/%s/heal'
% vnf_instance.id
},
"changeExtConn": {
"href": '/vnflcm/v1/vnf_instances/%s/change_ext_conn'
% vnf_instance.id
}
}

44
tacker/api/vnflcm/v1/controller.py

@ -53,6 +53,7 @@ from tacker.extensions import vnfm
from tacker import manager
from tacker import objects
from tacker.objects import fields
from tacker.objects.fields import ErrorPoint as EP
from tacker.objects import vnf_lcm_op_occs as vnf_lcm_op_occs_obj
from tacker.objects import vnf_lcm_subscriptions as subscription_obj
from tacker.plugins.common import constants
@ -1577,6 +1578,49 @@ class VnfLcmController(wsgi.Controller):
vnf_lcm_subscription, cast=False)
return resp
@wsgi.response(http_client.ACCEPTED)
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN,
http_client.NOT_FOUND, http_client.CONFLICT))
@validation.schema(vnf_lcm.change_ext_conn)
def change_ext_conn(self, request, id, body):
context = request.environ['tacker.context']
context.can(vnf_lcm_policies.VNFLCM % 'change_ext_conn')
vnf = self._get_vnf(context, id)
vnf_instance = self._get_vnf_instance(context, id)
if (vnf_instance.instantiation_state !=
fields.VnfInstanceState.INSTANTIATED):
return self._make_problem_detail(
'VNF is not instantiated',
409,
title='VNF IS NOT INSTANTIATED')
vnf['before_error_point'] = EP.INITIAL
self._change_ext_conn(context, vnf_instance, vnf, body)
def _change_ext_conn(self, context, vnf_instance, vnf, request_body):
req_body = utils.convert_camelcase_to_snakecase(request_body)
change_ext_conn_req = objects.ChangeExtConnRequest.obj_from_primitive(
req_body, context)
# call notification process
if vnf['before_error_point'] == EP.INITIAL:
vnf_lcm_op_occs_id = self._notification_process(
context,
vnf_instance,
fields.LcmOccsOperationType.CHANGE_EXT_CONN,
change_ext_conn_req,
request_body)
else:
vnf_lcm_op_occs_id = vnf['vnf_lcm_op_occs_id']
# Call Conductor server.
self.rpc_api.change_ext_conn(
context,
vnf_instance,
vnf,
change_ext_conn_req,
vnf_lcm_op_occs_id)
def create_resource():
return wsgi.Resource(VnfLcmController())

5
tacker/api/vnflcm/v1/router.py

@ -150,3 +150,8 @@ class VnflcmAPIRouter(wsgi.Router):
methods = {"GET": "list_lcm_op_occs"}
self._setup_route(mapper, "/vnf_lcm_op_occs",
methods, controller, default_resource)
# {apiRoot}/vnf_instances/{vnfInstanceId}/change_ext_conn resource
methods = {"POST": "change_ext_conn"}
self._setup_route(mapper, "/vnf_instances/{id}/change_ext_conn",
methods, controller, default_resource)

10
tacker/common/exceptions.py

@ -305,6 +305,16 @@ class VnfHealFailed(TackerException):
message = _("Heal Vnf failed for vnf %(id)s, error: %(error)s")
class VnfChangeExtConnFailed(TackerException):
message = _("Change external connectivity failed "
"for vnf %(id)s, error: %(error)s")
class VnfChangeExtConnWaitFailed(TackerException):
message = _("Change external connectivity wait failed "
"for vnf %(id)s, error: %(error)s")
class LockCreationFailed(TackerException):
message = _('Unable to create lock. Coordination backend not started.')

260
tacker/conductor/conductor_server.py

@ -59,6 +59,7 @@ from tacker.glance_store import store as glance_store
from tacker import manager
from tacker import objects
from tacker.objects import fields
from tacker.objects.fields import ErrorPoint as EP
from tacker.objects.vnf_package import VnfPackagesList
from tacker.objects import vnfd as vnfd_db
from tacker.objects import vnfd_attribute as vnfd_attribute_db
@ -109,7 +110,8 @@ _ACTIVE_STATUS = ('ACTIVE',)
_PENDING_STATUS = ('PENDING_CREATE',
'PENDING_TERMINATE',
'PENDING_DELETE',
'PENDING_HEAL')
'PENDING_HEAL',
'PENDING_CHANGE_EXT_CONN')
_ERROR_STATUS = ('ERROR',)
_ALL_STATUSES = _ACTIVE_STATUS + _INACTIVE_STATUS + _PENDING_STATUS + \
_ERROR_STATUS
@ -781,6 +783,30 @@ class Conductor(manager.Manager):
format(error_msg))
raise exceptions.TackerException(message=error_msg)
@log.log
def _update_instantiated_vnf_info_change_ext_conn(
self, context, vnf_instance, change_ext_conn_req):
try:
vim_info = vnflcm_utils._get_vim(context,
vnf_instance.vim_connection_info)
vim_connection_info = \
objects.VimConnectionInfo.obj_from_primitive(
vim_info, context)
self.vnf_manager.invoke(
vim_connection_info.vim_type, 'post_change_ext_conn_vnf',
context=context, vnf_instance=vnf_instance,
vim_connection_info=vim_connection_info)
vnflcm_utils._update_instantiated_vnf_info(
change_ext_conn_req, vnf_instance)
vnf_instance.instantiated_vnf_info.save()
except Exception as exp:
error_msg = \
"Failed to update instantiation information for vnf {}: {}".\
format(vnf_instance.id, encodeutils.exception_to_unicode(exp))
raise exceptions.TackerException(message=error_msg)
@log.log
def _add_additional_vnf_info(self, context, vnf_instance):
'''this method adds misc info to 'vnf' table'''
@ -834,6 +860,104 @@ class Conductor(manager.Manager):
{'zip': csar_path, 'folder': csar_zip_temp_path,
'uuid': vnf_pack.id})
def _get_vnf_link_ports_by_vl(self, vnf_info, ext_vl_id,
resource_id):
results = []
vnf_vl_resource_info = vnf_info.vnf_virtual_link_resource_info
for vnf_vl_res in vnf_vl_resource_info:
if ((vnf_vl_res.vnf_virtual_link_desc_id == ext_vl_id) and
(vnf_vl_res.network_resource.resource_id != resource_id)):
results.extend(vnf_vl_res.vnf_link_ports)
return results
def _get_vnf_link_ports_by_cp(self, vnf_info, cpd_id=None):
vnf_vl_resource_info = vnf_info.vnf_virtual_link_resource_info
vnfc_resource_info = vnf_info.vnfc_resource_info
def _get_vnf_link_port(vnf_link_port_id):
for vnf_vl_res in vnf_vl_resource_info:
for vnf_link_port in vnf_vl_res.vnf_link_ports:
if vnf_link_port.id == vnf_link_port_id:
return vnf_link_port
results = []
for vnfc_resource in vnfc_resource_info:
for vnfc_cp_info in vnfc_resource.vnfc_cp_info:
if cpd_id == vnfc_cp_info.cpd_id:
results.append(
_get_vnf_link_port(vnfc_cp_info.vnf_link_port_id))
return results
@grant_error_common
def _change_ext_conn_grant(
self,
context,
vnf_instance,
change_ext_conn_req,
vnf_lcm_op_occ_id):
if not self._get_grant_execute():
return
vnf_inf = vnf_instance.instantiated_vnf_info
def _create_linkport_rd(linkport, cpd_id):
rh = linkport.resource_handle
rd = objects.ResourceDefinition()
rd.resource = objects.ResourceHandle()
rd.id = linkport.id
rd.type = constants.TYPE_LINKPORT
rd.resource_template_id = cpd_id
rd.resource.vim_connection_id = rh.vim_connection_id
rd.resource.resource_id = rh.resource_id
rd.resource.vim_level_resource_type = rh.vim_level_resource_type
return rd
def _get_cpd_id(cp_instance_id):
vnfc_resource_info = vnf_inf.vnfc_resource_info
for vnfc_resource in vnfc_resource_info:
for vnfc_cp_info in vnfc_resource.vnfc_cp_info:
if cp_instance_id == vnfc_cp_info.id:
return vnfc_cp_info.cpd_id
update_resources = dict()
# If network resource of the VirtualLink changed, get all LinkPort
# resource related to VirtualLink
for ext_vl in change_ext_conn_req.ext_virtual_links:
nw_changed_resources = self._get_vnf_link_ports_by_vl(
vnf_inf, ext_vl.id, ext_vl.resource_id)
LOG.debug('nw_changed_resources {}'.format(nw_changed_resources))
if nw_changed_resources:
for resource in nw_changed_resources:
cpd_id = _get_cpd_id(resource.cp_instance_id)
update_resources[resource.resource_handle.resource_id] = \
_create_linkport_rd(resource, cpd_id)
continue
# If network resource of the VirtualLink does not change,
# Searching vnfc_resource_info table by the cpd_id, if found, get
# LinkPort resource corresponding the CP.
# It does not check that the CP status updated or not.
for ext_cp in ext_vl.ext_cps:
cp_changed_resources = \
self._get_vnf_link_ports_by_cp(vnf_inf, ext_cp.cpd_id)
LOG.debug('cp_changed_resources {}'.format(
cp_changed_resources))
for resource in cp_changed_resources:
update_resources[resource.resource_handle.resource_id] = \
_create_linkport_rd(resource, ext_cp.cpd_id)
update_resources_list = list(update_resources.values())
LOG.debug("Update Resources: %s", update_resources_list)
grant_request = self._make_grant_request(
context,
vnf_instance,
vnf_lcm_op_occ_id,
'CHANGE_EXT_CONN',
False,
update_resources=update_resources_list)
return self._grant(context, grant_request)
def _grant(self, context, grant_request):
LOG.info(
"grant start grant_request[%s]" %
@ -855,7 +979,11 @@ class Conductor(manager.Manager):
grant_obj.remove_resources):
msg = "grant remove resource error"
raise exceptions.ValidationError(detail=msg)
if len(
grant_request.update_resources) != len(
grant_obj.update_resources):
msg = "grant update resource error"
raise exceptions.ValidationError(detail=msg)
self._check_res_add_remove_rsc(context, grant_request, grant_obj)
return grant_obj
@ -881,6 +1009,16 @@ class Conductor(manager.Manager):
msg = "grant remove resource error"
raise exceptions.ValidationError(detail=msg)
for update_resource in grant_request.update_resources:
match_flg = False
for rsc in grant_obj.update_resources:
if update_resource.id == rsc.resource_definition_id:
match_flg = True
break
if not match_flg:
msg = "grant update resource error"
raise exceptions.ValidationError(detail=msg)
@grant_error_common
def _instantiate_grant(self,
context,
@ -1307,6 +1445,7 @@ class Conductor(manager.Manager):
is_automatic_invocation,
add_resources=[],
remove_resources=[],
update_resources=[],
placement_constraints=[]):
grant_request = objects.GrantRequest()
grant_request.vnf_instance_id = vnf_instance.id
@ -1330,6 +1469,8 @@ class Conductor(manager.Manager):
grant_request.add_resources = add_resources
if remove_resources:
grant_request.remove_resources = remove_resources
if update_resources:
grant_request.update_resources = update_resources
if placement_constraints:
grant_request.placement_constraints = placement_constraints
@ -1443,7 +1584,12 @@ class Conductor(manager.Manager):
jsonutils.dumps(affected_resources_snake_case)
changed_resource = objects.ResourceChanges.obj_from_primitive(
resource_change_obj, context)
changed_ext_connectivity = \
vnflcm_utils._get_changed_ext_connectivity(
old_vnf_instance=old_vnf_instance,
new_vnf_instance=vnf_instance)
vnf_notif.resource_changes = changed_resource
vnf_notif.changed_ext_connectivity = changed_ext_connectivity
vnf_notif.save()
notification_data['affectedVnfcs'] = \
affected_resources.get('affectedVnfcs', [])
@ -1453,6 +1599,9 @@ class Conductor(manager.Manager):
affected_resources.get('affectedVirtualStorages', [])
notification_data['notificationStatus'] = \
fields.LcmOccsNotificationStatus.RESULT
notification_data['changedExtConnectivity'] = \
utils.convert_snakecase_to_camelcase(
[i.to_dict() for i in changed_ext_connectivity])
if operation_state == \
fields.LcmOccsOperationState.FAILED_TEMP \
@ -2035,6 +2184,113 @@ class Conductor(manager.Manager):
self.vnflcm_driver.rollback_vnf(context, vnf_info,
vnf_instance, operation_params)
@coordination.synchronized('{vnf_instance[id]}')
def change_ext_conn(
self,
context,
vnf_instance,
vnf_dict,
change_ext_conn_req,
vnf_lcm_op_occs_id):
"""Perform change external VNF connectivity operation.
This function will support changing external VNF connectivity
as defined in ETSI NFV SOL 002 and SOL 003, but now, you can
specify changing fixedAddresses or numDynamicAddresses in
ipAddresses attribute in extVirtualLinks.
Note:
1. Get grant from NFVO(if needed).
Request grant information is made from ExtVirtualLinkData
of ChangeExtConnRequest. If ExtVirtualLinkInfo is changed
from instantiated VNF, we inform VnfLinkPortInfo related
to that ExtVirtualLinkInfo. Also, we inform VnfLinkPortInfo
related to each individual VnfExtCpInfo.
2. Call vnflcm_driver to change networks.
Invoke vnflcm_driver to perform change external VNF
connectivity.
3. Update VNF information
Update InstantiatedVnfInfo as a post-processing.
Args:
context (Context): context for security/db session.
vnf_instance (VnfInstance): Information object for VNF instance.
vnf_dict (dict): Container for error point indication.
change_ext_conn_req (ChangeExtConnRequest):
Request object of change external connectivity.
vnf_lcm_op_occs_id (uuid): self-explanatory :)
"""
if vnf_dict['before_error_point'] == EP.INITIAL:
self._change_ext_conn_grant(
context,
vnf_instance,
change_ext_conn_req,
vnf_lcm_op_occs_id)
try:
old_vnf_instance = copy.deepcopy(vnf_instance)
# Update vnf_lcm_op_occs table and send notification "PROCESSING"
self._send_lcm_op_occ_notification(
context=context,
vnf_lcm_op_occs_id=vnf_lcm_op_occs_id,
old_vnf_instance=None,
vnf_instance=vnf_instance,
request_obj=change_ext_conn_req,
operation=fields.LcmOccsOperationType.CHANGE_EXT_CONN
)
vnf_dict['current_error_point'] = EP.NOTIFY_PROCESSING
if vnf_dict['before_error_point'] <= EP.NOTIFY_PROCESSING:
# update vnf status to PENDING_CHANGE_EXT_CONN
self._change_vnf_status(context, vnf_instance.id,
_ACTIVE_STATUS, 'PENDING_CHANGE_EXT_CONN')
self.vnflcm_driver.change_ext_conn_vnf(
context,
vnf_instance,
vnf_dict,
change_ext_conn_req)
vnf_dict['current_error_point'] = EP.NOTIFY_COMPLETED
self._update_instantiated_vnf_info_change_ext_conn(
context, vnf_instance, change_ext_conn_req)
# update vnf status to ACTIVE
self._update_vnf_attributes(context, vnf_instance, vnf_dict,
_PENDING_STATUS, _ACTIVE_STATUS)
# Update vnf_lcm_op_occs table and send notification "COMPLETED"
self._send_lcm_op_occ_notification(
context=context,
vnf_lcm_op_occs_id=vnf_lcm_op_occs_id,
old_vnf_instance=old_vnf_instance,
vnf_instance=vnf_instance,
request_obj=change_ext_conn_req,
operation=fields.LcmOccsOperationType.CHANGE_EXT_CONN,
operation_state=fields.LcmOccsOperationState.COMPLETED
)
except Exception as e:
# update vnf_status to 'ERROR' and create event with 'ERROR' status
self._change_vnf_status(context, vnf_instance.id,
_ALL_STATUSES, constants.ERROR, str(e))
LOG.error('Failed to execute operation. error={}'.format(e))
if vnf_dict['current_error_point'] in [EP.INTERNAL_PROCESSING,
EP.VNF_CONFIG_END]:
self._update_instantiated_vnf_info_change_ext_conn(
context, vnf_instance, change_ext_conn_req)
# update vnf_lcm_op_occs and send notification "FAILED_TEMP"
self._send_lcm_op_occ_notification(
context=context,
vnf_lcm_op_occs_id=vnf_lcm_op_occs_id,
old_vnf_instance=old_vnf_instance,
vnf_instance=vnf_instance,
request_obj=change_ext_conn_req,
operation=fields.LcmOccsOperationType.CHANGE_EXT_CONN,
operation_state=fields.LcmOccsOperationState.FAILED_TEMP,
error=str(e),
error_point=vnf_dict['current_error_point']
)
def init(args, **kwargs):
CONF(args=args, project='tacker',

22
tacker/conductor/conductorrpc/vnf_lcm_rpc.py

@ -136,3 +136,25 @@ class VNFLcmRPCAPI(object):
vnf_info=vnf_info,
vnf_instance=vnf_instance,
operation_params=operation_params)
def change_ext_conn(
self,
context,
vnf_instance,
vnf_dict,
change_ext_conn_req,
vnf_lcm_op_occs_id,
cast=True):
serializer = objects_base.TackerObjectSerializer()
client = rpc.get_client(self.target, version_cap=None,
serializer=serializer)
cctxt = client.prepare()
rpc_method = cctxt.cast if cast else cctxt.call
return rpc_method(
context,
'change_ext_conn',
vnf_instance=vnf_instance,
vnf_dict=vnf_dict,
change_ext_conn_req=change_ext_conn_req,
vnf_lcm_op_occs_id=vnf_lcm_op_occs_id)

4
tacker/extensions/vnfm.py

@ -87,6 +87,10 @@ class VNFHealWaitFailed(exceptions.TackerException):
message = _('VNF Heal %(reason)s')
class VNFChangeExtConnWaitFailed(exceptions.TackerException):
message = _('VNF ChangeExtConn %(reason)s')
class VNFDeleteFailed(exceptions.TackerException):
message = _('%(reason)s')

1
tacker/objects/__init__.py

@ -44,3 +44,4 @@ def register_all():
__import__('tacker.objects.grant')
__import__('tacker.objects.grant_request')
__import__('tacker.objects.vnfd_attribute')
__import__('tacker.objects.change_ext_conn_req')

68
tacker/objects/change_ext_conn_req.py

@ -0,0 +1,68 @@
# 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_log import log as logging
from tacker import objects
from tacker.objects import base
from tacker.objects import fields
LOG = logging.getLogger(__name__)
@base.TackerObjectRegistry.register
class ChangeExtConnRequest(base.TackerObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'vim_connection_info': fields.ListOfObjectsField(
'VimConnectionInfo', nullable=True, default=[]),
'ext_virtual_links': fields.ListOfObjectsField(
'ExtVirtualLinkData', nullable=False),
'additional_params': fields.DictOfNullableField(nullable=True,
default={})
}
@classmethod
def obj_from_primitive(cls, primitive, context):
if 'tacker_object.name' in primitive:
obj_change_ext_conn_req = super(
ChangeExtConnRequest, cls).obj_from_primitive(
primitive, context)
else:
if 'vim_connection_info' in primitive.keys():
obj_data = [objects.VimConnectionInfo._from_dict(
vim_conn) for vim_conn in primitive.get(
'vim_connection_info', [])]
primitive.update({'vim_connection_info': obj_data})
if 'ext_virtual_links' in primitive.keys():
obj_data = [objects.ExtVirtualLinkData.obj_from_primitive(
ext_vir_link, context) for ext_vir_link in primitive.get(
'ext_virtual_links', [])]
primitive.update({'ext_virtual_links': obj_data})
obj_change_ext_conn_req = ChangeExtConnRequest._from_dict(
primitive)
return obj_change_ext_conn_req
@classmethod
def _from_dict(cls, data_dict):
vim_connection_info = data_dict.get('vim_connection_info', [])
ext_virtual_links = data_dict.get('ext_virtual_links', [])
additional_params = data_dict.get('additional_params', {})
return cls(
vim_connection_info=vim_connection_info,
ext_virtual_links=ext_virtual_links,
additional_params=additional_params)

3
tacker/objects/fields.py

@ -233,8 +233,9 @@ class LcmOccsOperationType(BaseTackerEnum):
TERMINATE = 'TERMINATE'
HEAL = 'HEAL'
SCALE = 'SCALE'
CHANGE_EXT_CONN = 'CHANGE_EXT_CONN'
ALL = (INSTANTIATE, TERMINATE, HEAL, SCALE)
ALL = (INSTANTIATE, TERMINATE, HEAL, SCALE, CHANGE_EXT_CONN)
class LcmOccsNotificationStatus(BaseTackerEnum):

23
tacker/objects/grant.py

@ -33,8 +33,12 @@ class Grant(base.TackerObject):
'GrantInfo', nullable=True, default=[]),
'remove_resources': fields.ListOfObjectsField(
'GrantInfo', nullable=True, default=[]),
'update_resources': fields.ListOfObjectsField(
'GrantInfo', nullable=True, default=[]),
'vim_assets': fields.ObjectField(
'VimAssets', nullable=True)
'VimAssets', nullable=True),
'ext_virtual_links': fields.ListOfObjectsField(
'ExtVirtualLinkData', nullable=True, default=[]),
}
@classmethod
@ -65,11 +69,20 @@ class Grant(base.TackerObject):
remove_rsc) for remove_rsc in primitive.get(
'remove_resources', [])]
primitive.update({'remove_resources': obj_data})
if 'update_resources' in primitive.keys():
obj_data = [GrantInfo._from_dict(
update_rsc) for update_rsc in primitive.get(
'update_resources', [])]
primitive.update({'update_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})
if 'ext_virtual_links' in primitive.keys():
obj_data = [objects.ExtVirtualLinkData.obj_from_primitive(
ext_vir_link, context) for ext_vir_link in primitive.get(
'ext_virtual_links', [])]
primitive.update({'ext_virtual_links': obj_data})
obj_grant = Grant._from_dict(primitive)
return obj_grant
@ -83,7 +96,9 @@ class Grant(base.TackerObject):
zones = data_dict.get('zones', [])
add_resources = data_dict.get('add_resources', [])
remove_resources = data_dict.get('remove_resources', [])
update_resources = data_dict.get('update_resources', [])
vim_assets = data_dict.get('vim_assets')
ext_virtual_links = data_dict.get('ext_virtual_links', [])
obj = cls(
id=id,
@ -93,7 +108,9 @@ class Grant(base.TackerObject):
zones=zones,
add_resources=add_resources,
remove_resources=remove_resources,
vim_assets=vim_assets)
update_resources=update_resources,
vim_assets=vim_assets,
ext_virtual_links=ext_virtual_links)
return obj

19
tacker/objects/grant_request.py

@ -35,6 +35,8 @@ class GrantRequest(base.TackerObject):
'ResourceDefinition', nullable=True, default=[]),
'remove_resources': fields.ListOfObjectsField(
'ResourceDefinition', nullable=True, default=[]),
'update_resources': fields.ListOfObjectsField(
'ResourceDefinition', nullable=True, default=[]),
'placement_constraints': fields.ListOfObjectsField(
'PlacementConstraint', nullable=True, default=[]),
'_links': fields.ObjectField(
@ -57,11 +59,20 @@ class GrantRequest(base.TackerObject):
remove_rsc) for remove_rsc in primitive.get(
'remove_resources', [])]
primitive.update({'add_resources': obj_data})
if 'update_resources' in primitive.keys():
obj_data = [ResourceDefinition._from_dict(
update_rsc) for update_rsc in primitive.get(
'update_resources', [])]
primitive.update({'update_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})
if '_links' in primitive.keys():
obj_data = Links._from_dict(
primitive.get('_links', {}))
primitive.update({'_links': obj_data})
obj_grant_req = GrantRequest._from_dict(primitive)
return obj_grant_req
@ -76,6 +87,7 @@ class GrantRequest(base.TackerObject):
is_automatic_invocation = data_dict.get('is_automatic_invocation')
add_resources = data_dict.get('add_resources', [])
remove_resources = data_dict.get('remove_resources', [])
update_resources = data_dict.get('update_resources', [])
placement_constraints = data_dict.get('placement_constraints', [])
links = data_dict.get('_links')
@ -88,6 +100,7 @@ class GrantRequest(base.TackerObject):
is_automatic_invocation=is_automatic_invocation,
add_resources=add_resources,
remove_resources=remove_resources,
update_resources=update_resources,
placement_constraints=placement_constraints,
_links=links)
return obj
@ -112,6 +125,12 @@ class GrantRequest(base.TackerObject):
remove_resources_list.append(remove_resource.to_dict())
data.update({'remove_resources': remove_resources_list})
if self.update_resources:
update_resources_list = []
for update_resource in self.update_resources:
update_resources_list.append(update_resource.to_dict())
data.update({'update_resources': update_resources_list})
if self.placement_constraints:
placement_constraints_list = []
for placement_constraint in self.placement_constraints:

3
tacker/objects/vnf_lcm_op_occs.py

@ -291,7 +291,8 @@ class VnfLcmOpOcc(base.TackerObject, base.TackerObjectDictCompat,
changed_ext_conn = \
[objects.ExtVirtualLinkInfo.obj_from_primitive(
chg_ext_conn, context) for chg_ext_conn in
db_vnf_lcm_op_occ['changed_ext_connectivity']]
jsonutils.loads(
db_vnf_lcm_op_occ['changed_ext_connectivity'])]
vnf_lcm_op_occ_obj.changed_ext_connectivity = changed_ext_conn
vnf_lcm_op_occ_obj._context = context

1
tacker/plugins/common/constants.py

@ -40,6 +40,7 @@ PENDING_SCALE_IN = "PENDING_SCALE_IN"
PENDING_SCALE_OUT = "PENDING_SCALE_OUT"
PENDING_HEAL = "PENDING_HEAL"
PENDING_TERMINATE = "PENDING_TERMINATE"
PENDING_CHANGE_EXT_CONN = "PENDING_CHANGE_EXT_CONN"
DEAD = "DEAD"
ERROR = "ERROR"
NACK = "NACK"

12
tacker/policies/vnf_lcm.py

@ -176,6 +176,18 @@ rules = [
}
]
),
policy.DocumentedRuleDefault(
name=VNFLCM % 'change_ext_conn',
check_str=base.RULE_ADMIN_OR_OWNER,
description="Change external VNF connectivity.",
operations=[
{
'method': 'POST',
'path':
'/vnflcm/v1/vnf_instances/{vnfInstanceId}/change_ext_conn'
}
]
),
]

4
tacker/releasenotes/notes/change_external_vnf_connectivity-444c580a01479f33.yaml

@ -0,0 +1,4 @@
---
features:
- Add REST APIs for Change External VNF Connectivity.

14
tacker/tests/etc/samples/etsi/nfv/test_inst_terminate_vnf_with_vnflcmnoop/Scripts/vnflcm_noop.py

@ -74,3 +74,17 @@ class VnflcmMgmtNoop(vnflcm_abstract_driver.VnflcmMgmtAbstractDriver):
heal_vnf_request, grant,
grant_request, **kwargs):
pass
@log.log
def change_external_connectivity_start(
self, context, vnf_instance,
change_ext_conn_request, grant,
grant_request, **kwargs):
pass
@log.log
def change_external_connectivity_end(
self, context, vnf_instance,
change_ext_conn_request, grant,
grant_request, **kwargs):
pass

2
tacker/tests/etc/samples/etsi/nfv/test_inst_terminate_vnf_with_vnflcmnoop/TOSCA-Metadata/TOSCA.meta

@ -9,4 +9,4 @@ Content-type: application/x-iso9066-image
Name: Scripts/vnflcm_noop.py
Content-Type: text/x-python
Algorithm: SHA-256
Hash: 63cfaf9963680ff864981d4db809c2ec175d78054157c0bcd43ac7a85973af10
Hash: 950422e356c3eb6a7c92f424d975e2d3f295f480321bd91a97501045eddeab4a

38
tacker/tests/functional/sol/vnflcm/base.py

@ -262,6 +262,9 @@ class BaseVnfLcmTest(base.BaseTackerTest):
self.ext_link_ports = list()
# Create external subnet in net1
self.ext_subnets = list() # Store ids for cleaning.
# Create external networks to change.
self.changed_ext_networks = list()
self.changed_ext_subnets = list() # Store ids for cleaning.
networks = self.neutronclient().list_networks()
for nw in networks.get('networks'):
@ -281,13 +284,26 @@ class BaseVnfLcmTest(base.BaseTackerTest):
ext_mngd_net_id, _ = \
self._create_network("external_managed_internal_net")
self.ext_mngd_networks.append(ext_mngd_net_id)
changed_ext_net_id, changed_ext_net_name = \
self._create_network("changed_external_net")
self.changed_ext_networks.append(changed_ext_net_id)
# Chack how many networks are created.
networks = self.neutronclient().list_networks()
for nw in networks.get('networks'):
if nw['name'] not in [ext_net_name]:
if nw['name'] not in [ext_net_name, changed_ext_net_name]:
continue
self.ext_subnets.append(self._create_subnet(nw))
elif nw['name'] == ext_net_name:
self.ext_subnets.append(
self._create_subnet(nw,
cidr="22.22.1.0/24",
gateway="22.22.1.1"))
elif nw['name'] == changed_ext_net_name:
self.changed_ext_subnets.append(
self._create_subnet(nw,
cidr="22.22.2.0/24",
gateway="22.22.2.1"))
@classmethod
def _list_glance_image(cls, filter_name='cirros-0.4.0-x86_64-disk'):
@ -453,6 +469,16 @@ class BaseVnfLcmTest(base.BaseTackerTest):
return resp, body
def _change_ext_conn_vnf_instance(self, vnf_instance_id, request_body):
url = os.path.join(
self.base_vnf_instances_url,
vnf_instance_id,
"change_ext_conn")
resp, body = self.http_client.do_request(url, "POST",
body=jsonutils.dumps(request_body))
return resp, body
def _rollback_op_occs(self, vnf_lcm_op_occs_id):
rollback_url = os.path.join(
self.base_vnf_lcm_op_occs_url,
@ -1140,18 +1166,18 @@ class BaseVnfLcmTest(base.BaseTackerTest):
self.fail("Failed, create network=<%s>, %s" %
(uniq_name, e))
def _create_subnet(self, network):
cidr_prefix = "22.22.{}".format(str(len(self.ext_subnets)))
def _create_subnet(self, network, cidr, gateway):
body = {'subnet': {'network_id': network['id'],
'name': "subnet-%s" % uuidutils.generate_uuid(),
'cidr': "{}.0/24".format(cidr_prefix),
'cidr': "{}".format(cidr),
'ip_version': 4,
'gateway_ip': "{}.1".format(cidr_prefix),
'gateway_ip': "{}".format(gateway),
"enable_dhcp": True}}
try:
subnet = self.neutronclient().create_subnet(body=body)["subnet"]
self.addCleanup(self._delete_subnet, subnet['id'])
print("Create subnet success, %s" % subnet['id'], flush=True)
return subnet['id']
except Exception as e:
self.fail("Failed, create subnet for net_id=<%s>, %s" %

56
tacker/tests/functional/sol/vnflcm/fake_vnflcm.py

@ -40,7 +40,8 @@ class Subscription:
"SCALE",
"TERMINATE",
"HEAL",
"MODIFY_INFO"
"MODIFY_INFO",
"CHANGE_EXT_CONN"
]
},
"callbackUri": callback_uri
@ -373,3 +374,56 @@ class VnfInstances:
"samplekey": "samplevalue"
}
}
@staticmethod
def make_change_ext_conn_request_body(
tenant_id,
networks_id,
external_subnets_id):
# set external subnet_id on vim.
ext_cps_vdu2_cp2 = {
"cpdId": "VDU2_CP2",
"cpConfig": [{
"cpProtocolData": [{
"layerProtocol": "IP_OVER_ETHERNET",
"ipOverEthernet": {
"ipAddresses": [{
"type": "IPV4",
"fixedAddresses": ["22.22.2.200"],
"subnetId": external_subnets_id[0]
}]
}
}]
}]
}
ext_virtual_link_cp2 = {
"id": uuidsentinel.evl2_id,
"resourceId": networks_id[0],
"extCps": [
ext_cps_vdu2_cp2
]
}
data = {
"extVirtualLinks": [
ext_virtual_link_cp2
],
"vimConnectionInfo": [{
"id": uuidsentinel.vim_connection_id,
"vimType": "ETSINFV.OPENSTACK_KEYSTONE.v_2",
"vimConnectionId": uuidsentinel.vim_connection_id,
"interfaceInfo": {
"endpoint": "http://127.0.0.1/identity"
},
"accessInfo": {
"username": "nfv_user",
"region": "RegionOne",
"password": "devstack",
"tenant": tenant_id
}
}],
}
return data

185
tacker/tests/functional/sol/vnflcm/test_vnf_instance_with_user_data.py

@ -1596,6 +1596,48 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest):
expected_usage_state,
vnf_package_info['usageState'])
def _get_fixed_ips(self, vnf_instance_id, request_body):
res_name = None
for extvirlink in request_body['extVirtualLinks']:
if 'extCps' not in extvirlink:
continue
for extcps in extvirlink['extCps']:
if 'cpdId' in extcps:
if res_name is None:
res_name = list()
res_name.append(extcps['cpdId'])
break
if res_name is None:
return []
stack = self._get_heat_stack(vnf_instance_id)
stack_id = stack.id
stack_resource = self._get_heat_resource_list(stack_id, nested_depth=2)
releations = dict()
for elmt in stack_resource:
if elmt.resource_type != 'OS::Neutron::Port':
continue
if elmt.resource_name not in res_name:
continue
releations[elmt.parent_resource] = elmt.resource_name
details = list()
for (parent_name, resource_name) in releations.items():
for elmt in stack_resource:
if parent_name != elmt.resource_name:
continue
detail_stack = self._get_heat_resource(
elmt.physical_resource_id, resource_name)
details.append(detail_stack)
ans_list = list()
for detail in details:
ans_list.append(detail.attributes['fixed_ips'])
return ans_list
def _assert_occ_show(self, resp, op_occs_info):
self.assertEqual(200, resp.status_code)
@ -1684,3 +1726,146 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest):
self.assertIsNotNone(_links.get('rollback').get('href'))
if _links.get('grant') is not None:
self.assertIsNotNone(_links.get('grant').get('href'))
def test_inst_chgextconn_term(self):
"""Test basic life cycle operations with sample VNFD.
In this test case, we do following steps.
- Create subscription.
- Show subscriptions.
- Get list of subscriptions.
- Create VNF package.
- Upload VNF package.
- Create VNF instance.
- Instantiate VNF.
- Get list of VNF instances.
- Get VNF informations.
- Change External VNF Connectivity.
- Get opOccs informations.
- Terminate VNF
- Delete VNF
- Delete subscription
"""
# Create subscription and register it.
request_body = fake_vnflcm.Subscription.make_create_request_body(
'http://localhost:{}{}'.format(
vnflcm_base.FAKE_SERVER_MANAGER.SERVER_PORT,
os.path.join(vnflcm_base.MOCK_NOTIFY_CALLBACK_URL,
self._testMethodName)))
resp, response_body = self._register_subscription(request_body)
self.assertEqual(201, resp.status_code)
self.assert_http_header_location_for_subscription(resp.headers)
subscription_id = response_body.get('id')
self.addCleanup(
self._delete_subscription,
subscription_id)
# Subscription show
resp, body = self._wait_show_subscription(subscription_id)
self.assert_subscription_show(resp, body)
# Subscription list
resp, _ = self._list_subscription()
self.assertEqual(200, resp.status_code)
# Pre Setting: Create vnf package.
sample_name = 'functional5'
csar_package_path = os.path.abspath(
os.path.join(
os.path.dirname(__file__),
"../../../etc/samples/etsi/nfv",
sample_name))
tempname, _ = vnflcm_base._create_csar_with_unique_vnfd_id(
csar_package_path)
# upload vnf package
vnf_package_id, vnfd_id = vnflcm_base._create_and_upload_vnf_package(
self.tacker_client, user_defined_data={
"key": sample_name}, temp_csar_path=tempname)
# Post Setting: Reserve deleting vnf package.
self.addCleanup(vnflcm_base._delete_vnf_package, self.tacker_client,
vnf_package_id)
# Create vnf instance
resp, vnf_instance = self._create_vnf_instance_from_body(
fake_vnflcm.VnfInstances.make_create_request_body(vnfd_id))
vnf_instance_id = vnf_instance['id']
self._wait_lcm_done(vnf_instance_id=vnf_instance_id)
self.assert_create_vnf(resp, vnf_instance, vnf_package_id)
vnf_instance_name = vnf_instance['vnfInstanceName']
self.addCleanup(self._delete_vnf_instance, vnf_instance_id)
# Instantiate vnf instance
request_body = fake_vnflcm.VnfInstances.make_inst_request_body(
self.vim['tenant_id'], self.ext_networks, self.ext_mngd_networks,
self.ext_link_ports, self.ext_subnets)
resp, _ = self._instantiate_vnf_instance(vnf_instance_id, request_body)
self._wait_lcm_done('COMPLETED', vnf_instance_id=vnf_instance_id)
self.assert_instantiate_vnf(resp, vnf_instance_id, vnf_package_id)
# List vnf instance
filter_expr = {
'filter': "(eq,id,{});(eq,vnfInstanceName,{})".format(
vnf_instance_id, vnf_instance_name)}
resp, vnf_instances = self._list_vnf_instance(params=filter_expr)
self.assertEqual(200, resp.status_code)
self.assertEqual(1, len(vnf_instances))
# Show vnf instance
resp, vnf_instance = self._show_vnf_instance(vnf_instance_id)
self.assertEqual(200, resp.status_code)
# Change external connectivity
request_body = \
fake_vnflcm.VnfInstances.make_change_ext_conn_request_body(
self.vim['tenant_id'], self.changed_ext_networks,
self.changed_ext_subnets)
before_fixed_ips = self._get_fixed_ips(vnf_instance_id, request_body)
resp, _ = \
self._change_ext_conn_vnf_instance(vnf_instance_id, request_body)
self._wait_lcm_done('COMPLETED', vnf_instance_id=vnf_instance_id)
after_fixed_ips = self._get_fixed_ips(vnf_instance_id, request_body)
self.assertNotEqual(before_fixed_ips, after_fixed_ips)
callback_url = os.path.join(
vnflcm_base.MOCK_NOTIFY_CALLBACK_URL,
self._testMethodName)
notify_mock_responses = vnflcm_base.FAKE_SERVER_MANAGER.get_history(
callback_url)
vnflcm_base.FAKE_SERVER_MANAGER.clear_history(
callback_url)
vnflcm_op_occ_id = notify_mock_responses[0].request_body.get(
'vnfLcmOpOccId')
self.assertIsNotNone(vnflcm_op_occ_id)
vnflcm_base.FAKE_SERVER_MANAGER.clear_history(callback_url)
# occ-show(chgextconn)
resp, op_occs_info = self._show_op_occs(vnflcm_op_occ_id)
self._assert_occ_show(resp, op_occs_info)