13
12
# License for the specific language governing permissions and limitations
14
13
# under the License.
19
19
from heat.engine import environment
20
from heat.engine import function
20
21
from heat.engine import resource
21
22
from heat.engine import signal_responder
23
from heat.common import short_id
24
24
from heat.common import exception
25
25
from heat.common import timeutils as iso8601utils
26
from heat.openstack.common import excutils
26
27
from heat.openstack.common import log as logging
27
28
from heat.openstack.common import timeutils
28
29
from heat.engine.properties import Properties
29
30
from heat.engine import constraints
31
from heat.engine.notification import autoscaling as notification
30
32
from heat.engine import properties
31
33
from heat.engine import scheduler
32
34
from heat.engine import stack_resource
35
from heat.scaling import template
34
37
logger = logging.getLogger(__name__)
37
40
(SCALED_RESOURCE_TYPE,) = ('OS::Heat::ScaledResource',)
43
(EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY) = (
44
'ExactCapacity', 'ChangeInCapacity', 'PercentChangeInCapacity')
40
47
class CooldownMixin(object):
42
49
Utility class to encapsulate Cooldown related logic which is shared
93
_ROLLING_UPDATE_SCHEMA_KEYS = (
94
MIN_INSTANCES_IN_SERVICE, MAX_BATCH_SIZE, PAUSE_TIME
96
'MinInstancesInService', 'MaxBatchSize', 'PauseTime'
99
_UPDATE_POLICY_SCHEMA_KEYS = (ROLLING_UPDATE,) = ('RollingUpdate',)
86
101
properties_schema = {
87
102
AVAILABILITY_ZONES: properties.Schema(
88
103
properties.Schema.LIST,
131
146
"(Heat extension).")
133
148
rolling_update_schema = {
134
'MinInstancesInService': properties.Schema(properties.Schema.NUMBER,
136
'MaxBatchSize': properties.Schema(properties.Schema.NUMBER, default=1),
137
'PauseTime': properties.Schema(properties.Schema.STRING,
149
MIN_INSTANCES_IN_SERVICE: properties.Schema(properties.Schema.NUMBER,
151
MAX_BATCH_SIZE: properties.Schema(properties.Schema.NUMBER,
153
PAUSE_TIME: properties.Schema(properties.Schema.STRING,
140
156
update_policy_schema = {
141
'RollingUpdate': properties.Schema(properties.Schema.MAP,
142
schema=rolling_update_schema)
157
ROLLING_UPDATE: properties.Schema(properties.Schema.MAP,
158
schema=rolling_update_schema)
145
161
def __init__(self, name, json_snippet, stack):
163
180
self.update_policy.validate()
164
181
policy_name = self.update_policy_schema.keys()[0]
165
182
if self.update_policy[policy_name]:
166
pause_time = self.update_policy[policy_name]['PauseTime']
183
pause_time = self.update_policy[policy_name][self.PAUSE_TIME]
167
184
if iso8601utils.parse_isoduration(pause_time) > 3600:
168
185
raise ValueError('Maximum PauseTime is 1 hour.')
222
239
self.update_policy = Properties(
223
240
self.update_policy_schema,
224
241
json_snippet.get('UpdatePolicy', {}),
225
parent_name=self.name)
242
parent_name=self.name,
243
context=self.context)
228
246
self.properties = Properties(self.properties_schema,
229
247
json_snippet.get('Properties', {}),
230
248
self.stack.resolve_runtime_data,
233
252
# Replace instances first if launch configuration has changed
234
if (self.update_policy['RollingUpdate'] and
253
if (self.update_policy[self.ROLLING_UPDATE] and
235
254
self.LAUNCH_CONFIGURATION_NAME in prop_diff):
236
policy = self.update_policy['RollingUpdate']
237
self._replace(int(policy['MinInstancesInService']),
238
int(policy['MaxBatchSize']),
255
policy = self.update_policy[self.ROLLING_UPDATE]
256
self._replace(policy[self.MIN_INSTANCES_IN_SERVICE],
257
policy[self.MAX_BATCH_SIZE],
258
policy[self.PAUSE_TIME])
241
260
# Get the current capacity, we may need to adjust if
242
261
# Size has changed
243
262
if self.SIZE in prop_diff:
244
263
inst_list = self.get_instances()
245
if len(inst_list) != int(self.properties[self.SIZE]):
246
self.resize(int(self.properties[self.SIZE]))
264
if len(inst_list) != self.properties[self.SIZE]:
265
self.resize(self.properties[self.SIZE])
264
283
def _get_instance_definition(self):
265
284
conf_name = self.properties[self.LAUNCH_CONFIGURATION_NAME]
266
285
conf = self.stack.resource_by_refid(conf_name)
267
instance_definition = copy.deepcopy(conf.t)
286
instance_definition = function.resolve(conf.t)
268
287
instance_definition['Type'] = SCALED_RESOURCE_TYPE
269
288
instance_definition['Properties']['Tags'] = self._tags()
270
289
if self.properties.get('VPCZoneIdentifier'):
276
295
def _create_template(self, num_instances, num_replace=0):
278
Create the template for the nested stack of existing and new instances
297
Create a template to represent autoscaled instances.
280
For rolling update, if launch configuration is different, the
281
instance definition should come from the existing instance instead
282
of using the new launch configuration.
299
Also see heat.scaling.template.resource_templates.
284
instances = self.get_instances()[-num_instances:]
285
301
instance_definition = self._get_instance_definition()
286
num_create = num_instances - len(instances)
287
num_replace -= num_create
289
def instance_templates(num_replace):
290
for i in range(num_instances):
291
if i < len(instances):
293
if inst.t != instance_definition and num_replace > 0:
295
yield inst.name, instance_definition
297
yield inst.name, inst.t
299
yield short_id.generate_id(), instance_definition
301
return {"Resources": dict(instance_templates(num_replace))}
302
old_resources = [(instance.name, instance.t)
303
for instance in self.get_instances()]
304
templates = template.resource_templates(
305
old_resources, instance_definition, num_instances, num_replace)
306
return {"Resources": dict(templates)}
303
308
def _replace(self, min_in_service, batch_size, pause_time):
307
312
def changing_instances(tmpl):
308
313
instances = self.get_instances()
309
current = set((i.name, str(i.t)) for i in instances)
310
updated = set((k, str(v)) for k, v in tmpl['Resources'].items())
314
serialize_template = functools.partial(json.dumps, sort_keys=True)
315
current = set((i.name, serialize_template(i.t)) for i in instances)
316
updated = set((k, serialize_template(v))
317
for k, v in tmpl['Resources'].items())
311
318
# includes instances to be updated and deleted
312
319
affected = set(k for k, v in current ^ updated)
313
320
return set(i.FnGetRefId() for i in instances if i.name in affected)
408
415
return u','.join(inst.FnGetAtt('PublicIp')
409
416
for inst in self.get_instances()) or None
418
def child_template(self):
419
num_instances = int(self.properties[self.SIZE])
420
return self._create_template(num_instances)
422
def child_params(self):
423
return self._environment()
412
426
class AutoScalingGroup(InstanceGroup, CooldownMixin):
444
_UPDATE_POLICY_SCHEMA_KEYS = (
447
'AutoScalingRollingUpdate'
450
_ROLLING_UPDATE_SCHEMA_KEYS = (
451
MIN_INSTANCES_IN_SERVICE, MAX_BATCH_SIZE, PAUSE_TIME
453
'MinInstancesInService', 'MaxBatchSize', 'PauseTime'
430
456
properties_schema = {
431
457
AVAILABILITY_ZONES: properties.Schema(
432
458
properties.Schema.LIST,
440
466
update_allowed=True
442
468
MAX_SIZE: properties.Schema(
443
properties.Schema.STRING,
469
properties.Schema.INTEGER,
444
470
_('Maximum number of instances in the group.'),
446
472
update_allowed=True
448
474
MIN_SIZE: properties.Schema(
449
properties.Schema.STRING,
475
properties.Schema.INTEGER,
450
476
_('Minimum number of instances in the group.'),
452
478
update_allowed=True
454
480
COOLDOWN: properties.Schema(
455
properties.Schema.STRING,
481
properties.Schema.NUMBER,
456
482
_('Cooldown period, in seconds.'),
457
483
update_allowed=True
459
485
DESIRED_CAPACITY: properties.Schema(
460
properties.Schema.NUMBER,
486
properties.Schema.INTEGER,
461
487
_('Desired initial number of instances.'),
462
488
update_allowed=True
504
530
rolling_update_schema = {
505
'MinInstancesInService': properties.Schema(properties.Schema.NUMBER,
507
'MaxBatchSize': properties.Schema(properties.Schema.NUMBER, default=1),
508
'PauseTime': properties.Schema(properties.Schema.STRING,
531
MIN_INSTANCES_IN_SERVICE: properties.Schema(properties.Schema.INTEGER,
533
MAX_BATCH_SIZE: properties.Schema(properties.Schema.INTEGER,
535
PAUSE_TIME: properties.Schema(properties.Schema.STRING,
511
539
update_policy_schema = {
512
'AutoScalingRollingUpdate': properties.Schema(properties.Schema.MAP,
514
rolling_update_schema)
540
ROLLING_UPDATE: properties.Schema(
541
properties.Schema.MAP,
542
schema=rolling_update_schema)
517
545
update_allowed_keys = ('Properties', 'UpdatePolicy')
519
547
def handle_create(self):
520
548
if self.properties[self.DESIRED_CAPACITY]:
521
num_to_create = int(self.properties[self.DESIRED_CAPACITY])
549
num_to_create = self.properties[self.DESIRED_CAPACITY]
523
num_to_create = int(self.properties[self.MIN_SIZE])
551
num_to_create = self.properties[self.MIN_SIZE]
524
552
initial_template = self._create_template(num_to_create)
525
553
return self.create_with_template(initial_template,
526
554
self._environment())
530
558
done = super(AutoScalingGroup, self).check_create_complete(task)
532
560
self._cooldown_timestamp(
533
"%s : %s" % ('ExactCapacity', len(self.get_instances())))
561
"%s : %s" % (EXACT_CAPACITY, len(self.get_instances())))
536
564
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
544
572
self.update_policy = Properties(
545
573
self.update_policy_schema,
546
574
json_snippet.get('UpdatePolicy', {}),
547
parent_name=self.name)
575
parent_name=self.name,
576
context=self.context)
550
579
self.properties = Properties(self.properties_schema,
551
580
json_snippet.get('Properties', {}),
552
581
self.stack.resolve_runtime_data,
555
585
# Replace instances first if launch configuration has changed
556
if (self.update_policy['AutoScalingRollingUpdate'] and
557
'LaunchConfigurationName' in prop_diff):
558
policy = self.update_policy['AutoScalingRollingUpdate']
559
self._replace(int(policy['MinInstancesInService']),
560
int(policy['MaxBatchSize']),
586
if (self.update_policy[self.ROLLING_UPDATE] and
587
self.LAUNCH_CONFIGURATION_NAME in prop_diff):
588
policy = self.update_policy[self.ROLLING_UPDATE]
589
self._replace(policy[self.MIN_INSTANCES_IN_SERVICE],
590
policy[self.MAX_BATCH_SIZE],
591
policy[self.PAUSE_TIME])
563
593
# Get the current capacity, we may need to adjust if
564
594
# MinSize or MaxSize has changed
567
597
# Figure out if an adjustment is required
568
598
new_capacity = None
569
599
if self.MIN_SIZE in prop_diff:
570
if capacity < int(self.properties[self.MIN_SIZE]):
571
new_capacity = int(self.properties[self.MIN_SIZE])
600
if capacity < self.properties[self.MIN_SIZE]:
601
new_capacity = self.properties[self.MIN_SIZE]
572
602
if self.MAX_SIZE in prop_diff:
573
if capacity > int(self.properties[self.MAX_SIZE]):
574
new_capacity = int(self.properties[self.MAX_SIZE])
603
if capacity > self.properties[self.MAX_SIZE]:
604
new_capacity = self.properties[self.MAX_SIZE]
575
605
if self.DESIRED_CAPACITY in prop_diff:
576
606
if self.properties[self.DESIRED_CAPACITY]:
577
new_capacity = int(self.properties[self.DESIRED_CAPACITY])
607
new_capacity = self.properties[self.DESIRED_CAPACITY]
579
609
if new_capacity is not None:
580
self.adjust(new_capacity, adjustment_type='ExactCapacity')
610
self.adjust(new_capacity, adjustment_type=EXACT_CAPACITY)
582
def adjust(self, adjustment, adjustment_type='ChangeInCapacity'):
612
def adjust(self, adjustment, adjustment_type=CHANGE_IN_CAPACITY):
584
614
Adjust the size of the scaling group if the cooldown permits.
586
616
if self._cooldown_inprogress():
587
617
logger.info(_("%(name)s NOT performing scaling adjustment, "
588
618
"cooldown %(cooldown)s") % {
590
'cooldown': self.properties[self.COOLDOWN]})
620
'cooldown': self.properties[self.COOLDOWN]})
593
623
capacity = len(self.get_instances())
594
if adjustment_type == 'ChangeInCapacity':
624
if adjustment_type == CHANGE_IN_CAPACITY:
595
625
new_capacity = capacity + adjustment
596
elif adjustment_type == 'ExactCapacity':
626
elif adjustment_type == EXACT_CAPACITY:
597
627
new_capacity = adjustment
599
629
# PercentChangeInCapacity
628
658
logger.debug(_('no change in capacity %d') % capacity)
631
result = self.resize(new_capacity)
661
# send a notification before, on-error and on-success.
664
'adjustment': adjustment,
665
'adjustment_type': adjustment_type,
666
'capacity': capacity,
667
'groupname': self.FnGetRefId(),
668
'message': _("Start resizing the group %(group)s") % {
669
'group': self.FnGetRefId()},
672
notification.send(**notif)
674
self.resize(new_capacity)
675
except Exception as resize_ex:
676
with excutils.save_and_reraise_exception():
678
notif.update({'suffix': 'error',
679
'message': str(resize_ex),
681
notification.send(**notif)
683
logger.exception(_('Failed sending error notification'))
687
'capacity': new_capacity,
688
'message': _("End resizing the group %(group)s") % {
689
'group': notif['groupname']},
691
notification.send(**notif)
633
693
self._cooldown_timestamp("%s : %s" % (adjustment_type, adjustment))
638
696
"""Add Identifing Tags to all servers in the group.
711
# check validity of group size
712
min_size = self.properties[self.MIN_SIZE]
713
max_size = self.properties[self.MAX_SIZE]
715
if max_size < min_size:
716
msg = _("MinSize can not be greater than MaxSize")
717
raise exception.StackValidationFailed(message=msg)
720
msg = _("The size of AutoScalingGroup can not be less than zero")
721
raise exception.StackValidationFailed(message=msg)
723
if self.properties[self.DESIRED_CAPACITY]:
724
desired_capacity = self.properties[self.DESIRED_CAPACITY]
725
if desired_capacity < min_size or desired_capacity > max_size:
726
msg = _("DesiredCapacity must be between MinSize and MaxSize")
727
raise exception.StackValidationFailed(message=msg)
653
729
# TODO(pasquier-s): once Neutron is able to assign subnets to
654
730
# availability zones, it will be possible to specify multiple subnets.
655
731
# For now, only one subnet can be specified. The bug #1096017 tracks
737
813
return unicode(self.physical_resource_name())
816
class AutoScalingResourceGroup(AutoScalingGroup):
817
"""An autoscaling group that can scale arbitrary resources."""
820
RESOURCE, MAX_SIZE, MIN_SIZE, COOLDOWN, DESIRED_CAPACITY
822
'resource', 'max_size', 'min_size', 'cooldown', 'desired_capacity'
825
properties_schema = {
826
RESOURCE: properties.Schema(
827
properties.Schema.MAP,
828
_('Resource definition for the resources in the group, in HOT '
829
'format. The value of this property is the definition of a '
830
'resource just as if it had been declared in the template '
834
MAX_SIZE: properties.Schema(
835
properties.Schema.INTEGER,
836
_('Maximum number of resources in the group.'),
839
constraints=[constraints.Range(min=0)],
841
MIN_SIZE: properties.Schema(
842
properties.Schema.INTEGER,
843
_('Minimum number of resources in the group.'),
846
constraints=[constraints.Range(min=0)]
848
COOLDOWN: properties.Schema(
849
properties.Schema.INTEGER,
850
_('Cooldown period, in seconds.'),
853
DESIRED_CAPACITY: properties.Schema(
854
properties.Schema.INTEGER,
855
_('Desired initial number of resources.'),
860
update_allowed_keys = ('Properties',)
862
def _get_instance_definition(self):
863
resource_definition = self.properties[self.RESOURCE]
864
# resolve references within the context of this stack.
865
return self.stack.resolve_runtime_data(resource_definition)
867
def _lb_reload(self, exclude=None):
868
"""AutoScalingResourceGroup does not maintain load balancer
869
connections, so we just ignore calls to update the LB.
873
def _create_template(self, *args, **kwargs):
874
"""Use a HOT format for the template in the nested stack."""
875
tpl = super(AutoScalingResourceGroup, self)._create_template(
877
tpl['heat_template_version'] = '2013-05-23'
878
tpl['resources'] = tpl.pop('Resources')
740
882
class ScalingPolicy(signal_responder.SignalResponder, CooldownMixin):
742
884
AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE,
792
941
self.properties = Properties(self.properties_schema,
793
942
json_snippet.get('Properties', {}),
794
943
self.stack.resolve_runtime_data,
947
def _get_adjustement_type(self):
948
return self.properties[self.ADJUSTMENT_TYPE]
797
950
def handle_signal(self, details=None):
798
951
# ceilometer sends details like this:
832
985
logger.info(_('%(name)s Alarm, adjusting Group %(group)s with id '
833
986
'%(asgn_id)s by %(filter)s') % {
834
'name': self.name, 'group': group.name, 'asgn_id': asgn_id,
835
'filter': self.properties[self.SCALING_ADJUSTMENT]})
836
group.adjust(int(self.properties[self.SCALING_ADJUSTMENT]),
837
self.properties[self.ADJUSTMENT_TYPE])
987
'name': self.name, 'group': group.name,
989
'filter': self.properties[self.SCALING_ADJUSTMENT]})
990
adjustment_type = self._get_adjustement_type()
991
group.adjust(self.properties[self.SCALING_ADJUSTMENT], adjustment_type)
839
993
self._cooldown_timestamp("%s : %s" %
840
994
(self.properties[self.ADJUSTMENT_TYPE],
855
1009
return unicode(self.name)
1012
class AutoScalingPolicy(ScalingPolicy):
1013
"""A resource to manage scaling of `OS::Heat::AutoScalingGroup`.
1015
**Note** while it may incidentally support
1016
`AWS::AutoScaling::AutoScalingGroup` for now, please don't use it for that
1017
purpose and use `AWS::AutoScaling::ScalingPolicy` instead.
1020
AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE,
1023
'auto_scaling_group_id', 'scaling_adjustment', 'adjustment_type',
1027
EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY = (
1028
'exact_capacity', 'change_in_capacity', 'percent_change_in_capacity')
1030
properties_schema = {
1031
AUTO_SCALING_GROUP_NAME: properties.Schema(
1032
properties.Schema.STRING,
1033
_('AutoScaling group ID to apply policy to.'),
1036
SCALING_ADJUSTMENT: properties.Schema(
1037
properties.Schema.NUMBER,
1038
_('Size of adjustment.'),
1042
ADJUSTMENT_TYPE: properties.Schema(
1043
properties.Schema.STRING,
1044
_('Type of adjustment (absolute or percentage).'),
1047
constraints.AllowedValues([CHANGE_IN_CAPACITY,
1049
PERCENT_CHANGE_IN_CAPACITY]),
1053
COOLDOWN: properties.Schema(
1054
properties.Schema.NUMBER,
1055
_('Cooldown period, in seconds.'),
1060
update_allowed_keys = ('Properties',)
1062
attributes_schema = {
1063
"alarm_url": _("A signed url to handle the alarm.")
1066
def _get_adjustement_type(self):
1067
adjustment_type = self.properties[self.ADJUSTMENT_TYPE]
1068
return ''.join([t.capitalize() for t in adjustment_type.split('_')])
1070
def _resolve_attribute(self, name):
1071
if name == 'alarm_url' and self.resource_id is not None:
1072
return unicode(self._get_signed_url())
1074
def FnGetRefId(self):
1075
return resource.Resource.FnGetRefId(self)
858
1078
def resource_mapping():
860
1080
'AWS::AutoScaling::LaunchConfiguration': LaunchConfiguration,
861
1081
'AWS::AutoScaling::AutoScalingGroup': AutoScalingGroup,
862
1082
'AWS::AutoScaling::ScalingPolicy': ScalingPolicy,
863
1083
'OS::Heat::InstanceGroup': InstanceGroup,
1084
'OS::Heat::AutoScalingGroup': AutoScalingResourceGroup,
1085
'OS::Heat::ScalingPolicy': AutoScalingPolicy,