Add min_adjustment_step property to ScalingPolicy

Adds `min_adjustment_step` property to ScalingPolicy
resource. It represents the minimum number of resources
that are added or removed when AutoScaling group scales
up or down. This can be used only when specifying value
`percent_change_in_capacity` for `adjustment_type` property.

Change-Id: Ia8976e52047ab47245566487ce1872a8890bcff2
Closes-Bug: #1447913
This commit is contained in:
Rabi Mishra 2015-04-22 15:56:43 +05:30
parent d644ea964d
commit 63646f8cfd
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):