VNF scaling: Functional test
implements blueprint: #vnf-scaling Change-Id: I29269369faf57766ac51c2d12e10fecc1331e474
This commit is contained in:
parent
cf7f677fc7
commit
f291eda290
|
@ -71,6 +71,10 @@ class VNFCreateWaitFailed(exceptions.TackerException):
|
||||||
message = _('%(reason)s')
|
message = _('%(reason)s')
|
||||||
|
|
||||||
|
|
||||||
|
class VNFScaleWaitFailed(exceptions.TackerException):
|
||||||
|
message = _('%(reason)s')
|
||||||
|
|
||||||
|
|
||||||
class VNFDeleteFailed(exceptions.TackerException):
|
class VNFDeleteFailed(exceptions.TackerException):
|
||||||
message = _('deleting VNF %(vnf_id)s failed')
|
message = _('deleting VNF %(vnf_id)s failed')
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,4 @@ VNF_CIRROS_DELETE_TIMEOUT = 300
|
||||||
VNF_CIRROS_DEAD_TIMEOUT = 250
|
VNF_CIRROS_DEAD_TIMEOUT = 250
|
||||||
ACTIVE_SLEEP_TIME = 3
|
ACTIVE_SLEEP_TIME = 3
|
||||||
DEAD_SLEEP_TIME = 1
|
DEAD_SLEEP_TIME = 1
|
||||||
|
SCALE_WINDOW_SLEEP_TIME = 120
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
|
||||||
|
tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0
|
||||||
|
|
||||||
|
description: sample-tosca-vnfd-scaling
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
template_name: sample-tosca-vnfd-scaling
|
||||||
|
|
||||||
|
topology_template:
|
||||||
|
node_templates:
|
||||||
|
VDU1:
|
||||||
|
type: tosca.nodes.nfv.VDU.Tacker
|
||||||
|
capabilities:
|
||||||
|
nfv_compute:
|
||||||
|
properties:
|
||||||
|
num_cpus: 1
|
||||||
|
mem_size: 512 MB
|
||||||
|
disk_size: 1 GB
|
||||||
|
properties:
|
||||||
|
image: cirros-0.3.4-x86_64-uec
|
||||||
|
mgmt_driver: noop
|
||||||
|
availability_zone: nova
|
||||||
|
|
||||||
|
CP1:
|
||||||
|
type: tosca.nodes.nfv.CP.Tacker
|
||||||
|
properties:
|
||||||
|
management: true
|
||||||
|
anti_spoofing_protection: false
|
||||||
|
requirements:
|
||||||
|
- virtualLink:
|
||||||
|
node: VL1
|
||||||
|
- virtualBinding:
|
||||||
|
node: VDU1
|
||||||
|
|
||||||
|
VL1:
|
||||||
|
type: tosca.nodes.nfv.VL
|
||||||
|
properties:
|
||||||
|
network_name: net_mgmt
|
||||||
|
vendor: Tacker
|
||||||
|
|
||||||
|
policies:
|
||||||
|
- SP1:
|
||||||
|
type: tosca.policy.tacker.Scaling
|
||||||
|
properties:
|
||||||
|
increment: 1
|
||||||
|
cooldown: 60
|
||||||
|
min_instances: 1
|
||||||
|
max_instances: 3
|
||||||
|
default_instances: 2
|
||||||
|
targets: [VDU1]
|
|
@ -0,0 +1,86 @@
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from tacker.tests import constants
|
||||||
|
from tacker.tests.functional import base
|
||||||
|
from tacker.tests.utils import read_file
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class VnfTestToscaScale(base.BaseTackerTest):
|
||||||
|
def test_vnf_tosca_scale(self):
|
||||||
|
data = dict()
|
||||||
|
data['tosca'] = read_file('sample-tosca-scale-all.yaml')
|
||||||
|
vnfd_name = 'test_tosca_vnf_scale_all'
|
||||||
|
toscal = data['tosca']
|
||||||
|
tosca_arg = {'vnfd': {'name': vnfd_name,
|
||||||
|
'attributes': {'vnfd': toscal}}}
|
||||||
|
|
||||||
|
# Create vnfd with tosca template
|
||||||
|
vnfd_instance = self.client.create_vnfd(body=tosca_arg)
|
||||||
|
self.assertIsNotNone(vnfd_instance)
|
||||||
|
|
||||||
|
# Create vnf with vnfd_id
|
||||||
|
vnfd_id = vnfd_instance['vnfd']['id']
|
||||||
|
vnf_name = 'test_tosca_vnf_scale_all'
|
||||||
|
vnf_arg = {'vnf': {'vnfd_id': vnfd_id, 'name': vnf_name}}
|
||||||
|
vnf_instance = self.client.create_vnf(body=vnf_arg)
|
||||||
|
|
||||||
|
self.validate_vnf_instance(vnfd_instance, vnf_instance)
|
||||||
|
|
||||||
|
vnf_id = vnf_instance['vnf']['id']
|
||||||
|
|
||||||
|
# TODO(kanagaraj-manickam) once load-balancer support is enabled,
|
||||||
|
# update this logic to validate the scaling
|
||||||
|
def _wait(count):
|
||||||
|
self.wait_until_vnf_active(
|
||||||
|
vnf_id,
|
||||||
|
constants.VNF_CIRROS_CREATE_TIMEOUT,
|
||||||
|
constants.ACTIVE_SLEEP_TIME)
|
||||||
|
vnf = self.client.show_vnf(vnf_id)['vnf']
|
||||||
|
|
||||||
|
# {"VDU1": ["10.0.0.14", "10.0.0.5"]}
|
||||||
|
self.assertEqual(count, len(json.loads(vnf['mgmt_url'])['VDU1']))
|
||||||
|
|
||||||
|
_wait(2)
|
||||||
|
|
||||||
|
def _scale(type, count):
|
||||||
|
body = {"scale": {'type': type, 'policy': 'SP1'}}
|
||||||
|
self.client.scale_vnf(vnf_id, body)
|
||||||
|
_wait(count)
|
||||||
|
|
||||||
|
# scale out
|
||||||
|
time.sleep(constants.SCALE_WINDOW_SLEEP_TIME)
|
||||||
|
_scale('out', 3)
|
||||||
|
|
||||||
|
# scale in
|
||||||
|
time.sleep(constants.SCALE_WINDOW_SLEEP_TIME)
|
||||||
|
_scale('in', 2)
|
||||||
|
|
||||||
|
# Delete vnf_instance with vnf_id
|
||||||
|
try:
|
||||||
|
self.client.delete_vnf(vnf_id)
|
||||||
|
except Exception:
|
||||||
|
assert False, "vnf Delete failed"
|
||||||
|
|
||||||
|
# Delete vnfd_instance
|
||||||
|
self.addCleanup(self.client.delete_vnfd, vnfd_id)
|
||||||
|
self.addCleanup(self.wait_until_vnf_delete, vnf_id,
|
||||||
|
constants.VNF_CIRROS_DELETE_TIMEOUT)
|
|
@ -469,7 +469,7 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin):
|
||||||
policy=policy['id']
|
policy=policy['id']
|
||||||
)
|
)
|
||||||
|
|
||||||
LOG.debug(_("Policy %s is validated successfully") % policy)
|
LOG.debug(_("Policy %s is validated successfully"), policy['id'])
|
||||||
|
|
||||||
def _get_status():
|
def _get_status():
|
||||||
if policy['action'] == constants.ACTION_SCALE_IN:
|
if policy['action'] == constants.ACTION_SCALE_IN:
|
||||||
|
@ -487,7 +487,7 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin):
|
||||||
[constants.ACTIVE],
|
[constants.ACTIVE],
|
||||||
status)
|
status)
|
||||||
LOG.debug(_("Policy %(policy)s vnf is at %(status)s"),
|
LOG.debug(_("Policy %(policy)s vnf is at %(status)s"),
|
||||||
{'policy': policy,
|
{'policy': policy['id'],
|
||||||
'status': status})
|
'status': status})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -500,14 +500,14 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin):
|
||||||
new_status,
|
new_status,
|
||||||
mgmt_url)
|
mgmt_url)
|
||||||
LOG.debug(_("Policy %(policy)s vnf is at %(status)s"),
|
LOG.debug(_("Policy %(policy)s vnf is at %(status)s"),
|
||||||
{'policy': policy,
|
{'policy': policy['id'],
|
||||||
'status': new_status})
|
'status': new_status})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# action
|
# action
|
||||||
def _vnf_policy_action():
|
def _vnf_policy_action():
|
||||||
try:
|
try:
|
||||||
self._vnf_manager.invoke(
|
last_event_id = self._vnf_manager.invoke(
|
||||||
infra_driver,
|
infra_driver,
|
||||||
'scale',
|
'scale',
|
||||||
plugin=self,
|
plugin=self,
|
||||||
|
@ -516,24 +516,25 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin):
|
||||||
policy=policy,
|
policy=policy,
|
||||||
region_name=region_name
|
region_name=region_name
|
||||||
)
|
)
|
||||||
LOG.debug(_("Policy %s action is started successfully") %
|
LOG.debug(_("Policy %s action is started successfully"),
|
||||||
policy)
|
policy['id'])
|
||||||
|
return last_event_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(_("Policy %s action is failed to start") %
|
LOG.error(_("Policy %s action is failed to start"),
|
||||||
policy)
|
policy)
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
vnf['status'] = constants.ERROR
|
vnf['status'] = constants.ERROR
|
||||||
self.set_vnf_error_status_reason(
|
self.set_vnf_error_status_reason(
|
||||||
context,
|
context,
|
||||||
policy['vnf_id'],
|
policy['vnf']['id'],
|
||||||
six.text_type(e))
|
six.text_type(e))
|
||||||
_handle_vnf_scaling_post(constants.ERROR)
|
_handle_vnf_scaling_post(constants.ERROR)
|
||||||
|
|
||||||
# wait
|
# wait
|
||||||
def _vnf_policy_action_wait():
|
def _vnf_policy_action_wait():
|
||||||
try:
|
try:
|
||||||
LOG.debug(_("Policy %s action is in progress") %
|
LOG.debug(_("Policy %s action is in progress"),
|
||||||
policy)
|
policy['id'])
|
||||||
mgmt_url = self._vnf_manager.invoke(
|
mgmt_url = self._vnf_manager.invoke(
|
||||||
infra_driver,
|
infra_driver,
|
||||||
'scale_wait',
|
'scale_wait',
|
||||||
|
@ -541,19 +542,20 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin):
|
||||||
context=context,
|
context=context,
|
||||||
auth_attr=vim_auth,
|
auth_attr=vim_auth,
|
||||||
policy=policy,
|
policy=policy,
|
||||||
region_name=region_name
|
region_name=region_name,
|
||||||
|
last_event_id=last_event_id
|
||||||
)
|
)
|
||||||
LOG.debug(_("Policy %s action is completed successfully") %
|
LOG.debug(_("Policy %s action is completed successfully"),
|
||||||
policy)
|
policy['id'])
|
||||||
_handle_vnf_scaling_post(constants.ACTIVE, mgmt_url)
|
_handle_vnf_scaling_post(constants.ACTIVE, mgmt_url)
|
||||||
# TODO(kanagaraj-manickam): Add support for config and mgmt
|
# TODO(kanagaraj-manickam): Add support for config and mgmt
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(_("Policy %s action is failed to complete") %
|
LOG.error(_("Policy %s action is failed to complete") %
|
||||||
policy)
|
policy['id'])
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
self.set_vnf_error_status_reason(
|
self.set_vnf_error_status_reason(
|
||||||
context,
|
context,
|
||||||
policy['vnf_id'],
|
policy['vnf']['id'],
|
||||||
six.text_type(e))
|
six.text_type(e))
|
||||||
_handle_vnf_scaling_post(constants.ERROR)
|
_handle_vnf_scaling_post(constants.ERROR)
|
||||||
|
|
||||||
|
@ -565,7 +567,7 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin):
|
||||||
infra_driver = self._infra_driver_name(vnf)
|
infra_driver = self._infra_driver_name(vnf)
|
||||||
vim_auth = self.get_vim(context, vnf)
|
vim_auth = self.get_vim(context, vnf)
|
||||||
region_name = vnf.get('placement_attr', {}).get('region_name', None)
|
region_name = vnf.get('placement_attr', {}).get('region_name', None)
|
||||||
_vnf_policy_action()
|
last_event_id = _vnf_policy_action()
|
||||||
self.spawn_n(_vnf_policy_action_wait)
|
self.spawn_n(_vnf_policy_action_wait)
|
||||||
|
|
||||||
return policy
|
return policy
|
||||||
|
|
|
@ -777,12 +777,12 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver,
|
||||||
|
|
||||||
mgmt_ips = {}
|
mgmt_ips = {}
|
||||||
for group_name in group_names:
|
for group_name in group_names:
|
||||||
|
# Get scale group
|
||||||
grp = heat_client.resource_get(instance_id,
|
grp = heat_client.resource_get(instance_id,
|
||||||
group_name)
|
group_name)
|
||||||
# Get scale group
|
|
||||||
for rsc in heat_client.resource_get_list(
|
for rsc in heat_client.resource_get_list(
|
||||||
grp.physical_resource_id):
|
grp.physical_resource_id):
|
||||||
# Get list of resoruces in scale group
|
# Get list of resources in scale group
|
||||||
scale_rsc = heat_client.resource_get(
|
scale_rsc = heat_client.resource_get(
|
||||||
grp.physical_resource_id,
|
grp.physical_resource_id,
|
||||||
rsc.resource_name)
|
rsc.resource_name)
|
||||||
|
@ -804,11 +804,21 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver,
|
||||||
policy,
|
policy,
|
||||||
region_name):
|
region_name):
|
||||||
heatclient_ = HeatClient(auth_attr, region_name)
|
heatclient_ = HeatClient(auth_attr, region_name)
|
||||||
return heatclient_.resource_signal(policy['instance_id'],
|
policy_rsc = get_scaling_policy_name(
|
||||||
get_scaling_policy_name(
|
|
||||||
policy_name=policy['id'],
|
policy_name=policy['id'],
|
||||||
action=policy['action']
|
action=policy['action']
|
||||||
))
|
)
|
||||||
|
events = heatclient_.resource_event_list(
|
||||||
|
policy['instance_id'],
|
||||||
|
policy_rsc,
|
||||||
|
limit=1,
|
||||||
|
sort_dir='desc',
|
||||||
|
sort_keys='event_time'
|
||||||
|
)
|
||||||
|
|
||||||
|
heatclient_.resource_signal(policy['instance_id'],
|
||||||
|
policy_rsc)
|
||||||
|
return events[0].id
|
||||||
|
|
||||||
@log.log
|
@log.log
|
||||||
def scale_wait(self,
|
def scale_wait(self,
|
||||||
|
@ -816,29 +826,59 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver,
|
||||||
plugin,
|
plugin,
|
||||||
auth_attr,
|
auth_attr,
|
||||||
policy,
|
policy,
|
||||||
region_name):
|
region_name,
|
||||||
|
last_event_id):
|
||||||
heatclient_ = HeatClient(auth_attr, region_name)
|
heatclient_ = HeatClient(auth_attr, region_name)
|
||||||
|
|
||||||
# TODO(kanagaraj-manickam) make wait logic into separate utility method
|
# TODO(kanagaraj-manickam) make wait logic into separate utility method
|
||||||
# and make use of it here and other actions like create and delete
|
# and make use of it here and other actions like create and delete
|
||||||
|
stack_retries = STACK_RETRIES
|
||||||
while (True):
|
while (True):
|
||||||
time.sleep(STACK_RETRY_WAIT)
|
|
||||||
try:
|
try:
|
||||||
rsc = heatclient_.resource_get(
|
time.sleep(STACK_RETRY_WAIT)
|
||||||
policy['instance_id'],
|
stack_id = policy['instance_id']
|
||||||
get_scaling_policy_name(policy_name=policy['id'],
|
policy_name = get_scaling_policy_name(
|
||||||
action=policy['action']))
|
policy_name=policy['id'],
|
||||||
except Exception:
|
action=policy['action'])
|
||||||
LOG.exception(_("VNF scaling may not have "
|
events = heatclient_.resource_event_list(
|
||||||
"happened because Heat API request failed "
|
stack_id,
|
||||||
"while waiting for the stack %(stack)s to be "
|
policy_name,
|
||||||
"scaled"), {'stack': policy['instance_id']})
|
limit=1,
|
||||||
break
|
sort_dir='desc',
|
||||||
|
sort_keys='event_time')
|
||||||
|
|
||||||
if rsc.resource_status == 'SIGNAL_IN_PROGRESS':
|
if events[0].id != last_event_id:
|
||||||
continue
|
if events[0].resource_status == 'SIGNAL_COMPLETE':
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
error_reason = _("VNF scaling failed for stack %(stack)s with "
|
||||||
|
"error %(error)s") % {
|
||||||
|
'stack': policy['instance_id'],
|
||||||
|
'error': e.message
|
||||||
|
}
|
||||||
|
LOG.warning(error_reason)
|
||||||
|
raise vnfm.VNFScaleWaitFailed(
|
||||||
|
vnf_id=policy['vnf']['id'],
|
||||||
|
reason=error_reason)
|
||||||
|
|
||||||
break
|
if stack_retries == 0:
|
||||||
|
metadata = heatclient_.resource_metadata(stack_id, policy_name)
|
||||||
|
if not metadata['scaling_in_progress']:
|
||||||
|
error_reason = _('when signal occurred within cool down '
|
||||||
|
'window, no events generated from heat, '
|
||||||
|
'so ignore it')
|
||||||
|
LOG.warning(error_reason)
|
||||||
|
break
|
||||||
|
error_reason = _(
|
||||||
|
"VNF scaling failed to complete within %{wait}s seconds "
|
||||||
|
"while waiting for the stack %(stack)s to be "
|
||||||
|
"scaled.") % {'stack': stack_id,
|
||||||
|
'wait': STACK_RETRIES * STACK_RETRY_WAIT}
|
||||||
|
LOG.warning(error_reason)
|
||||||
|
raise vnfm.VNFScaleWaitFailed(
|
||||||
|
vnf_id=policy['vnf']['id'],
|
||||||
|
reason=error_reason)
|
||||||
|
stack_retries -= 1
|
||||||
|
|
||||||
def _fill_scaling_group_name():
|
def _fill_scaling_group_name():
|
||||||
vnf = policy['vnf']
|
vnf = policy['vnf']
|
||||||
|
@ -916,3 +956,9 @@ class HeatClient(object):
|
||||||
|
|
||||||
def resource_get(self, stack_id, rsc_name):
|
def resource_get(self, stack_id, rsc_name):
|
||||||
return self.heat.resources.get(stack_id, rsc_name)
|
return self.heat.resources.get(stack_id, rsc_name)
|
||||||
|
|
||||||
|
def resource_event_list(self, stack_id, rsc_name, **kwargs):
|
||||||
|
return self.heat.events.list(stack_id, rsc_name, **kwargs)
|
||||||
|
|
||||||
|
def resource_metadata(self, stack_id, rsc_name):
|
||||||
|
return self.heat.resources.metadata(stack_id, rsc_name)
|
||||||
|
|
Loading…
Reference in New Issue