Support scaling operations for VNF based on ETSI

Implemented scaling VNF feature.

* POST /vnflcm/v1/vnf_instances/{vnfInstanceId}/scale

Implements: blueprint support-etsi-nfv-specs
Spec: https://specs.openstack.org/openstack/tacker-specs/specs/victoria/support-scale-api-based-on-etsi-nfv-sol.html

Change-Id: Ief7d52af908581c00939b3c6c23de7c85ea5f7cf
changes/11/747511/27
Aldinson Esto 2 years ago
parent 8a715cce10
commit e8623c1df0
  1. 33
      api-ref/source/v1/parameters_vnflcm.yaml
  2. 5
      api-ref/source/v1/samples/vnflcm/scale-vnf-instance-request.json
  3. 42
      api-ref/source/v1/vnflcm.inc
  4. 13
      tacker/api/schemas/vnf_lcm.py
  5. 133
      tacker/api/vnflcm/v1/controller.py
  6. 7
      tacker/api/vnflcm/v1/router.py
  7. 20
      tacker/conductor/conductor_server.py
  8. 18
      tacker/conductor/conductorrpc/vnf_lcm_rpc.py
  9. 1
      tacker/db/db_sqlalchemy/models.py
  10. 2
      tacker/db/migration/alembic_migrations/versions/HEAD
  11. 35
      tacker/db/migration/alembic_migrations/versions/ee98bbc0789d_add_scale_column.py
  12. 57
      tacker/db/vnfm/vnfm_db.py
  13. 1
      tacker/objects/__init__.py
  14. 16
      tacker/objects/instantiate_vnf_req.py
  15. 52
      tacker/objects/scale_vnf_request.py
  16. 76
      tacker/objects/vnf_instantiated_info.py
  17. 11
      tacker/policies/vnf_lcm.py
  18. 14
      tacker/tests/etc/samples/hot_lcm_template.yaml
  19. 6
      tacker/tests/functional/base.py
  20. 15
      tacker/tests/unit/conductor/fakes.py
  21. 46
      tacker/tests/unit/conductor/test_conductor_server.py
  22. 43
      tacker/tests/unit/vnflcm/fakes.py
  23. 307
      tacker/tests/unit/vnflcm/test_controller.py
  24. 155
      tacker/tests/unit/vnflcm/test_vnflcm_driver.py
  25. 48
      tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py
  26. 571
      tacker/vnflcm/vnflcm_driver.py
  27. 42
      tacker/vnfm/infra_drivers/kubernetes/kubernetes_driver.py
  28. 408
      tacker/vnfm/infra_drivers/openstack/openstack.py
  29. 47
      tacker/vnfm/infra_drivers/scale_driver.py
  30. 3
      tacker/vnfm/mgmt_drivers/constants.py

@ -163,6 +163,12 @@ affected_vnfcs_vdu_id:
in: body
required: true
type: string
aspect_id:
description: |
Identifier of the scaling aspect.
in: body
required: true
type: string
authentication:
description: |
Authentication parameters to configure the use of
@ -842,6 +848,14 @@ notification_vnf_lcm_op_occ_id:
in: body
required: true
type: string
number_of_steps:
description: |
Number of scaling steps to be executed as part of this
Scale VNF operation. It shall be a positive number and the
default value shall be 1.
in: body
required: false
type: int
operation:
description: |
Type of the actual LCM operation represented by this
@ -922,6 +936,14 @@ resource_handle_vim_level_resource_type:
in: body
required: false
type: string
scale_additional_params:
description: |
Additional parameters passed by the NFVO as input to the
scaling process, specific to the VNF being scaled, as
declared in the VNFD as part of "ScaleVnfOpConfig".
in: body
required: false
type: string
scale_status:
description: |
Scale status of the VNF, one entry per aspect.
@ -946,6 +968,17 @@ scale_status_scale_level:
in: body
required: true
type: string
scale_type:
description: |
Indicates the type of the scale operation requested.
Permitted values:
SCALE_OUT: adding additional VNFC instances to the VNF to increase capacity.
SCALE_IN: removing VNFC instances from the VNF in order to release unused capacity.
in: body
required: true
type: string
start_time:
description: |
Date-time of the start of the operation.

@ -0,0 +1,5 @@
{
"type": "SCALE_OUT",
"aspectId": "scale_aspect",
"numberOfSteps": "1"
}

@ -570,6 +570,48 @@ Response Example
.. literalinclude:: samples/vnflcm/list-vnf-instance-response.json
:language: javascript
Scale a VNF instance
========================
.. rest_method:: POST /vnflcm/v1/vnf_instances/{vnfInstanceId}/scale
This task resource represents the "Scale VNF" operation. The client can use
this resource to request scaling a VNF instance.
The POST method requests to scale 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
- type: scale_type
- aspectId: aspect_id
- numberOfSteps: number_of_steps
- additionalParams: scale_additional_params
Request Example
---------------
.. literalinclude:: samples/vnflcm/scale-vnf-instance-request.json
:language: javascript
Modify a VNF instance
========================

@ -252,3 +252,16 @@ update = {
},
'additionalProperties': False,
}
scale = {
'type': 'object',
'properties': {
'type': {'type': 'string',
'enum': ['SCALE_OUT', 'SCALE_IN']},
'aspectId': {'type': 'string'},
'numberOfSteps': {'type': 'integer'},
'additionalParams': parameter_types.keyvalue_pairs
},
'required': ['type', 'aspectId'],
'additionalProperties': False,
}

