Merge "Add min_adjustment_step property to ScalingPolicy"

This commit is contained in:
Jenkins 2015-05-04 00:26:46 +00:00 committed by Gerrit Code Review
commit 729aa08f4a
9 changed files with 284 additions and 84 deletions

View File

@ -381,6 +381,11 @@ class ResourcePropertyDependency(HeatException):
msg_fmt = _('%(prop1)s cannot be specified without %(prop2)s.')
class ResourcePropertyValueDependency(HeatException):
msg_fmt = _('%(prop1)s property should only be specified '
'for %(prop2)s with value %(value)s.')
class PropertyUnspecifiedError(HeatException):
msg_fmt = _('At least one of the following properties '
'must be specified: %(props)s')

View File

@ -41,12 +41,18 @@ LOG = logging.getLogger(__name__)
def _calculate_new_capacity(current, adjustment, adjustment_type,
minimum, maximum):
min_adjustment_step, minimum, maximum):
"""
Given the current capacity, calculates the new capacity which results
from applying the given adjustment of the given adjustment-type. The
new capacity will be kept within the maximum and minimum bounds.
"""
def _get_minimum_adjustment(adjustment, min_adjustment_step):
if min_adjustment_step and min_adjustment_step > abs(adjustment):
adjustment = (min_adjustment_step if adjustment > 0
else -min_adjustment_step)
return adjustment
if adjustment_type == CHANGE_IN_CAPACITY:
new_capacity = current + adjustment
elif adjustment_type == EXACT_CAPACITY:
@ -60,7 +66,8 @@ def _calculate_new_capacity(current, adjustment, adjustment_type,
else:
rounded = int(math.floor(delta) if delta > 0.0
else math.ceil(delta))
new_capacity = current + rounded
adjustment = _get_minimum_adjustment(rounded, min_adjustment_step)
new_capacity = current + adjustment
if new_capacity > maximum:
LOG.debug('truncating growth to %s' % maximum)
@ -293,7 +300,8 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin):
current_capacity = grouputils.get_size(self)
self.adjust(current_capacity, adjustment_type=EXACT_CAPACITY)
def adjust(self, adjustment, adjustment_type=CHANGE_IN_CAPACITY):
def adjust(self, adjustment, adjustment_type=CHANGE_IN_CAPACITY,
min_adjustment_step=None):
"""
Adjust the size of the scaling group if the cooldown permits.
"""
@ -309,7 +317,9 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin):
upper = self.properties[self.MAX_SIZE]
new_capacity = _calculate_new_capacity(capacity, adjustment,
adjustment_type, lower, upper)
adjustment_type,
min_adjustment_step,
lower, upper)
# send a notification before, on-error and on-success.
notif = {

View File

@ -14,26 +14,22 @@
from oslo_log import log as logging
import six
from heat.common import exception
from heat.common.i18n import _
from heat.common.i18n import _LI
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine.resources import signal_responder
from heat.scaling import cooldown
from heat.engine.resources.openstack.heat import scaling_policy as heat_sp
LOG = logging.getLogger(__name__)
class AWSScalingPolicy(signal_responder.SignalResponder,
cooldown.CooldownMixin):
class AWSScalingPolicy(heat_sp.AutoScalingPolicy):
PROPERTIES = (
AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE,
COOLDOWN,
COOLDOWN, MIN_ADJUSTMENT_STEP,
) = (
'AutoScalingGroupName', 'ScalingAdjustment', 'AdjustmentType',
'Cooldown',
'Cooldown', 'MinAdjustmentStep',
)
EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY = (
@ -73,6 +69,20 @@ class AWSScalingPolicy(signal_responder.SignalResponder,
_('Cooldown period, in seconds.'),
update_allowed=True
),
MIN_ADJUSTMENT_STEP: properties.Schema(
properties.Schema.INTEGER,
_('Minimum number of resources that are added or removed '
'when the AutoScaling group scales up or down. This can '
'be used only when specifying PercentChangeInCapacity '
'for the AdjustmentType property.'),
constraints=[
constraints.Range(
min=0,
),
],
update_allowed=True
),
}
attributes_schema = {
@ -81,76 +91,9 @@ class AWSScalingPolicy(signal_responder.SignalResponder,
),
}
def handle_create(self):
super(AWSScalingPolicy, self).handle_create()
self.resource_id_set(self._get_user_id())
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
"""
If Properties has changed, update self.properties, so we get the new
values during any subsequent adjustment.
"""
if prop_diff:
self.properties = json_snippet.properties(self.properties_schema,
self.context)
def _get_adjustement_type(self):
return self.properties[self.ADJUSTMENT_TYPE]
def handle_signal(self, details=None):
# ceilometer sends details like this:
# {u'alarm_id': ID, u'previous': u'ok', u'current': u'alarm',
# u'reason': u'...'})
# in this policy we currently assume that this gets called
# only when there is an alarm. But the template writer can
# put the policy in all the alarm notifiers (nodata, and ok).
#
# our watchrule has upper case states so lower() them all.
if details is None:
alarm_state = 'alarm'
else:
alarm_state = details.get('current',
details.get('state', 'alarm')).lower()
LOG.info(_LI('%(name)s Alarm, new state %(state)s'),
{'name': self.name, 'state': alarm_state})
if alarm_state != 'alarm':
return
if self._cooldown_inprogress():
LOG.info(_LI("%(name)s NOT performing scaling action, "
"cooldown %(cooldown)s"),
{'name': self.name,
'cooldown': self.properties[self.COOLDOWN]})
return
asgn_id = self.properties[self.AUTO_SCALING_GROUP_NAME]
group = self.stack.resource_by_refid(asgn_id)
if group is None:
raise exception.NotFound(_('Alarm %(alarm)s could not find '
'scaling group named "%(group)s"') % {
'alarm': self.name,
'group': asgn_id})
LOG.info(_LI('%(name)s Alarm, adjusting Group %(group)s with id '
'%(asgn_id)s by %(filter)s'),
{'name': self.name, 'group': group.name, 'asgn_id': asgn_id,
'filter': self.properties[self.SCALING_ADJUSTMENT]})
adjustment_type = self._get_adjustement_type()
group.adjust(self.properties[self.SCALING_ADJUSTMENT], adjustment_type)
self._cooldown_timestamp("%s : %s" %
(self.properties[self.ADJUSTMENT_TYPE],
self.properties[self.SCALING_ADJUSTMENT]))
def _resolve_attribute(self, name):
'''
heat extension: "AlarmUrl" returns the url to post to the policy
when there is an alarm.
'''
if name == self.ALARM_URL and self.resource_id is not None:
return six.text_type(self._get_signed_url())
def FnGetRefId(self):
if self.resource_id is not None:
return six.text_type(self._get_signed_url())

View File

@ -37,10 +37,10 @@ class AutoScalingPolicy(signal_responder.SignalResponder,
"""
PROPERTIES = (
AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE,
COOLDOWN,
COOLDOWN, MIN_ADJUSTMENT_STEP
) = (
'auto_scaling_group_id', 'scaling_adjustment', 'adjustment_type',
'cooldown',
'cooldown', 'min_adjustment_step',
)
EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY = (
@ -81,6 +81,20 @@ class AutoScalingPolicy(signal_responder.SignalResponder,
_('Cooldown period, in seconds.'),
update_allowed=True
),
MIN_ADJUSTMENT_STEP: properties.Schema(
properties.Schema.INTEGER,
_('Minimum number of resources that are added or removed '
'when the AutoScaling group scales up or down. This can '
'be used only when specifying percent_change_in_capacity '
'for the adjustment_type property.'),
constraints=[
constraints.Range(
min=0,
),
],
update_allowed=True
),
}
attributes_schema = {
@ -89,6 +103,20 @@ class AutoScalingPolicy(signal_responder.SignalResponder,
),
}
def validate(self):
"""
Add validation for min_adjustment_step
"""
super(AutoScalingPolicy, self).validate()
adjustment_type = self.properties.get(self.ADJUSTMENT_TYPE)
adjustment_step = self.properties.get(self.MIN_ADJUSTMENT_STEP)
if (adjustment_type != self.PERCENT_CHANGE_IN_CAPACITY
and adjustment_step is not None):
raise exception.ResourcePropertyValueDependency(
prop1=self.MIN_ADJUSTMENT_STEP,
prop2=self.ADJUSTMENT_TYPE,
value=self.PERCENT_CHANGE_IN_CAPACITY)
def handle_create(self):
super(AutoScalingPolicy, self).handle_create()
self.resource_id_set(self._get_user_id())
@ -146,7 +174,8 @@ class AutoScalingPolicy(signal_responder.SignalResponder,
{'name': self.name, 'group': group.name, 'asgn_id': asgn_id,
'filter': self.properties[self.SCALING_ADJUSTMENT]})
adjustment_type = self._get_adjustement_type()
group.adjust(self.properties[self.SCALING_ADJUSTMENT], adjustment_type)
group.adjust(self.properties[self.SCALING_ADJUSTMENT], adjustment_type,
self.properties[self.MIN_ADJUSTMENT_STEP])
self._cooldown_timestamp("%s : %s" %
(self.properties[self.ADJUSTMENT_TYPE],

View File

@ -164,6 +164,66 @@ class TestGroupAdjust(common.HeatTestCase):
resize.assert_called_once_with(3)
cd_stamp.assert_called_once_with('ExactCapacity : 3')
def test_scale_up_min_adjustment(self):
self.patchobject(grouputils, 'get_size', return_value=1)
resize = self.patchobject(self.group, 'resize')
cd_stamp = self.patchobject(self.group, '_cooldown_timestamp')
notify = self.patch('heat.engine.notification.autoscaling.send')
self.patchobject(self.group, '_cooldown_inprogress',
return_value=False)
self.group.adjust(33, adjustment_type='PercentChangeInCapacity',
min_adjustment_step=2)
expected_notifies = [
mock.call(
capacity=1, suffix='start',
adjustment_type='PercentChangeInCapacity',
groupname=u'my-group',
message=u'Start resizing the group my-group',
adjustment=33,
stack=self.group.stack),
mock.call(
capacity=3, suffix='end',
adjustment_type='PercentChangeInCapacity',
groupname=u'my-group',
message=u'End resizing the group my-group',
adjustment=33,
stack=self.group.stack)]
self.assertEqual(expected_notifies, notify.call_args_list)
resize.assert_called_once_with(3)
cd_stamp.assert_called_once_with('PercentChangeInCapacity : 33')
def test_scale_down_min_adjustment(self):
self.patchobject(grouputils, 'get_size', return_value=3)
resize = self.patchobject(self.group, 'resize')
cd_stamp = self.patchobject(self.group, '_cooldown_timestamp')
notify = self.patch('heat.engine.notification.autoscaling.send')
self.patchobject(self.group, '_cooldown_inprogress',
return_value=False)
self.group.adjust(-33, adjustment_type='PercentChangeInCapacity',
min_adjustment_step=2)
expected_notifies = [
mock.call(
capacity=3, suffix='start',
adjustment_type='PercentChangeInCapacity',
groupname=u'my-group',
message=u'Start resizing the group my-group',
adjustment=-33,
stack=self.group.stack),
mock.call(
capacity=1, suffix='end',
adjustment_type='PercentChangeInCapacity',
groupname=u'my-group',
message=u'End resizing the group my-group',
adjustment=-33,
stack=self.group.stack)]
self.assertEqual(expected_notifies, notify.call_args_list)
resize.assert_called_once_with(1)
cd_stamp.assert_called_once_with('PercentChangeInCapacity : -33')
def test_scaling_policy_cooldown_ok(self):
self.patchobject(grouputils, 'get_members', return_value=[])
resize = self.patchobject(self.group, 'resize')

View File

@ -20,9 +20,11 @@ import six
from heat.common import exception
from heat.common import template_format
from heat.engine import resource
from heat.engine import scheduler
from heat.tests.autoscaling import inline_templates
from heat.tests import common
from heat.tests import generic_resource
from heat.tests import utils
@ -33,6 +35,8 @@ as_params = inline_templates.as_params
class TestAutoScalingPolicy(common.HeatTestCase):
def setUp(self):
super(TestAutoScalingPolicy, self).setUp()
resource._register_class('ResourceWithPropsAndAttrs',
generic_resource.ResourceWithPropsAndAttrs)
cfg.CONF.set_default('heat_waitcondition_server_url',
'http://server.test:8000/v1/waitcondition')
@ -43,6 +47,32 @@ class TestAutoScalingPolicy(common.HeatTestCase):
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
return rsrc
def test_validate_scaling_policy_ok(self):
t = template_format.parse(as_template)
t['resources']['my-policy']['properties'][
'scaling_adjustment'] = 33
t['resources']['my-policy']['properties'][
'adjustment_type'] = 'percent_change_in_capacity'
t['resources']['my-policy']['properties'][
'min_adjustment_step'] = 2
stack = utils.parse_stack(t)
self.assertIsNone(stack.validate())
def test_validate_scaling_policy_error(self):
t = template_format.parse(as_template)
t['resources']['my-policy']['properties'][
'scaling_adjustment'] = 1
t['resources']['my-policy']['properties'][
'adjustment_type'] = 'change_in_capacity'
t['resources']['my-policy']['properties'][
'min_adjustment_step'] = 2
stack = utils.parse_stack(t)
ex = self.assertRaises(exception.ResourcePropertyValueDependency,
stack.validate)
self.assertIn('min_adjustment_step property should only '
'be specified for adjustment_type with '
'value percent_change_in_capacity.', six.text_type(ex))
def test_scaling_policy_bad_group(self):
t = template_format.parse(inline_templates.as_heat_template_bad_group)
stack = utils.parse_stack(t)
@ -92,7 +122,7 @@ class TestAutoScalingPolicy(common.HeatTestCase):
return_value=False) as mock_cip:
pol.handle_signal(details=test)
mock_cip.assert_called_once_with()
group.adjust.assert_called_once_with(1, 'ChangeInCapacity')
group.adjust.assert_called_once_with(1, 'ChangeInCapacity', None)
class TestCooldownMixin(common.HeatTestCase):

View File

@ -22,52 +22,86 @@ class TestCapacityChanges(common.HeatTestCase):
# r rounded (+up, -down)
# e EXACT_CAPACITY
# p PERCENT_CHANGE_IN_CAPACITY
# s MIN_ADJUSTMENT_STEP
scenarios = [
('+n', dict(current=2, adjustment=3,
adjustment_type=asc.CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=0, maximum=10, expected=5)),
('-n', dict(current=6, adjustment=-2,
adjustment_type=asc.CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=0, maximum=5, expected=4)),
('+nb', dict(current=2, adjustment=8,
adjustment_type=asc.CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=0, maximum=5, expected=5)),
('-nb', dict(current=2, adjustment=-10,
adjustment_type=asc.CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=1, maximum=5, expected=1)),
('e', dict(current=2, adjustment=4,
adjustment_type=asc.EXACT_CAPACITY,
min_adjustment_step=None,
minimum=0, maximum=5, expected=4)),
('+eb', dict(current=2, adjustment=11,
adjustment_type=asc.EXACT_CAPACITY,
min_adjustment_step=None,
minimum=0, maximum=5, expected=5)),
('-eb', dict(current=4, adjustment=1,
adjustment_type=asc.EXACT_CAPACITY,
min_adjustment_step=None,
minimum=3, maximum=5, expected=3)),
('+p', dict(current=4, adjustment=50,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=1, maximum=10, expected=6)),
('-p', dict(current=4, adjustment=-25,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=1, maximum=10, expected=3)),
('+pb', dict(current=4, adjustment=100,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=1, maximum=6, expected=6)),
('-pb', dict(current=6, adjustment=-50,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=4, maximum=10, expected=4)),
('-p+r', dict(current=2, adjustment=-33,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=0, maximum=10, expected=1)),
('+p+r', dict(current=1, adjustment=33,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=0, maximum=10, expected=2)),
('-p-r', dict(current=2, adjustment=-66,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=0, maximum=10, expected=1)),
('+p-r', dict(current=1, adjustment=225,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=None,
minimum=0, maximum=10, expected=3)),
('+ps', dict(current=1, adjustment=100,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=3,
minimum=0, maximum=10, expected=4)),
('+p+rs', dict(current=1, adjustment=33,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=2,
minimum=0, maximum=10, expected=3)),
('+p-rs', dict(current=1, adjustment=325,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=2,
minimum=0, maximum=10, expected=4)),
('-p-rs', dict(current=3, adjustment=-25,
adjustment_type=asc.PERCENT_CHANGE_IN_CAPACITY,
min_adjustment_step=2,
minimum=0, maximum=10, expected=1)),
]
def test_calc(self):
@ -75,4 +109,5 @@ class TestCapacityChanges(common.HeatTestCase):
asc._calculate_new_capacity(
self.current, self.adjustment,
self.adjustment_type,
self.min_adjustment_step,
self.minimum, self.maximum))

View File

@ -345,6 +345,66 @@ class TestGroupAdjust(common.HeatTestCase):
resize.assert_called_once_with(3)
cd_stamp.assert_called_once_with('ExactCapacity : 3')
def test_scale_up_min_adjustment(self):
self.patchobject(grouputils, 'get_size', return_value=1)
resize = self.patchobject(self.group, 'resize')
cd_stamp = self.patchobject(self.group, '_cooldown_timestamp')
notify = self.patch('heat.engine.notification.autoscaling.send')
self.patchobject(self.group, '_cooldown_inprogress',
return_value=False)
self.group.adjust(33, adjustment_type='PercentChangeInCapacity',
min_adjustment_step=2)
expected_notifies = [
mock.call(
capacity=1, suffix='start',
adjustment_type='PercentChangeInCapacity',
groupname=u'WebServerGroup',
message=u'Start resizing the group WebServerGroup',
adjustment=33,
stack=self.group.stack),
mock.call(
capacity=3, suffix='end',
adjustment_type='PercentChangeInCapacity',
groupname=u'WebServerGroup',
message=u'End resizing the group WebServerGroup',
adjustment=33,
stack=self.group.stack)]
self.assertEqual(expected_notifies, notify.call_args_list)
resize.assert_called_once_with(3)
cd_stamp.assert_called_once_with('PercentChangeInCapacity : 33')
def test_scale_down_min_adjustment(self):
self.patchobject(grouputils, 'get_size', return_value=5)
resize = self.patchobject(self.group, 'resize')
cd_stamp = self.patchobject(self.group, '_cooldown_timestamp')
notify = self.patch('heat.engine.notification.autoscaling.send')
self.patchobject(self.group, '_cooldown_inprogress',
return_value=False)
self.group.adjust(-33, adjustment_type='PercentChangeInCapacity',
min_adjustment_step=2)
expected_notifies = [
mock.call(
capacity=5, suffix='start',
adjustment_type='PercentChangeInCapacity',
groupname=u'WebServerGroup',
message=u'Start resizing the group WebServerGroup',
adjustment=-33,
stack=self.group.stack),
mock.call(
capacity=3, suffix='end',
adjustment_type='PercentChangeInCapacity',
groupname=u'WebServerGroup',
message=u'End resizing the group WebServerGroup',
adjustment=-33,
stack=self.group.stack)]
self.assertEqual(expected_notifies, notify.call_args_list)
resize.assert_called_once_with(3)
cd_stamp.assert_called_once_with('PercentChangeInCapacity : -33')
def test_scaling_policy_cooldown_ok(self):
self.patchobject(grouputils, 'get_members', return_value=[])
resize = self.patchobject(self.group, 'resize')

View File

@ -43,6 +43,34 @@ class TestAutoScalingPolicy(common.HeatTestCase):
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
return rsrc
def test_validate_scaling_policy_ok(self):
t = template_format.parse(inline_templates.as_template)
t['Resources']['WebServerScaleUpPolicy']['Properties'][
'ScalingAdjustment'] = 33
t['Resources']['WebServerScaleUpPolicy']['Properties'][
'AdjustmentType'] = 'PercentChangeInCapacity'
t['Resources']['WebServerScaleUpPolicy']['Properties'][
'MinAdjustmentStep'] = 2
stack = utils.parse_stack(t, params=as_params)
self.policy = stack['WebServerScaleUpPolicy']
self.assertIsNone(self.policy.validate())
def test_validate_scaling_policy_error(self):
t = template_format.parse(inline_templates.as_template)
t['Resources']['WebServerScaleUpPolicy']['Properties'][
'ScalingAdjustment'] = 1
t['Resources']['WebServerScaleUpPolicy']['Properties'][
'AdjustmentType'] = 'ChangeInCapacity'
t['Resources']['WebServerScaleUpPolicy']['Properties'][
'MinAdjustmentStep'] = 2
stack = utils.parse_stack(t, params=as_params)
self.policy = stack['WebServerScaleUpPolicy']
ex = self.assertRaises(exception.ResourcePropertyValueDependency,
self.policy.validate)
self.assertIn('MinAdjustmentStep property should only '
'be specified for AdjustmentType with '
'value PercentChangeInCapacity.', six.text_type(ex))
def test_scaling_policy_bad_group(self):
t = template_format.parse(inline_templates.as_template_bad_group)
stack = utils.parse_stack(t, params=as_params)
@ -93,7 +121,7 @@ class TestAutoScalingPolicy(common.HeatTestCase):
return_value=False) as mock_cip:
pol.handle_signal(details=test)
mock_cip.assert_called_once_with()
group.adjust.assert_called_once_with(1, 'ChangeInCapacity')
group.adjust.assert_called_once_with(1, 'ChangeInCapacity', None)
class TestCooldownMixin(common.HeatTestCase):