~ubuntu-branches/ubuntu/trusty/heat/trusty

« back to all changes in this revision

Viewing changes to heat/engine/resources/autoscaling.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short, Chuck Short, Adam Gandelman
  • Date: 2014-03-06 17:18:51 UTC
  • mfrom: (1.1.10)
  • Revision ID: package-import@ubuntu.com-20140306171851-2h4id4c4bsh9xl31
Tags: 2014.1~b3-0ubuntu1
[ Chuck Short ]
* New upstream release.
* debian/patches/adjust-dependencies.patch: Dropped no longer needed.
* debian/control: Add python-troveclient.
* debian/rules: fail to build if testsuite fails.
* debian/patches/use-oslo.sphinx-namespace.patch: Use oslo.sphinx namespace.

[ Adam Gandelman ]
* debian/heat-engine.install: Install /etc/heat/environment.d/*.
  (LP: #1285875).

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
1
 
3
2
#
4
3
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
13
12
#    License for the specific language governing permissions and limitations
14
13
#    under the License.
15
14
 
16
 
import copy
 
15
import functools
 
16
import json
17
17
import math
18
18
 
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
22
23
 
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
33
36
 
34
37
logger = logging.getLogger(__name__)
35
38
 
37
40
(SCALED_RESOURCE_TYPE,) = ('OS::Heat::ScaledResource',)
38
41
 
39
42
 
 
43
(EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY) = (
 
44
    'ExactCapacity', 'ChangeInCapacity', 'PercentChangeInCapacity')
 
45
 
 
46
 
40
47
class CooldownMixin(object):
41
48
    '''
42
49
    Utility class to encapsulate Cooldown related logic which is shared
46
53
        inprogress = False
47
54
        try:
48
55
            # Negative values don't make sense, so they are clamped to zero
49
 
            cooldown = max(0, int(self.properties['Cooldown']))
 
56
            cooldown = max(0, self.properties[self.COOLDOWN])
50
57
        except TypeError:
51
58
            # If not specified, it will be None, same as cooldown == 0
52
59
            cooldown = 0
83
90
        'Key', 'Value',
84
91
    )
85
92
 
 
93
    _ROLLING_UPDATE_SCHEMA_KEYS = (
 
94
        MIN_INSTANCES_IN_SERVICE, MAX_BATCH_SIZE, PAUSE_TIME
 
95
    ) = (
 
96
        'MinInstancesInService', 'MaxBatchSize', 'PauseTime'
 
97
    )
 
98
 
 
99
    _UPDATE_POLICY_SCHEMA_KEYS = (ROLLING_UPDATE,) = ('RollingUpdate',)
 
100
 
86
101
    properties_schema = {
87
102
        AVAILABILITY_ZONES: properties.Schema(
88
103
            properties.Schema.LIST,
96
111
            update_allowed=True
97
112
        ),
98
113
        SIZE: properties.Schema(
99
 
            properties.Schema.NUMBER,
 
114
            properties.Schema.INTEGER,
100
115
            _('Desired number of instances.'),
101
116
            required=True,
102
117
            update_allowed=True
131
146
                          "(Heat extension).")
132
147
    }
133
148
    rolling_update_schema = {
134
 
        'MinInstancesInService': properties.Schema(properties.Schema.NUMBER,
135
 
                                                   default=0),
136
 
        'MaxBatchSize': properties.Schema(properties.Schema.NUMBER, default=1),
137
 
        'PauseTime': properties.Schema(properties.Schema.STRING,
138
 
                                       default='PT0S')
 
149
        MIN_INSTANCES_IN_SERVICE: properties.Schema(properties.Schema.NUMBER,
 
150
                                                    default=0),
 
151
        MAX_BATCH_SIZE: properties.Schema(properties.Schema.NUMBER,
 
152
                                          default=1),
 
153
        PAUSE_TIME: properties.Schema(properties.Schema.STRING,
 
154
                                      default='PT0S')
139
155
    }
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)
143
159
    }
144
160
 
145
161
    def __init__(self, name, json_snippet, stack):
151
167
        super(InstanceGroup, self).__init__(name, json_snippet, stack)
152
168
        self.update_policy = Properties(self.update_policy_schema,
153
169
                                        self.t.get('UpdatePolicy', {}),
154
 
                                        parent_name=self.name)
 
170
                                        parent_name=self.name,
 
171
                                        context=self.context)
155
172
 
156
173
    def validate(self):
157
174
        """
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.')
169
186
 
196
213
 
197
214
    def handle_create(self):
198
215
        """Create a nested stack and add the initial resources to it."""
199
 
        num_instances = int(self.properties[self.SIZE])
 
216
        num_instances = self.properties[self.SIZE]
200
217
        initial_template = self._create_template(num_instances)
201
218
        return self.create_with_template(initial_template, self._environment())
202
219
 
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)
226
244
 
227
245
        if prop_diff:
228
246
            self.properties = Properties(self.properties_schema,
229
247
                                         json_snippet.get('Properties', {}),
230
248
                                         self.stack.resolve_runtime_data,
231
 
                                         self.name)
 
249
                                         self.name,
 
250
                                         self.context)
232
251
 
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']),
239
 
                              policy['PauseTime'])
 
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])
240
259
 
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])
247
266
 
248
267
    def _tags(self):
249
268
        """
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'):
275
294
 
276
295
    def _create_template(self, num_instances, num_replace=0):
277
296
        """
278
 
        Create the template for the nested stack of existing and new instances
 
297
        Create a template to represent autoscaled instances.
279
298
 
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.
283
300
        """
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
288
 
 
289
 
        def instance_templates(num_replace):
290
 
            for i in range(num_instances):
291
 
                if i < len(instances):
292
 
                    inst = instances[i]
293
 
                    if inst.t != instance_definition and num_replace > 0:
294
 
                        num_replace -= 1
295
 
                        yield inst.name, instance_definition
296
 
                    else:
297
 
                        yield inst.name, inst.t
298
 
                else:
299
 
                    yield short_id.generate_id(), instance_definition
300
 
 
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)}
302
307
 
303
308
    def _replace(self, min_in_service, batch_size, pause_time):
304
309
        """
306
311
        """
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)
329
336
            raise ValueError('The current UpdatePolicy will result '
330
337
                             'in stack update timeout.')
331
338
 
332
 
        # effective capacity includes temporary capacity added to accomodate
 
339
        # effective capacity includes temporary capacity added to accommodate
333
340
        # the minimum number of instances in service during update
334
341
        efft_capacity = max(capacity - efft_bat_sz, efft_min_sz) + efft_bat_sz
335
342
 
390
397
                        id_list)
391
398
                else:
392
399
                    raise exception.Error(
393
 
                        "Unsupported resource '%s' in LoadBalancerNames" %
 
400
                        _("Unsupported resource '%s' in LoadBalancerNames") %
394
401
                        (lb,))
395
402
                resolved_snippet = self.stack.resolve_static_data(
396
403
                    lb_resource.json_snippet)
408
415
            return u','.join(inst.FnGetAtt('PublicIp')
409
416
                             for inst in self.get_instances()) or None
410
417
 
 
418
    def child_template(self):
 
419
        num_instances = int(self.properties[self.SIZE])
 
420
        return self._create_template(num_instances)
 
421
 
 
422
    def child_params(self):
 
423
        return self._environment()
 
424
 
411
425
 
412
426
class AutoScalingGroup(InstanceGroup, CooldownMixin):
413
427
 
427
441
        'Key', 'Value',
428
442
    )
429
443
 
 
444
    _UPDATE_POLICY_SCHEMA_KEYS = (
 
445
        ROLLING_UPDATE
 
446
    ) = (
 
447
        'AutoScalingRollingUpdate'
 
448
    )
 
449
 
 
450
    _ROLLING_UPDATE_SCHEMA_KEYS = (
 
451
        MIN_INSTANCES_IN_SERVICE, MAX_BATCH_SIZE, PAUSE_TIME
 
452
    ) = (
 
453
        'MinInstancesInService', 'MaxBatchSize', 'PauseTime'
 
454
    )
 
455
 
430
456
    properties_schema = {
431
457
        AVAILABILITY_ZONES: properties.Schema(
432
458
            properties.Schema.LIST,
440
466
            update_allowed=True
441
467
        ),
442
468
        MAX_SIZE: properties.Schema(
443
 
            properties.Schema.STRING,
 
469
            properties.Schema.INTEGER,
444
470
            _('Maximum number of instances in the group.'),
445
471
            required=True,
446
472
            update_allowed=True
447
473
        ),
448
474
        MIN_SIZE: properties.Schema(
449
 
            properties.Schema.STRING,
 
475
            properties.Schema.INTEGER,
450
476
            _('Minimum number of instances in the group.'),
451
477
            required=True,
452
478
            update_allowed=True
453
479
        ),
454
480
        COOLDOWN: properties.Schema(
455
 
            properties.Schema.STRING,
 
481
            properties.Schema.NUMBER,
456
482
            _('Cooldown period, in seconds.'),
457
483
            update_allowed=True
458
484
        ),
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
463
489
        ),
502
528
    }
503
529
 
504
530
    rolling_update_schema = {
505
 
        'MinInstancesInService': properties.Schema(properties.Schema.NUMBER,
506
 
                                                   default=0),
507
 
        'MaxBatchSize': properties.Schema(properties.Schema.NUMBER, default=1),
508
 
        'PauseTime': properties.Schema(properties.Schema.STRING,
509
 
                                       default='PT0S')
 
531
        MIN_INSTANCES_IN_SERVICE: properties.Schema(properties.Schema.INTEGER,
 
532
                                                    default=0),
 
533
        MAX_BATCH_SIZE: properties.Schema(properties.Schema.INTEGER,
 
534
                                          default=1),
 
535
        PAUSE_TIME: properties.Schema(properties.Schema.STRING,
 
536
                                      default='PT0S')
510
537
    }
 
538
 
511
539
    update_policy_schema = {
512
 
        'AutoScalingRollingUpdate': properties.Schema(properties.Schema.MAP,
513
 
                                                      schema=
514
 
                                                      rolling_update_schema)
 
540
        ROLLING_UPDATE: properties.Schema(
 
541
            properties.Schema.MAP,
 
542
            schema=rolling_update_schema)
515
543
    }
516
544
 
517
545
    update_allowed_keys = ('Properties', 'UpdatePolicy')
518
546
 
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]
522
550
        else:
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)
531
559
        if done:
532
560
            self._cooldown_timestamp(
533
 
                "%s : %s" % ('ExactCapacity', len(self.get_instances())))
 
561
                "%s : %s" % (EXACT_CAPACITY, len(self.get_instances())))
534
562
        return done
535
563
 
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)
548
577
 
549
578
        if prop_diff:
550
579
            self.properties = Properties(self.properties_schema,
551
580
                                         json_snippet.get('Properties', {}),
552
581
                                         self.stack.resolve_runtime_data,
553
 
                                         self.name)
 
582
                                         self.name,
 
583
                                         self.context)
554
584
 
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']),
561
 
                              policy['PauseTime'])
 
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])
562
592
 
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]
578
608
 
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)
581
611
 
582
 
    def adjust(self, adjustment, adjustment_type='ChangeInCapacity'):
 
612
    def adjust(self, adjustment, adjustment_type=CHANGE_IN_CAPACITY):
583
613
        """
584
614
        Adjust the size of the scaling group if the cooldown permits.
585
615
        """
586
616
        if self._cooldown_inprogress():
587
617
            logger.info(_("%(name)s NOT performing scaling adjustment, "
588
618
                        "cooldown %(cooldown)s") % {
589
 
                        'name': self.name,
590
 
                        'cooldown': self.properties[self.COOLDOWN]})
 
619
                            'name': self.name,
 
620
                            'cooldown': self.properties[self.COOLDOWN]})
591
621
            return
592
622
 
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
598
628
        else:
599
629
            # PercentChangeInCapacity
606
636
                              else math.ceil(delta))
607
637
            new_capacity = capacity + rounded
608
638
 
609
 
        upper = int(self.properties[self.MAX_SIZE])
610
 
        lower = int(self.properties[self.MIN_SIZE])
 
639
        upper = self.properties[self.MAX_SIZE]
 
640
        lower = self.properties[self.MIN_SIZE]
611
641
 
612
642
        if new_capacity > upper:
613
643
            if upper > capacity:
628
658
            logger.debug(_('no change in capacity %d') % capacity)
629
659
            return
630
660
 
631
 
        result = self.resize(new_capacity)
 
661
        # send a notification before, on-error and on-success.
 
662
        notif = {
 
663
            'stack': self.stack,
 
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()},
 
670
            'suffix': 'start',
 
671
        }
 
672
        notification.send(**notif)
 
673
        try:
 
674
            self.resize(new_capacity)
 
675
        except Exception as resize_ex:
 
676
            with excutils.save_and_reraise_exception():
 
677
                try:
 
678
                    notif.update({'suffix': 'error',
 
679
                                  'message': str(resize_ex),
 
680
                                  })
 
681
                    notification.send(**notif)
 
682
                except Exception:
 
683
                    logger.exception(_('Failed sending error notification'))
 
684
        else:
 
685
            notif.update({
 
686
                'suffix': 'end',
 
687
                'capacity': new_capacity,
 
688
                'message': _("End resizing the group %(group)s") % {
 
689
                    'group': notif['groupname']},
 
690
            })
 
691
            notification.send(**notif)
632
692
 
633
693
        self._cooldown_timestamp("%s : %s" % (adjustment_type, adjustment))
634
694
 
635
 
        return result
636
 
 
637
695
    def _tags(self):
638
696
        """Add Identifing Tags to all servers in the group.
639
697
 
650
708
        if res:
651
709
            return res
652
710
 
 
711
        # check validity of group size
 
712
        min_size = self.properties[self.MIN_SIZE]
 
713
        max_size = self.properties[self.MAX_SIZE]
 
714
 
 
715
        if max_size < min_size:
 
716
            msg = _("MinSize can not be greater than MaxSize")
 
717
            raise exception.StackValidationFailed(message=msg)
 
718
 
 
719
        if min_size < 0:
 
720
            msg = _("The size of AutoScalingGroup can not be less than zero")
 
721
            raise exception.StackValidationFailed(message=msg)
 
722
 
 
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)
 
728
 
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())
738
814
 
739
815
 
 
816
class AutoScalingResourceGroup(AutoScalingGroup):
 
817
    """An autoscaling group that can scale arbitrary resources."""
 
818
 
 
819
    PROPERTIES = (
 
820
        RESOURCE, MAX_SIZE, MIN_SIZE, COOLDOWN, DESIRED_CAPACITY
 
821
    ) = (
 
822
        'resource', 'max_size', 'min_size', 'cooldown', 'desired_capacity'
 
823
    )
 
824
 
 
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 '
 
831
              'itself.'),
 
832
            required=True
 
833
        ),
 
834
        MAX_SIZE: properties.Schema(
 
835
            properties.Schema.INTEGER,
 
836
            _('Maximum number of resources in the group.'),
 
837
            required=True,
 
838
            update_allowed=True,
 
839
            constraints=[constraints.Range(min=0)],
 
840
        ),
 
841
        MIN_SIZE: properties.Schema(
 
842
            properties.Schema.INTEGER,
 
843
            _('Minimum number of resources in the group.'),
 
844
            required=True,
 
845
            update_allowed=True,
 
846
            constraints=[constraints.Range(min=0)]
 
847
        ),
 
848
        COOLDOWN: properties.Schema(
 
849
            properties.Schema.INTEGER,
 
850
            _('Cooldown period, in seconds.'),
 
851
            update_allowed=True
 
852
        ),
 
853
        DESIRED_CAPACITY: properties.Schema(
 
854
            properties.Schema.INTEGER,
 
855
            _('Desired initial number of resources.'),
 
856
            update_allowed=True
 
857
        ),
 
858
    }
 
859
 
 
860
    update_allowed_keys = ('Properties',)
 
861
 
 
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)
 
866
 
 
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.
 
870
        """
 
871
        pass
 
872
 
 
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(
 
876
            *args, **kwargs)
 
877
        tpl['heat_template_version'] = '2013-05-23'
 
878
        tpl['resources'] = tpl.pop('Resources')
 
879
        return tpl
 
880
 
 
881
 
740
882
class ScalingPolicy(signal_responder.SignalResponder, CooldownMixin):
741
883
    PROPERTIES = (
742
884
        AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE,
746
888
        'Cooldown',
747
889
    )
748
890
 
 
891
    EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY = (
 
892
        'ExactCapacity', 'ChangeInCapacity', 'PercentChangeInCapacity')
 
893
 
749
894
    properties_schema = {
750
895
        AUTO_SCALING_GROUP_NAME: properties.Schema(
751
896
            properties.Schema.STRING,
763
908
            _('Type of adjustment (absolute or percentage).'),
764
909
            required=True,
765
910
            constraints=[
766
 
                constraints.AllowedValues(['ChangeInCapacity',
767
 
                                           'ExactCapacity',
768
 
                                           'PercentChangeInCapacity']),
 
911
                constraints.AllowedValues([CHANGE_IN_CAPACITY,
 
912
                                           EXACT_CAPACITY,
 
913
                                           PERCENT_CHANGE_IN_CAPACITY]),
769
914
            ],
770
915
            update_allowed=True
771
916
        ),
783
928
                      "(Heat extension).")
784
929
    }
785
930
 
 
931
    def handle_create(self):
 
932
        super(ScalingPolicy, self).handle_create()
 
933
        self.resource_id_set(self._get_user_id())
 
934
 
786
935
    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
787
936
        """
788
937
        If Properties has changed, update self.properties, so we get the new
792
941
            self.properties = Properties(self.properties_schema,
793
942
                                         json_snippet.get('Properties', {}),
794
943
                                         self.stack.resolve_runtime_data,
795
 
                                         self.name)
 
944
                                         self.name,
 
945
                                         self.context)
 
946
 
 
947
    def _get_adjustement_type(self):
 
948
        return self.properties[self.ADJUSTMENT_TYPE]
796
949
 
797
950
    def handle_signal(self, details=None):
798
951
        # ceilometer sends details like this:
817
970
        if self._cooldown_inprogress():
818
971
            logger.info(_("%(name)s NOT performing scaling action, "
819
972
                        "cooldown %(cooldown)s") % {
820
 
                        'name': self.name,
821
 
                        'cooldown': self.properties[self.COOLDOWN]})
 
973
                            'name': self.name,
 
974
                            'cooldown': self.properties[self.COOLDOWN]})
822
975
            return
823
976
 
824
977
        asgn_id = self.properties[self.AUTO_SCALING_GROUP_NAME]
831
984
 
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,
 
988
                        'asgn_id': asgn_id,
 
989
                        'filter': self.properties[self.SCALING_ADJUSTMENT]})
 
990
        adjustment_type = self._get_adjustement_type()
 
991
        group.adjust(self.properties[self.SCALING_ADJUSTMENT], adjustment_type)
838
992
 
839
993
        self._cooldown_timestamp("%s : %s" %
840
994
                                 (self.properties[self.ADJUSTMENT_TYPE],
855
1009
            return unicode(self.name)
856
1010
 
857
1011
 
 
1012
class AutoScalingPolicy(ScalingPolicy):
 
1013
    """A resource to manage scaling of `OS::Heat::AutoScalingGroup`.
 
1014
 
 
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.
 
1018
    """
 
1019
    PROPERTIES = (
 
1020
        AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE,
 
1021
        COOLDOWN,
 
1022
    ) = (
 
1023
        'auto_scaling_group_id', 'scaling_adjustment', 'adjustment_type',
 
1024
        'cooldown',
 
1025
    )
 
1026
 
 
1027
    EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY = (
 
1028
        'exact_capacity', 'change_in_capacity', 'percent_change_in_capacity')
 
1029
 
 
1030
    properties_schema = {
 
1031
        AUTO_SCALING_GROUP_NAME: properties.Schema(
 
1032
            properties.Schema.STRING,
 
1033
            _('AutoScaling group ID to apply policy to.'),
 
1034
            required=True
 
1035
        ),
 
1036
        SCALING_ADJUSTMENT: properties.Schema(
 
1037
            properties.Schema.NUMBER,
 
1038
            _('Size of adjustment.'),
 
1039
            required=True,
 
1040
            update_allowed=True
 
1041
        ),
 
1042
        ADJUSTMENT_TYPE: properties.Schema(
 
1043
            properties.Schema.STRING,
 
1044
            _('Type of adjustment (absolute or percentage).'),
 
1045
            required=True,
 
1046
            constraints=[
 
1047
                constraints.AllowedValues([CHANGE_IN_CAPACITY,
 
1048
                                           EXACT_CAPACITY,
 
1049
                                           PERCENT_CHANGE_IN_CAPACITY]),
 
1050
            ],
 
1051
            update_allowed=True
 
1052
        ),
 
1053
        COOLDOWN: properties.Schema(
 
1054
            properties.Schema.NUMBER,
 
1055
            _('Cooldown period, in seconds.'),
 
1056
            update_allowed=True
 
1057
        ),
 
1058
    }
 
1059
 
 
1060
    update_allowed_keys = ('Properties',)
 
1061
 
 
1062
    attributes_schema = {
 
1063
        "alarm_url": _("A signed url to handle the alarm.")
 
1064
    }
 
1065
 
 
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('_')])
 
1069
 
 
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())
 
1073
 
 
1074
    def FnGetRefId(self):
 
1075
        return resource.Resource.FnGetRefId(self)
 
1076
 
 
1077
 
858
1078
def resource_mapping():
859
1079
    return {
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,
864
1086
    }