@ -984,7 +984,138 @@ class VnfLcmController(wsgi.Controller):
return self._make_problem_detail(
str(e), 500, title='Internal Server Error')
# Generate a response when an error occurs as a problem_detail object
def _scale(self, context, vnf_info, vnf_instance, request_body):
req_body = utils.convert_camelcase_to_snakecase(request_body)
scale_vnf_request = objects.ScaleVnfRequest.obj_from_primitive(
req_body, context=context)
inst_vnf_info = vnf_instance.instantiated_vnf_info
aspect = False
current_level = 0
for scale in inst_vnf_info.scale_status:
if scale_vnf_request.aspect_id == scale.aspect_id:
aspect = True
current_level = scale.scale_level
break
if not aspect:
return self._make_problem_detail(
'aspectId not in ScaleStatus',
400,
title='aspectId not in ScaleStatus')
if not scale_vnf_request.number_of_steps:
scale_vnf_request.number_of_steps = 1
if not scale_vnf_request.additional_params:
scale_vnf_request.additional_params = {"is_reverse": "False",
"is_auto": "False"}
if not scale_vnf_request.additional_params.get('is_reverse'):
scale_vnf_request.additional_params['is_reverse'] = "False"
if not scale_vnf_request.additional_params.get('is_auto'):
scale_vnf_request.additional_params['is_auto'] = "False"
if scale_vnf_request.type == 'SCALE_IN':
if current_level == 0 or\
current_level < scale_vnf_request.number_of_steps:
return self._make_problem_detail(
'can not scale_in', 400, title='can not scale_in')
scale_level = current_level - scale_vnf_request.number_of_steps
elif scale_vnf_request.type == 'SCALE_OUT':
scaleGroupDict = jsonutils.loads(
vnf_info['attributes']['scale_group'])
max_level = (scaleGroupDict['scaleGroupDict']
[scale_vnf_request.aspect_id]['maxLevel'])
scale_level = current_level + scale_vnf_request.number_of_steps
if max_level < scale_level:
return self._make_problem_detail(
'can not scale_out', 400, title='can not scale_out')
vnf_lcm_op_occs_id = uuidutils.generate_uuid()
timestamp = datetime.datetime.utcnow()
operation_params = {
'type': scale_vnf_request.type,
'aspect_id': scale_vnf_request.aspect_id,
'number_of_steps': scale_vnf_request.number_of_steps,
'additional_params': scale_vnf_request.additional_params}
vnf_lcm_op_occ = objects.VnfLcmOpOcc(
context=context,
id=vnf_lcm_op_occs_id,
operation_state='STARTING',
state_entered_time=timestamp,
start_time=timestamp,
vnf_instance_id=inst_vnf_info.vnf_instance_id,
operation='SCALE',
is_automatic_invocation=scale_vnf_request.additional_params.get('\
is_auto'),
operation_params=json.dumps(operation_params),
error_point=1)
vnf_lcm_op_occ.create()
vnflcm_url = CONF.vnf_lcm.endpoint_url + \
"/vnflcm/v1/vnf_lcm_op_occs/" + vnf_lcm_op_occs_id
insta_url = CONF.vnf_lcm.endpoint_url + \
"/vnflcm/v1/vnf_instances/" + inst_vnf_info.vnf_instance_id
vnf_info['vnflcm_id'] = vnf_lcm_op_occs_id
vnf_info['vnf_lcm_op_occ'] = vnf_lcm_op_occ
vnf_info['after_scale_level'] = scale_level
vnf_info['scale_level'] = current_level
self.rpc_api.scale(context, vnf_info, vnf_instance, scale_vnf_request)
notification = {}
notification['notificationType'] = \
'VnfLcmOperationOccurrenceNotification'
notification['vnfInstanceId'] = inst_vnf_info.vnf_instance_id
notification['notificationStatus'] = 'START'
notification['operation'] = 'SCALE'
notification['operationState'] = 'STARTING'
notification['isAutomaticInvocation'] = \
scale_vnf_request.additional_params.get('is_auto')
notification['vnfLcmOpOccId'] = vnf_lcm_op_occs_id
notification['_links'] = {}
notification['_links']['vnfInstance'] = {}
notification['_links']['vnfInstance']['href'] = insta_url
notification['_links']['vnfLcmOpOcc'] = {}
notification['_links']['vnfLcmOpOcc']['href'] = vnflcm_url
self.rpc_api.send_notification(context, notification)
vnf_info['notification'] = notification
res = webob.Response()
res.status_int = 202
location = ('Location', vnflcm_url)
res.headerlist.append(location)
return res
@validation.schema(vnf_lcm.scale)
@wsgi.response(http_client.ACCEPTED)
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN,
http_client.NOT_FOUND, http_client.CONFLICT))
def scale(self, request, id, body):
context = request.environ['tacker.context']
context.can(vnf_lcm_policies.VNFLCM % 'scale')
try:
vnf_info = self._vnfm_plugin.get_vnf(context, id)
if vnf_info['status'] != constants.ACTIVE:
return self._make_problem_detail(
'VNF IS NOT ACTIVE', 409, title='VNF IS NOT ACTIVE')
vnf_instance = self._get_vnf_instance(context, id)
if not vnf_instance.instantiated_vnf_info.scale_status:
return self._make_problem_detail(
'NOT SCALE VNF', 409, title='NOT SCALE VNF')
return self._scale(context, vnf_info, vnf_instance, body)
except vnfm.VNFNotFound as vnf_e:
return self._make_problem_detail(
str(vnf_e), 404, title='VNF NOT FOUND')
except webob.exc.HTTPNotFound as inst_e:
return self._make_problem_detail(
str(inst_e), 404, title='VNF NOT FOUND')
except Exception as e:
LOG.error(traceback.format_exc())
return self._make_problem_detail(
str(e), 500, title='Internal Server Error')
def _make_problem_detail(
self,
detail,

@ -97,6 +97,13 @@ class VnflcmAPIRouter(wsgi.Router):
"/vnf_instances/{id}/heal",
methods, controller, default_resource)
# Allowed methods on
# /vnflcm/v1/vnf_instances/{vnfInstanceId}/scale resource
methods = {"POST": "scale"}
self._setup_route(mapper,
"/vnf_instances/{id}/scale",
methods, controller, default_resource)
methods = {"GET": "subscription_list", "POST": "register_subscription"}
self._setup_route(mapper, "/subscriptions",
methods, controller, default_resource)

@ -918,7 +918,7 @@ class Conductor(manager.Manager):
self.__set_auth_subscription(line)
for num in range(CONF.vnf_lcm.retry_num):
LOG.warn("send notify[%s]" % json.dumps(notification))
LOG.info("send notify[%s]" % json.dumps(notification))
auth_client = auth.auth_manager.get_auth_client(
notification['subscriptionId'])
response = auth_client.post(
@ -953,7 +953,7 @@ class Conductor(manager.Manager):
except Exception as e:
LOG.warn("Internal Sever Error[%s]" % str(e))
LOG.warn(traceback.format_exc())
return 99
return -2
return 0
@coordination.synchronized('{vnf_instance[id]}')
@ -1148,6 +1148,22 @@ class Conductor(manager.Manager):
error=str(ex)
)
@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
self.vnflcm_driver.scale_vnf(
context, vnf_info, vnf_instance, scale_vnf_request)
def __set_auth_subscription(self, vnf_lcm_subscription):
def decode(val):
return val if isinstance(val, str) else val.decode()

@ -85,13 +85,12 @@ class VNFLcmRPCAPI(object):
vnfd_pkg_data=vnfd_pkg_data,
vnfd_id=vnfd_id)
def update_vnf_instance_content(
def scale(
self,
context,
vnf_lcm_opoccs,
body_data,
vnfd_pkg_data,
vnfd_id,
vnf_info,
vnf_instance,
scale_vnf_request,
cast=True):
serializer = objects_base.TackerObjectSerializer()
@ -99,11 +98,10 @@ class VNFLcmRPCAPI(object):
serializer=serializer)
cctxt = client.prepare()
rpc_method = cctxt.cast if cast else cctxt.call
return rpc_method(context, 'update_lcm',
vnf_lcm_opoccs=vnf_lcm_opoccs,
body_data=body_data,
vnfd_pkg_data=vnfd_pkg_data,
vnfd_id=vnfd_id)
return rpc_method(context, 'scale',
vnf_info=vnf_info,
vnf_instance=vnf_instance,
scale_vnf_request=scale_vnf_request)
def send_notification(self, context, notification, cast=True):
serializer = objects_base.TackerObjectSerializer()

@ -230,6 +230,7 @@ class VnfInstantiatedInfo(model_base.BASE, models.SoftDeleteMixin,
sa.ForeignKey('vnf_instances.id'),
nullable=False)
flavour_id = sa.Column(sa.String(255), nullable=False)
scale_status = sa.Column(sa.JSON(), nullable=True)
ext_cp_info = sa.Column(sa.JSON(), nullable=False)
ext_virtual_link_info = sa.Column(sa.JSON(), nullable=True)
ext_managed_virtual_link_info = sa.Column(sa.JSON(), nullable=True)

@ -0,0 +1,35 @@
# 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 scale column
Revision ID: ee98bbc0789d
Revises: c47a733f425a
Create Date: 2020-09-11 16:39:04.039173
"""
# flake8: noqa: E402
# revision identifiers, used by Alembic.
revision = 'ee98bbc0789d'
down_revision = 'c47a733f425a'
from alembic import op
import sqlalchemy as sa
def upgrade(active_plugins=None, options=None):
op.add_column('vnf_instantiated_info',
sa.Column('scale_status', sa.JSON(), nullable=True))

@ -537,6 +537,63 @@ class VNFMPluginDb(vnfm.VNFMPluginBase, db_base.CommonDbMixin):
tstamp=timeutils.utcnow())
return updated_vnf_dict
def _update_vnf_scaling_status_err(self,
context,
vnf_info):
previous_statuses = ['PENDING_SCALE_OUT', 'PENDING_SCALE_IN', 'ACTIVE']
try:
with context.session.begin(subtransactions=True):
self._update_vnf_status_db(
context, vnf_info['id'], previous_statuses, 'ERROR')
except Exception as e:
LOG.warning("Failed to revert scale info for vnf "
"instance %(id)s. Error: %(error)s",
{"id": vnf_info['id'], "error": e})
self._cos_db_plg.create_event(
context, res_id=vnf_info['id'],
res_type=constants.RES_TYPE_VNF,
res_state='ERROR',
evt_type=constants.RES_EVT_SCALE,
tstamp=timeutils.utcnow())
def _update_vnf_scaling(self,
context,
vnf_info,
previous_statuses,
status,
vnf_instance=None,
vnf_lcm_op_occ=None):
with context.session.begin(subtransactions=True):
timestamp = timeutils.utcnow()
(self._model_query(context, VNF).
filter(VNF.id == vnf_info['id']).
filter(VNF.status == previous_statuses).
update({'status': status,
'updated_at': timestamp}))
dev_attrs = vnf_info.get('attributes', {})
(context.session.query(VNFAttribute).
filter(VNFAttribute.vnf_id == vnf_info['id']).
filter(~VNFAttribute.key.in_(dev_attrs.keys())).
delete(synchronize_session='fetch'))
for (key, value) in dev_attrs.items():
if 'vim_auth' not in key:
self._vnf_attribute_update_or_create(
context, vnf_info['id'], key, value)
self._cos_db_plg.create_event(
context, res_id=vnf_info['id'],
res_type=constants.RES_TYPE_VNF,
res_state=status,
evt_type=constants.RES_EVT_SCALE,
tstamp=timestamp)
if vnf_lcm_op_occ:
vnf_lcm_op_occ.state_entered_time = timestamp
vnf_lcm_op_occ.save()
if vnf_instance:
vnf_instance.save()
def _update_vnf_pre(self, context, vnf_id, new_status):
with context.session.begin(subtransactions=True):
vnf_db = self._update_vnf_status_db(

@ -41,3 +41,4 @@ def register_all():
__import__('tacker.objects.terminate_vnf_req')
__import__('tacker.objects.vnf_artifact')
__import__('tacker.objects.vnf_lcm_subscriptions')
__import__('tacker.objects.scale_vnf_request')

@ -101,8 +101,8 @@ def _get_cp_protocol_data_list(ext_cp_info):
ip_addresses = []
for ip_address in ip_addresses:
# TODO(nitin-uikey): How to determine num_dynamic_addresses
# back from InstantiatedVnfInfo->IpAddress.
ip_address_data = IpAddress(
# back from InstantiatedVnfInfo->IpAddressReq.
ip_address_data = IpAddressReq(
type=ip_address.type,
subnet_id=ip_address.subnet_id,
fixed_addresses=ip_address.addresses)
@ -479,8 +479,12 @@ class IpOverEthernetAddressData(base.TackerObject):
VERSION = '1.0'
fields = {
'mac_address': fields.StringField(nullable=True, default=None),
'ip_addresses': fields.ListOfObjectsField('IpAddress', nullable=True,
'mac_address': fields.StringField(
nullable=True,
default=None),
'ip_addresses': fields.ListOfObjectsField(
'IpAddressReq',
nullable=True,
default=[]),
}
@ -492,7 +496,7 @@ class IpOverEthernetAddressData(base.TackerObject):
primitive, context)
else:
if 'ip_addresses' in primitive.keys():
obj_data = [IpAddress._from_dict(
obj_data = [IpAddressReq._from_dict(
ip_address) for ip_address in primitive.get(
'ip_addresses', [])]
primitive.update({'ip_addresses': obj_data})
@ -510,7 +514,7 @@ class IpOverEthernetAddressData(base.TackerObject):
@base.TackerObjectRegistry.register
class IpAddress(base.TackerObject):
class IpAddressReq(base.TackerObject):
# Version 1.0: Initial version
VERSION = '1.0'

@ -0,0 +1,52 @@
# 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.objects import base
from tacker.objects import fields
@base.TackerObjectRegistry.register
class ScaleVnfRequest(base.TackerObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'type': fields.StringField(nullable=False),
'aspect_id': fields.StringField(nullable=False),
'number_of_steps': fields.IntegerField(nullable=True, default=1),
'additional_params': fields.DictOfStringsField(nullable=True,
default={}),
}
@classmethod
def obj_from_primitive(cls, primitive, context):
if 'tacker_object.name' in primitive:
obj_scle_vnf_req = super(
ScaleVnfRequest, cls).obj_from_primitive(primitive, context)
else:
obj_scle_vnf_req = ScaleVnfRequest._from_dict(primitive)
return obj_scle_vnf_req
@classmethod
def _from_dict(cls, data_dict):
type = data_dict.get('type')
aspect_id = data_dict.get('aspect_id')
number_of_steps = data_dict.get('number_of_steps')
additional_params = data_dict.get('additional_params')
obj = cls(type=type,
aspect_id=aspect_id,
number_of_steps=number_of_steps,
additional_params=additional_params)
return obj

@ -72,6 +72,8 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat,
fields = {
'flavour_id': fields.StringField(nullable=False),
'vnf_instance_id': fields.UUIDField(nullable=False),
'scale_status': fields.ListOfObjectsField(
'ScaleInfo', nullable=True, default=[]),
'ext_cp_info': fields.ListOfObjectsField(
'VnfExtCpInfo', nullable=False),
'ext_virtual_link_info': fields.ListOfObjectsField(
@ -142,7 +144,8 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat,
@staticmethod
def _from_db_object(context, inst_vnf_info, db_inst_vnf_info):
special_fields = ['ext_cp_info', 'ext_virtual_link_info',
special_fields = ['scale_status',
'ext_cp_info', 'ext_virtual_link_info',
'ext_managed_virtual_link_info',
'vnfc_resource_info',
'vnf_virtual_link_resource_info',
@ -154,6 +157,11 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat,
setattr(inst_vnf_info, key, db_inst_vnf_info.get(key))
scale_status = db_inst_vnf_info['scale_status']
scale_status_list = [ScaleInfo.obj_from_primitive(scale, context)
for scale in scale_status]
inst_vnf_info.scale_status = scale_status_list
ext_cp_info = db_inst_vnf_info['ext_cp_info']
ext_cp_info_list = [VnfExtCpInfo.obj_from_primitive(ext_cp, context)
for ext_cp in ext_cp_info]
@ -226,6 +234,12 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat,
InstantiatedVnfInfo, cls).obj_from_primitive(
primitive, context)
else:
if 'scale_status' in primitive.keys():
obj_data = [ScaleInfo.obj_from_primitive(
scale, context) for scale in primitive.get(
'scale_status', [])]
primitive.update({'scale_status': obj_data})
if 'ext_cp_info' in primitive.keys():
obj_data = [VnfExtCpInfo.obj_from_primitive(
vnf_ext_cp, context) for vnf_ext_cp in primitive.get(
@ -285,6 +299,7 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat,
@classmethod
def _from_dict(cls, data_dict):
flavour_id = data_dict.get('flavour_id')
scale_status = data_dict.get('scale_status', [])
ext_cp_info = data_dict.get('ext_cp_info', [])
ext_virtual_link_info = data_dict.get('ext_virtual_link_info', [])
ext_managed_virtual_link_info = data_dict.get(
@ -299,7 +314,9 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat,
additional_params = data_dict.get('additional_params', {})
vnfc_info = data_dict.get('vnfc_info', [])
obj = cls(flavour_id=flavour_id, ext_cp_info=ext_cp_info,
obj = cls(flavour_id=flavour_id,
scale_status=scale_status,
ext_cp_info=ext_cp_info,
ext_virtual_link_info=ext_virtual_link_info,
ext_managed_virtual_link_info=ext_managed_virtual_link_info,
vnfc_resource_info=vnfc_resource_info,
@ -315,6 +332,13 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat,
data = {'flavour_id': self.flavour_id,
'vnf_state': self.vnf_state}
if self.scale_status:
scale_status_list = []
for scale_status in self.scale_status:
scale_status_list.append(scale_status.to_dict())
data.update({'scale_status': scale_status_list})
ext_cp_info_list = []
for ext_cp_info in self.ext_cp_info:
ext_cp_info_list.append(ext_cp_info.to_dict())
@ -377,6 +401,7 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat,
def reinitialize(self):
# Reinitialize vnf to non instantiated state.
self.scale_status = []
self.ext_cp_info = []
self.ext_virtual_link_info = []
self.ext_managed_virtual_link_info = []
@ -396,6 +421,44 @@ class InstantiatedVnfInfo(base.TackerObject, base.TackerObjectDictCompat,
_destroy_instantiated_vnf_info(context, self.vnf_instance_id)
@base.TackerObjectRegistry.register
class ScaleInfo(base.TackerObject, base.TackerObjectDictCompat,
base.TackerPersistentObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'aspect_id': fields.StringField(nullable=False),
'scale_level': fields.IntegerField(nullable=False),
}
@classmethod
def obj_from_primitive(cls, primitive, context):
if 'tacker_object.name' in primitive:
obj_scale_status = super(
ScaleInfo, cls).obj_from_primitive(
primitive, context)
else:
obj_scale_status = ScaleInfo._from_dict(primitive)
return obj_scale_status
@classmethod
def _from_dict(cls, data_dict):
aspect_id = data_dict.get('aspect_id')
scale_level = data_dict.get('scale_level')
obj = cls(aspect_id=aspect_id,
scale_level=scale_level)
return obj
def to_dict(self):
return {'aspect_id': self.aspect_id,
'scale_level': self.scale_level}
@base.TackerObjectRegistry.register
class VnfExtCpInfo(base.TackerObject, base.TackerObjectDictCompat,
base.TackerPersistentObject):
@ -1090,6 +1153,8 @@ class ResourceHandle(base.TackerObject,
# TODO(esto-aln):Add vimConnectionId in Type:ResourceHandle
fields = {
'vim_connection_id': fields.StringField(nullable=True,
default=None),
'resource_id': fields.StringField(nullable=False, default=""),
'vim_level_resource_type': fields.StringField(nullable=True,
default=None)
@ -1108,14 +1173,17 @@ class ResourceHandle(base.TackerObject,
@classmethod
def _from_dict(cls, data_dict):
vim_connection_id = data_dict.get('vim_connection_id')
resource_id = data_dict.get('resource_id', "")
vim_level_resource_type = data_dict.get('vim_level_resource_type')
obj = cls(resource_id=resource_id,
obj = cls(vim_connection_id=vim_connection_id,
resource_id=resource_id,
vim_level_resource_type=vim_level_resource_type)
return obj
def to_dict(self):
return {'resource_id': self.resource_id,
return {'vim_connection_id': self.vim_connection_id,
'resource_id': self.resource_id,
'vim_level_resource_type': self.vim_level_resource_type}

@ -77,6 +77,17 @@ rules = [
}
]
),
policy.DocumentedRuleDefault(
name=VNFLCM % 'scale',
check_str=base.RULE_ADMIN_OR_OWNER,
description="Scale a VNF instance.",
operations=[
{
'method': 'POST',
'path': '/vnflcm/v1/vnf_instances/{vnfInstanceId}/scale'
}
]
),
policy.DocumentedRuleDefault(
name=VNFLCM % 'show_lcm_op_occs',
check_str=base.RULE_ADMIN_OR_OWNER,

@ -0,0 +1,14 @@
heat_template_version: 2020-08-07
description: 'Template for test _generate_hot_from_tosca().'
parameters:
nfv:
type: json
resources:
SP1_scale_in:
type: OS::Nova::Server
properties:
cooldown: 60
outputs: {}

@ -81,15 +81,12 @@ class SessionClient(adapter.Adapter):
class BaseTackerTest(base.BaseTestCase):
"""Base test case class for all Tacker API tests."""
# Class specific variables
tacker_config_file = '/etc/tacker/tacker.conf'
@classmethod
def setUpClass(cls):
super(BaseTackerTest, cls).setUpClass()
kwargs = {}
cfg.CONF(args=['--config-file', cls.tacker_config_file],
cfg.CONF(args=['--config-file', '/etc/tacker/tacker.conf'],
project='tacker',
version='%%prog %s' % version.version_info.release_string(),
**kwargs)
@ -97,7 +94,6 @@ class BaseTackerTest(base.BaseTestCase):
cls.client = cls.tackerclient()
cls.http_client = cls.tacker_http_client()
cls.h_client = cls.heatclient()
cls.glance_client = cls.glanceclient()
@classmethod
def get_credentials(cls):

@ -28,10 +28,10 @@ import zipfile
from oslo_config import cfg
from tacker.db.db_sqlalchemy import models
from tacker.objects import scale_vnf_request
from tacker.tests import utils
from tacker.tests import uuidsentinel
VNF_UPLOAD_VNF_PACKAGE_CONTENT = {
'algorithm': 'sha512', 'created_at': '2019-08-16T06:57:09Z',
'deleted': False, 'deleted_at': None,
@ -288,3 +288,16 @@ def _get_vnf(**updates):
vnf_data.update(**updates)
return vnf_data
def scale_request(type, number_of_steps):
scale_request_data = {
'type': type,
'aspect_id': "SP1",
'number_of_steps': number_of_steps,
'scale_level': 1,
'additional_params': {"test": "test_value"},
}
scale_request = scale_vnf_request.ScaleVnfRequest(**scale_request_data)
return scale_request

@ -799,9 +799,29 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase):
password=password)
self.assertEqual('CREATED', self.vnf_package.onboarding_state)
@mock.patch.object(coordination.Coordinator, 'get_lock')
@mock.patch.object(objects.VnfInstance, "get_by_id")
def test_scale(self, mock_vnf_by_id, mock_get_lock):
mock_vnf_by_id.return_value = fakes.return_vnf_instance(
fields.VnfInstanceState.INSTANTIATED)
vnf_info = fakes._get_vnf()
vnf_instance = fakes.return_vnf_instance(
fields.VnfInstanceState.INSTANTIATED,
scale_status="scale_status")
scale_vnf_request = fakes.scale_request("SCALE_IN", 1)
self.conductor.scale(
self.context,
vnf_info,
vnf_instance,
scale_vnf_request)
self.vnflcm_driver.scale_vnf.assert_called_once_with(
self.context, vnf_info, mock.ANY, scale_vnf_request)
@mock.patch.object(objects.LccnSubscriptionRequest,
'vnf_lcm_subscriptions_get')
def test_sendNotification_notFoundSubscription(self,
def test_send_notification_not_found_subscription(self,
mock_subscriptions_get):
mock_subscriptions_get.return_value = None
notification = {
@ -815,7 +835,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase):
@mock.patch.object(objects.LccnSubscriptionRequest,
'vnf_lcm_subscriptions_get')
def test_sendNotification_vnfLcmOperationOccurrence(self,
def test_send_notification_vnf_lcm_operation_occurrence(self,
mock_subscriptions_get):
self.requests_mock.register_uri('POST',
"https://localhost/callback",
@ -843,7 +863,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase):
@mock.patch.object(objects.LccnSubscriptionRequest,
'vnf_lcm_subscriptions_get')
def test_sendNotification_vnfIdentifierCreation(self,
def test_send_notification_vnf_identifier_creation(self,
mock_subscriptions_get):
self.requests_mock.register_uri(
'POST',
@ -870,9 +890,8 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase):
@mock.patch.object(objects.LccnSubscriptionRequest,
'vnf_lcm_subscriptions_get')
def test_sendNotification_with_auth_basic(self, mock_subscriptions_get):
self.requests_mock.register_uri(
'POST',
def test_send_notification_with_auth_basic(self, mock_subscriptions_get):
self.requests_mock.register_uri('POST',
"https://localhost/callback",
headers={
'Content-Type': 'application/json'},
@ -881,7 +900,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase):
auth_user_name = 'test_user'
auth_password = 'test_password'
mock_subscriptions_get.return_value = self._create_subscriptions(
{'authType': ['BASIC'],
{'authType': 'BASIC',
'paramsBasic': {'userName': auth_user_name,
'password': auth_password}})
@ -906,7 +925,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase):
@mock.patch.object(objects.LccnSubscriptionRequest,
'vnf_lcm_subscriptions_get')
def test_sendNotification_with_auth_client_credentials(
def test_send_notification_with_auth_client_credentials(
self, mock_subscriptions_get):
auth.auth_manager = auth._AuthManager()
self.requests_mock.register_uri(
@ -951,10 +970,9 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase):
@mock.patch.object(objects.LccnSubscriptionRequest,
'vnf_lcm_subscriptions_get')
def test_sendNotification_retyNotification(self,
mock_subscriptions_get):
self.requests_mock.register_uri(
'POST',
def test_send_notification_rety_notification(self,
mock_subscriptions_get):
self.requests_mock.register_uri('POST',
"https://localhost/callback",
headers={
'Content-Type': 'application/json'},
@ -1003,7 +1021,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase):
@mock.patch.object(objects.LccnSubscriptionRequest,
'vnf_lcm_subscriptions_get')
def test_sendNotification_internalServerError(
def test_send_notification_internal_server_error(
self, mock_subscriptions_get):
mock_subscriptions_get.side_effect = Exception("MockException")
notification = {
@ -1013,7 +1031,7 @@ class TestConductor(SqlTestCase, unit_base.FixturedTestCase):
result = self.conductor.send_notification(self.context, notification)
self.assertEqual(result, 99)
self.assertEqual(result, -2)
mock_subscriptions_get.assert_called()
@mock.patch.object(conductor_server, 'revert_update_lcm')

@ -27,11 +27,14 @@ from tacker.objects import fields
from tacker.objects.instantiate_vnf_req import ExtManagedVirtualLinkData
from tacker.objects.instantiate_vnf_req import ExtVirtualLinkData
from tacker.objects.instantiate_vnf_req import InstantiateVnfRequest
from tacker.objects import scale_vnf_request
from tacker.objects.vim_connection import VimConnectionInfo
from tacker.tests import constants
from tacker.tests import uuidsentinel
from tacker import wsgi
import tacker.db.vnfm.vnfm_db
import tacker.conf
CONF = tacker.conf.CONF
@ -101,6 +104,19 @@ def return_vnf_package_vnfd():
return model_obj
def scale_request_make(type, number_of_steps):
scale_request_data = {
'type': type,
'aspect_id': "SP1",
'number_of_steps': number_of_steps,
'scale_level': 1,
'additional_params': {"test": "test_value"},
}
scale_request = scale_vnf_request.ScaleVnfRequest(**scale_request_data)
return scale_request
def _model_non_instantiated_vnf_instance(**updates):
vnf_instance = {
'created_at': datetime.datetime(2020, 1, 1, 1, 1, 1,
@ -752,8 +768,11 @@ def _get_vnf(**updates):
'placement_attr': 'fake_placement_attr',
'vim_id': 'uuidsentinel.vim_id',
'error_reason': 'fake_error_reason',
'instance_id': uuidsentinel.instance_id,
'attributes': {
"scale_group": '{"scaleGroupDict" : {"SP1": {"maxLevel" : 3}}}'}}
"scale_group": '{"scaleGroupDict" : {"SP1": {"maxLevel" : 3}}}',
"heat_template": os.path.dirname(__file__) +
"/../../etc/samples/hot_lcm_template.yaml"}}
if updates:
vnf_data.update(**updates)
@ -761,6 +780,20 @@ def _get_vnf(**updates):
return vnf_data
def scale_request(type, number_of_steps, is_reverse):
scale_request_data = {
'type': type,
'aspect_id': "SP1",
'number_of_steps': number_of_steps,
'scale_level': 1,
'additional_params': {"is_reverse": is_reverse},
}
scale_request = \
scale_vnf_request.ScaleVnfRequest(**scale_request_data)
return scale_request
def get_dummy_grant_response():
return {'VDU1': {'checksum': {'algorithm': 'fake algo',
'hash': 'fake hash'},
@ -791,6 +824,14 @@ def return_vnf_resource():
return version_obj
def vnf_scale():
return tacker.db.vnfm.vnfm_db.VNF(id=constants.UUID,
vnfd_id=uuidsentinel.vnfd_id,
name='test',
status='ACTIVE',
vim_id=uuidsentinel.vim_id)
class InjectContext(wsgi.Middleware):
"""Add a 'tacker.context' to WSGI environ."""

@ -2206,10 +2206,10 @@ class TestController(base.TestCase):
@mock.patch.object(objects.VNF, "vnf_index_list")
@mock.patch.object(objects.VnfInstanceList, "vnf_instance_list")
@mock.patch.object(objects.VnfPackageVnfd, 'get_vnf_package_vnfd')
@mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "update_vnf_instance_content")
@mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "update")
def test_update_none_vnf_package_info(
self, input_id,
mock_update_vnf_instance_content,
mock_update,
mock_vnf_package_vnf_get_vnf_package_vnfd,
mock_vnf_instance_list,
mock_vnf_index_list,
@ -2257,10 +2257,10 @@ class TestController(base.TestCase):
@mock.patch.object(objects.VNF, "vnf_index_list")
@mock.patch.object(objects.VnfInstanceList, "vnf_instance_list")
@mock.patch.object(objects.VnfPackageVnfd, 'get_vnf_package_vnfd')
@mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "update_vnf_instance_content")
@mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "update")
def test_update_none_vnf_package_vnfd(
self, input_id,
mock_update_vnf_instance_content,
mock_update,
mock_vnf_package_vnf_get_vnf_package_vnfd,
mock_vnf_instance_list,
mock_vnf_index_list,
@ -2295,3 +2295,302 @@ class TestController(base.TestCase):
resp = req.get_response(self.app)
self.assertEqual(http_client.INTERNAL_SERVER_ERROR, resp.status_code)
@mock.patch.object(TackerManager, 'get_service_plugins',
return_value={'VNFM': FakeVNFMPlugin()})
@mock.patch.object(objects.VnfInstance, "get_by_id")
def test_scale_not_scale_err(
self,
mock_vnf_instance_get_by_id,
mock_get_service_plugins):
mock_vnf_instance_get_by_id.return_value =\
fakes.return_vnf_instance(fields.VnfInstanceState.INSTANTIATED)
body = {
"type": "SCALE_OUT",
"aspectId": "SP1",
"numberOfSteps": 1,
"additionalParams": {
"test": "test_value"}}
req = fake_request.HTTPRequest.blank(
'/vnf_instances/%s/scale' %
constants.UUID)
req.body = jsonutils.dump_as_bytes(body)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
res = self._make_problem_detail(
'NOT SCALE VNF', 409, title='NOT SCALE VNF')
resp = req.get_response(self.app)
self.assertEqual(res.text, resp.text)
@mock.patch.object(TackerManager, 'get_service_plugins',
return_value={'VNFM': FakeVNFMPlugin()})
def test_scale_not_active_err(self,
mock_get_service_plugins):
body = {
"type": "SCALE_OUT",
"aspectId": "SP1",
"numberOfSteps": 1,
"additionalParams": {
"test": "test_value"}}
req = fake_request.HTTPRequest.blank(
'/vnf_instances/%s/scale' %
'91e32c20-6d1f-47a4-9ba7-08f5e5effe07')
req.body = jsonutils.dump_as_bytes(body)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
res = self._make_problem_detail(
'VNF IS NOT ACTIVE', 409, title='VNF IS NOT ACTIVE')
resp = req.get_response(self.app)
self.assertEqual(res.text, resp.text)
@mock.patch.object(TackerManager, 'get_service_plugins',
return_value={'VNFM': FakeVNFMPlugin()})
def test_scale_vnfnotfound_err(self,
mock_get_service_plugins):
msg = _('VNF %(vnf_id)s could not be found')
body = {
"type": "SCALE_OUT",
"aspectId": "SP1",
"numberOfSteps": 1,
"additionalParams": {
"test": "test_value"}}
req = fake_request.HTTPRequest.blank(
'/vnf_instances/%s/scale' %
'7168062e-9fa1-4203-8cb7-f5c99ff3ee1b')
req.body = jsonutils.dump_as_bytes(body)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
res = self._make_problem_detail(msg, 404, title='VNF NOT FOUND')
resp = req.get_response(self.app)
self.assertEqual(res.text, resp.text)
@mock.patch.object(TackerManager, 'get_service_plugins',
return_value={'VNFM': FakeVNFMPlugin()})
@mock.patch.object(objects.VnfLcmOpOcc, "create")
@mock.patch.object(objects.ScaleVnfRequest, "obj_from_primitive")
@mock.patch.object(objects.VnfInstance, "get_by_id")
@mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, "get_vnf")
@mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "scale")
@mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "send_notification")
def test_scale_in(
self,
mock_send_notification,
mock_scale,
mock_get_vnf,
mock_vnf_instance_get_by_id,
mock_obj_from_primitive,
mock_create,
mock_get_service_plugins):
mock_get_vnf.return_value = fakes._get_vnf()
mock_vnf_instance_get_by_id.return_value = fakes.return_vnf_instance(
fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status")
mock_obj_from_primitive.return_value = fakes.scale_request_make(
"SCALE_IN", 1)
mock_create.return_value = 200
body = {
"type": "SCALE_IN",
"aspectId": "SP1",
"numberOfSteps": 1,
"additionalParams": {
"test": "test_value"}}
req = fake_request.HTTPRequest.blank(
'/vnf_instances/%s/scale' %
constants.UUID)
req.body = jsonutils.dump_as_bytes(body)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
resp = req.get_response(self.app)
self.assertEqual(http_client.ACCEPTED, resp.status_code)
mock_scale.assert_called_once()
@mock.patch.object(TackerManager, 'get_service_plugins',
return_value={'VNFM': FakeVNFMPlugin()})
@mock.patch.object(objects.VnfLcmOpOcc, "create")
@mock.patch.object(objects.ScaleVnfRequest, "obj_from_primitive")
@mock.patch.object(objects.VnfInstance, "get_by_id")
@mock.patch.object(tacker.db.vnfm.vnfm_db.VNFMPluginDb, "get_vnf")
@mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "scale")
@mock.patch.object(vnf_lcm_rpc.VNFLcmRPCAPI, "send_notification")
def test_scale_out(
self,
mock_send_notification,
mock_scale,
mock_get_vnf,
mock_vnf_instance_get_by_id,
mock_obj_from_primitive,
mock_create,
mock_get_service_plugins):
mock_get_vnf.return_value = fakes._get_vnf()
mock_vnf_instance_get_by_id.return_value = fakes.return_vnf_instance(
fields.VnfInstanceState.INSTANTIATED, scale_status="scale_status")
mock_obj_from_primitive.return_value = fakes.scale_request_make(
"SCALE_OUT", 1)