Пример #1
0
class AutoScalingGroup(InstanceGroup, CooldownMixin):
    tags_schema = {'Key': {'Type': 'String',
                           'Required': True},
                   'Value': {'Type': 'String',
                             'Required': True}}
    properties_schema = {
        'AvailabilityZones': {
            'Required': True,
            'Type': 'List',
            'Description': _('Not Implemented.')},
        'LaunchConfigurationName': {
            'Required': True,
            'Type': 'String',
            'Description': _('Name of LaunchConfiguration resource.')},
        'MaxSize': {
            'Required': True,
            'Type': 'String',
            'Description': _('Maximum number of instances in the group.')},
        'MinSize': {
            'Required': True,
            'Type': 'String',
            'Description': _('Minimum number of instances in the group.')},
        'Cooldown': {
            'Type': 'String',
            'Description': _('Cooldown period, in seconds.')},
        'DesiredCapacity': {
            'Type': 'Number',
            'Description': _('Desired initial number of instances.')},
        'HealthCheckGracePeriod': {
            'Type': 'Integer',
            'Implemented': False,
            'Description': _('Not Implemented.')},
        'HealthCheckType': {
            'Type': 'String',
            'AllowedValues': ['EC2', 'ELB'],
            'Implemented': False,
            'Description': _('Not Implemented.')},
        'LoadBalancerNames': {
            'Type': 'List',
            'Description': _('List of LoadBalancer resources.')},
        'VPCZoneIdentifier': {
            'Type': 'List',
            'Description': _('List of VPC subnet identifiers.')},
        'Tags': {
            'Type': 'List',
            'Schema': {'Type': 'Map', 'Schema': tags_schema},
            'Description': _('Tags to attach to this group.')}
    }
    rolling_update_schema = {
        'MinInstancesInService': properties.Schema(properties.NUMBER,
                                                   default=0),
        'MaxBatchSize': properties.Schema(properties.NUMBER,
                                          default=1),
        'PauseTime': properties.Schema(properties.STRING,
                                       default='PT0S')
    }
    update_policy_schema = {
        'AutoScalingRollingUpdate': properties.Schema(
            properties.MAP, schema=rolling_update_schema)
    }

    # template keys and properties supported for handle_update,
    # note trailing comma is required for a single item to get a tuple
    update_allowed_keys = ('Properties', 'UpdatePolicy',)
    update_allowed_properties = ('LaunchConfigurationName',
                                 'MaxSize', 'MinSize',
                                 'Cooldown', 'DesiredCapacity',)

    def handle_create(self):
        if self.properties['DesiredCapacity']:
            num_to_create = int(self.properties['DesiredCapacity'])
        else:
            num_to_create = int(self.properties['MinSize'])
        initial_template = self._create_template(num_to_create)
        return self.create_with_template(initial_template, {})

    def check_create_complete(self, task):
        """Invoke the cooldown after creation succeeds."""
        done = super(AutoScalingGroup, self).check_create_complete(task)
        if done:
            self._cooldown_timestamp(
                "%s : %s" % ('ExactCapacity', len(self.get_instances())))
        return done

    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 tmpl_diff:
            # parse update policy
            if 'UpdatePolicy' in tmpl_diff:
                self.update_policy = Properties(
                    self.update_policy_schema,
                    json_snippet.get('UpdatePolicy', {}),
                    parent_name=self.name)

        if prop_diff:
            self.properties = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         self.stack.resolve_runtime_data,
                                         self.name)

            # Get the current capacity, we may need to adjust if
            # MinSize or MaxSize has changed
            capacity = len(self.get_instances())

            # Figure out if an adjustment is required
            new_capacity = None
            if 'MinSize' in prop_diff:
                if capacity < int(self.properties['MinSize']):
                    new_capacity = int(self.properties['MinSize'])
            if 'MaxSize' in prop_diff:
                if capacity > int(self.properties['MaxSize']):
                    new_capacity = int(self.properties['MaxSize'])
            if 'DesiredCapacity' in prop_diff:
                if self.properties['DesiredCapacity']:
                    new_capacity = int(self.properties['DesiredCapacity'])

            if new_capacity is not None:
                self.adjust(new_capacity, adjustment_type='ExactCapacity')

    def adjust(self, adjustment, adjustment_type='ChangeInCapacity'):
        """
        Adjust the size of the scaling group if the cooldown permits.
        """
        if self._cooldown_inprogress():
            logger.info("%s NOT performing scaling adjustment, cooldown %s" %
                        (self.name, self.properties['Cooldown']))
            return

        capacity = len(self.get_instances())
        if adjustment_type == 'ChangeInCapacity':
            new_capacity = capacity + adjustment
        elif adjustment_type == 'ExactCapacity':
            new_capacity = adjustment
        else:
            # PercentChangeInCapacity
            new_capacity = capacity + (capacity * adjustment / 100)

        if new_capacity > int(self.properties['MaxSize']):
            logger.warn('can not exceed %s' % self.properties['MaxSize'])
            return
        if new_capacity < int(self.properties['MinSize']):
            logger.warn('can not be less than %s' % self.properties['MinSize'])
            return

        if new_capacity == capacity:
            logger.debug('no change in capacity %d' % capacity)
            return

        result = self.resize(new_capacity)

        self._cooldown_timestamp("%s : %s" % (adjustment_type, adjustment))

        return result

    def _tags(self):
        """Add Identifing Tags to all servers in the group.

        This is so the Dimensions received from cfn-push-stats all include
        the groupname and stack id.
        Note: the group name must match what is returned from FnGetRefId
        """
        autoscaling_tag = [{'Key': 'AutoScalingGroupName',
                            'Value': self.FnGetRefId()}]
        return super(AutoScalingGroup, self)._tags() + autoscaling_tag

    def validate(self):
        res = super(AutoScalingGroup, self).validate()
        if res:
            return res

        # TODO(pasquier-s): once Neutron is able to assign subnets to
        # availability zones, it will be possible to specify multiple subnets.
        # For now, only one subnet can be specified. The bug #1096017 tracks
        # this issue.
        if self.properties.get('VPCZoneIdentifier') and \
                len(self.properties['VPCZoneIdentifier']) != 1:
            raise exception.NotSupported(feature=_("Anything other than one "
                                         "VPCZoneIdentifier"))
Пример #2
0
class InstanceGroup(stack_resource.StackResource):
    tags_schema = {'Key': {'Type': 'String',
                           'Required': True},
                   'Value': {'Type': 'String',
                             'Required': True}}
    properties_schema = {
        'AvailabilityZones': {
            'Required': True,
            'Type': 'List',
            'Description': _('Not Implemented.')},
        'LaunchConfigurationName': {
            'Required': True,
            'Type': 'String',
            'Description': _('Name of LaunchConfiguration resource.')},
        'Size': {
            'Required': True,
            'Type': 'Number',
            'Description': _('Desired number of instances.')},
        'LoadBalancerNames': {
            'Type': 'List',
            'Description': _('List of LoadBalancer resources.')},
        'Tags': {
            'Type': 'List',
            'Schema': {'Type': 'Map', 'Schema': tags_schema},
            'Description': _('Tags to attach to this group.')}
    }
    update_allowed_keys = ('Properties', 'UpdatePolicy',)
    update_allowed_properties = ('Size', 'LaunchConfigurationName',)
    attributes_schema = {
        "InstanceList": _("A comma-delimited list of server ip addresses. "
                          "(Heat extension).")
    }
    rolling_update_schema = {
        'MinInstancesInService': properties.Schema(properties.NUMBER,
                                                   default=0),
        'MaxBatchSize': properties.Schema(properties.NUMBER,
                                          default=1),
        'PauseTime': properties.Schema(properties.STRING,
                                       default='PT0S')
    }
    update_policy_schema = {
        'RollingUpdate': properties.Schema(properties.MAP,
                                           schema=rolling_update_schema)
    }

    def __init__(self, name, json_snippet, stack):
        """
        UpdatePolicy is currently only specific to InstanceGroup and
        AutoScalingGroup. Therefore, init is overridden to parse for the
        UpdatePolicy.
        """
        super(InstanceGroup, self).__init__(name, json_snippet, stack)
        self.update_policy = Properties(self.update_policy_schema,
                                        self.t.get('UpdatePolicy', {}),
                                        parent_name=self.name)

    def validate(self):
        """
        Add validation for update_policy
        """
        super(InstanceGroup, self).validate()
        if self.update_policy:
            self.update_policy.validate()

    def get_instance_names(self):
        """Get a list of resource names of the instances in this InstanceGroup.

        Failed resources will be ignored.
        """
        return sorted(x.name for x in self.get_instances())

    def get_instances(self):
        """Get a set of all the instance resources managed by this group."""
        return [resource for resource in self.nested().itervalues()
                if resource.state[1] != resource.FAILED]

    def handle_create(self):
        """Create a nested stack and add the initial resources to it."""
        num_instances = int(self.properties['Size'])
        initial_template = self._create_template(num_instances)
        return self.create_with_template(initial_template, {})

    def check_create_complete(self, task):
        """
        When stack creation is done, update the load balancer.

        If any instances failed to be created, delete them.
        """
        done = super(InstanceGroup, self).check_create_complete(task)
        if done:
            self._lb_reload()
        return done

    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 tmpl_diff:
            # parse update policy
            if 'UpdatePolicy' in tmpl_diff:
                self.update_policy = Properties(
                    self.update_policy_schema,
                    json_snippet.get('UpdatePolicy', {}),
                    parent_name=self.name)

        if prop_diff:
            self.properties = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         self.stack.resolve_runtime_data,
                                         self.name)

            # Get the current capacity, we may need to adjust if
            # Size has changed
            if 'Size' in prop_diff:
                inst_list = self.get_instances()
                if len(inst_list) != int(self.properties['Size']):
                    self.resize(int(self.properties['Size']))

    def _tags(self):
        """
        Make sure that we add a tag that Ceilometer can pick up.
        These need to be prepended with 'metering.'.
        """
        tags = self.properties.get('Tags') or []
        for t in tags:
            if t['Key'].startswith('metering.'):
                # the user has added one, don't add another.
                return tags
        return tags + [{'Key': 'metering.groupname',
                        'Value': self.FnGetRefId()}]

    def handle_delete(self):
        return self.delete_nested()

    def _create_template(self, num_instances):
        """
        Create a template with a number of instance definitions based on the
        launch configuration.
        """
        conf_name = self.properties['LaunchConfigurationName']
        conf = self.stack.resource_by_refid(conf_name)
        instance_definition = copy.deepcopy(conf.t)
        instance_definition['Type'] = 'AWS::EC2::Instance'
        instance_definition['Properties']['Tags'] = self._tags()
        if self.properties.get('VPCZoneIdentifier'):
            instance_definition['Properties']['SubnetId'] = \
                self.properties['VPCZoneIdentifier'][0]
        # resolve references within the context of this stack.
        fully_parsed = self.stack.resolve_runtime_data(instance_definition)

        resources = {}
        for i in range(num_instances):
            resources["%s-%d" % (self.name, i)] = fully_parsed
        return {"Resources": resources}

    def resize(self, new_capacity):
        """
        Resize the instance group to the new capacity.

        When shrinking, the newest instances will be removed.
        """
        new_template = self._create_template(new_capacity)
        try:
            updater = self.update_with_template(new_template, {})
            updater.run_to_completion()
            self.check_update_complete(updater)
        finally:
            # Reload the LB in any case, so it's only pointing at healthy
            # nodes.
            self._lb_reload()

    def _lb_reload(self):
        '''
        Notify the LoadBalancer to reload its config to include
        the changes in instances we have just made.

        This must be done after activation (instance in ACTIVE state),
        otherwise the instances' IP addresses may not be available.
        '''
        if self.properties['LoadBalancerNames']:
            id_list = [inst.FnGetRefId() for inst in self.get_instances()]
            for lb in self.properties['LoadBalancerNames']:
                lb_resource = self.stack[lb]
                if 'Instances' in lb_resource.properties_schema:
                    lb_resource.json_snippet['Properties']['Instances'] = (
                        id_list)
                elif 'members' in lb_resource.properties_schema:
                    lb_resource.json_snippet['Properties']['members'] = (
                        id_list)
                else:
                    raise exception.Error(
                        "Unsupported resource '%s' in LoadBalancerNames" %
                        (lb,))
                resolved_snippet = self.stack.resolve_static_data(
                    lb_resource.json_snippet)
                scheduler.TaskRunner(lb_resource.update, resolved_snippet)()

    def FnGetRefId(self):
        return self.physical_resource_name()

    def _resolve_attribute(self, name):
        '''
        heat extension: "InstanceList" returns comma delimited list of server
        ip addresses.
        '''
        if name == 'InstanceList':
            return u','.join(inst.FnGetAtt('PublicIp')
                             for inst in self.get_instances()) or None
Пример #3
0
class AutoScalingGroup(InstanceGroup, CooldownMixin):

    PROPERTIES = (
        AVAILABILITY_ZONES, LAUNCH_CONFIGURATION_NAME, MAX_SIZE, MIN_SIZE,
        COOLDOWN, DESIRED_CAPACITY, HEALTH_CHECK_GRACE_PERIOD,
        HEALTH_CHECK_TYPE, LOAD_BALANCER_NAMES, VPCZONE_IDENTIFIER, TAGS,
    ) = (
        'AvailabilityZones', 'LaunchConfigurationName', 'MaxSize', 'MinSize',
        'Cooldown', 'DesiredCapacity', 'HealthCheckGracePeriod',
        'HealthCheckType', 'LoadBalancerNames', 'VPCZoneIdentifier', 'Tags',
    )

    _TAG_KEYS = (
        TAG_KEY, TAG_VALUE,
    ) = (
        'Key', 'Value',
    )

    properties_schema = {
        AVAILABILITY_ZONES: properties.Schema(
            properties.Schema.LIST,
            _('Not Implemented.'),
            required=True
        ),
        LAUNCH_CONFIGURATION_NAME: properties.Schema(
            properties.Schema.STRING,
            _('Name of LaunchConfiguration resource.'),
            required=True,
            update_allowed=True
        ),
        MAX_SIZE: properties.Schema(
            properties.Schema.STRING,
            _('Maximum number of instances in the group.'),
            required=True,
            update_allowed=True
        ),
        MIN_SIZE: properties.Schema(
            properties.Schema.STRING,
            _('Minimum number of instances in the group.'),
            required=True,
            update_allowed=True
        ),
        COOLDOWN: properties.Schema(
            properties.Schema.STRING,
            _('Cooldown period, in seconds.'),
            update_allowed=True
        ),
        DESIRED_CAPACITY: properties.Schema(
            properties.Schema.NUMBER,
            _('Desired initial number of instances.'),
            update_allowed=True
        ),
        HEALTH_CHECK_GRACE_PERIOD: properties.Schema(
            properties.Schema.INTEGER,
            _('Not Implemented.'),
            implemented=False
        ),
        HEALTH_CHECK_TYPE: properties.Schema(
            properties.Schema.STRING,
            _('Not Implemented.'),
            constraints=[
                constraints.AllowedValues(['EC2', 'ELB']),
            ],
            implemented=False
        ),
        LOAD_BALANCER_NAMES: properties.Schema(
            properties.Schema.LIST,
            _('List of LoadBalancer resources.')
        ),
        VPCZONE_IDENTIFIER: properties.Schema(
            properties.Schema.LIST,
            _('List of VPC subnet identifiers.')
        ),
        TAGS: properties.Schema(
            properties.Schema.LIST,
            _('Tags to attach to this group.'),
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    TAG_KEY: properties.Schema(
                        properties.Schema.STRING,
                        required=True
                    ),
                    TAG_VALUE: properties.Schema(
                        properties.Schema.STRING,
                        required=True
                    ),
                },
            )
        ),
    }

    rolling_update_schema = {
        'MinInstancesInService': properties.Schema(properties.Schema.NUMBER,
                                                   default=0),
        'MaxBatchSize': properties.Schema(properties.Schema.NUMBER, default=1),
        'PauseTime': properties.Schema(properties.Schema.STRING,
                                       default='PT0S')
    }
    update_policy_schema = {
        'AutoScalingRollingUpdate': properties.Schema(properties.Schema.MAP,
                                                      schema=
                                                      rolling_update_schema)
    }

    update_allowed_keys = ('Properties', 'UpdatePolicy')

    def handle_create(self):
        if self.properties[self.DESIRED_CAPACITY]:
            num_to_create = int(self.properties[self.DESIRED_CAPACITY])
        else:
            num_to_create = int(self.properties[self.MIN_SIZE])
        initial_template = self._create_template(num_to_create)
        return self.create_with_template(initial_template,
                                         self._environment())

    def check_create_complete(self, task):
        """Invoke the cooldown after creation succeeds."""
        done = super(AutoScalingGroup, self).check_create_complete(task)
        if done:
            self._cooldown_timestamp(
                "%s : %s" % ('ExactCapacity', len(self.get_instances())))
        return done

    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 tmpl_diff:
            # parse update policy
            if 'UpdatePolicy' in tmpl_diff:
                self.update_policy = Properties(
                    self.update_policy_schema,
                    json_snippet.get('UpdatePolicy', {}),
                    parent_name=self.name,
                    context=self.context)

        if prop_diff:
            self.properties = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         self.stack.resolve_runtime_data,
                                         self.name,
                                         self.context)

            # Replace instances first if launch configuration has changed
            if (self.update_policy['AutoScalingRollingUpdate'] and
                    'LaunchConfigurationName' in prop_diff):
                policy = self.update_policy['AutoScalingRollingUpdate']
                self._replace(int(policy['MinInstancesInService']),
                              int(policy['MaxBatchSize']),
                              policy['PauseTime'])

            # Get the current capacity, we may need to adjust if
            # MinSize or MaxSize has changed
            capacity = len(self.get_instances())

            # Figure out if an adjustment is required
            new_capacity = None
            if self.MIN_SIZE in prop_diff:
                if capacity < int(self.properties[self.MIN_SIZE]):
                    new_capacity = int(self.properties[self.MIN_SIZE])
            if self.MAX_SIZE in prop_diff:
                if capacity > int(self.properties[self.MAX_SIZE]):
                    new_capacity = int(self.properties[self.MAX_SIZE])
            if self.DESIRED_CAPACITY in prop_diff:
                if self.properties[self.DESIRED_CAPACITY]:
                    new_capacity = int(self.properties[self.DESIRED_CAPACITY])

            if new_capacity is not None:
                self.adjust(new_capacity, adjustment_type='ExactCapacity')

    def adjust(self, adjustment, adjustment_type='ChangeInCapacity'):
        """
        Adjust the size of the scaling group if the cooldown permits.
        """
        if self._cooldown_inprogress():
            logger.info(_("%(name)s NOT performing scaling adjustment, "
                        "cooldown %(cooldown)s") % {
                            'name': self.name,
                            'cooldown': self.properties[self.COOLDOWN]})
            return

        capacity = len(self.get_instances())
        if adjustment_type == 'ChangeInCapacity':
            new_capacity = capacity + adjustment
        elif adjustment_type == 'ExactCapacity':
            new_capacity = adjustment
        else:
            # PercentChangeInCapacity
            delta = capacity * adjustment / 100.0
            if math.fabs(delta) < 1.0:
                rounded = int(math.ceil(delta) if delta > 0.0
                              else math.floor(delta))
            else:
                rounded = int(math.floor(delta) if delta > 0.0
                              else math.ceil(delta))
            new_capacity = capacity + rounded

        upper = int(self.properties[self.MAX_SIZE])
        lower = int(self.properties[self.MIN_SIZE])

        if new_capacity > upper:
            if upper > capacity:
                logger.info(_('truncating growth to %s') % upper)
                new_capacity = upper
            else:
                logger.warn(_('can not exceed %s') % upper)
                return
        if new_capacity < lower:
            if lower < capacity:
                logger.info(_('truncating shrinkage to %s') % lower)
                new_capacity = lower
            else:
                logger.warn(_('can not be less than %s') % lower)
                return

        if new_capacity == capacity:
            logger.debug(_('no change in capacity %d') % capacity)
            return

        # send a notification before, on-error and on-success.
        notif = {
            'stack': self.stack,
            'adjustment': adjustment,
            'adjustment_type': adjustment_type,
            'capacity': capacity,
            'groupname': self.FnGetRefId(),
            'message': _("Start resizing the group %(group)s") % {
                'group': self.FnGetRefId()},
            'suffix': 'start',
        }
        notification.send(**notif)
        try:
            self.resize(new_capacity)
        except Exception as resize_ex:
            with excutils.save_and_reraise_exception():
                try:
                    notif.update({'suffix': 'error',
                                  'message': str(resize_ex),
                                  })
                    notification.send(**notif)
                except Exception:
                    logger.exception(_('Failed sending error notification'))
        else:
            notif.update({
                'suffix': 'end',
                'capacity': new_capacity,
                'message': _("End resizing the group %(group)s") % {
                    'group': notif['groupname']},
            })
            notification.send(**notif)

        self._cooldown_timestamp("%s : %s" % (adjustment_type, adjustment))

    def _tags(self):
        """Add Identifing Tags to all servers in the group.

        This is so the Dimensions received from cfn-push-stats all include
        the groupname and stack id.
        Note: the group name must match what is returned from FnGetRefId
        """
        autoscaling_tag = [{self.TAG_KEY: 'AutoScalingGroupName',
                            self.TAG_VALUE: self.FnGetRefId()}]
        return super(AutoScalingGroup, self)._tags() + autoscaling_tag

    def validate(self):
        res = super(AutoScalingGroup, self).validate()
        if res:
            return res

        # check validity of group size
        min_size = int(self.properties[self.MIN_SIZE])
        max_size = int(self.properties[self.MAX_SIZE])

        if max_size < min_size:
            msg = _("MinSize can not be greater than MaxSize")
            raise exception.StackValidationFailed(message=msg)

        if min_size < 0:
            msg = _("The size of AutoScalingGroup can not be less than zero")
            raise exception.StackValidationFailed(message=msg)

        if self.properties[self.DESIRED_CAPACITY]:
            desired_capacity = int(self.properties[self.DESIRED_CAPACITY])
            if desired_capacity < min_size or desired_capacity > max_size:
                msg = _("DesiredCapacity must be between MinSize and MaxSize")
                raise exception.StackValidationFailed(message=msg)

        # TODO(pasquier-s): once Neutron is able to assign subnets to
        # availability zones, it will be possible to specify multiple subnets.
        # For now, only one subnet can be specified. The bug #1096017 tracks
        # this issue.
        if self.properties.get(self.VPCZONE_IDENTIFIER) and \
                len(self.properties[self.VPCZONE_IDENTIFIER]) != 1:
            raise exception.NotSupported(feature=_("Anything other than one "
                                         "VPCZoneIdentifier"))
Пример #4
0
class InstanceGroup(stack_resource.StackResource):

    PROPERTIES = (
        AVAILABILITY_ZONES, LAUNCH_CONFIGURATION_NAME, SIZE,
        LOAD_BALANCER_NAMES, TAGS,
    ) = (
        'AvailabilityZones', 'LaunchConfigurationName', 'Size',
        'LoadBalancerNames', 'Tags',
    )

    _TAG_KEYS = (
        TAG_KEY, TAG_VALUE,
    ) = (
        'Key', 'Value',
    )

    properties_schema = {
        AVAILABILITY_ZONES: properties.Schema(
            properties.Schema.LIST,
            _('Not Implemented.'),
            required=True
        ),
        LAUNCH_CONFIGURATION_NAME: properties.Schema(
            properties.Schema.STRING,
            _('Name of LaunchConfiguration resource.'),
            required=True,
            update_allowed=True
        ),
        SIZE: properties.Schema(
            properties.Schema.NUMBER,
            _('Desired number of instances.'),
            required=True,
            update_allowed=True
        ),
        LOAD_BALANCER_NAMES: properties.Schema(
            properties.Schema.LIST,
            _('List of LoadBalancer resources.')
        ),
        TAGS: properties.Schema(
            properties.Schema.LIST,
            _('Tags to attach to this group.'),
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    TAG_KEY: properties.Schema(
                        properties.Schema.STRING,
                        required=True
                    ),
                    TAG_VALUE: properties.Schema(
                        properties.Schema.STRING,
                        required=True
                    ),
                },
            )
        ),
    }

    update_allowed_keys = ('Properties', 'UpdatePolicy',)

    attributes_schema = {
        "InstanceList": _("A comma-delimited list of server ip addresses. "
                          "(Heat extension).")
    }
    rolling_update_schema = {
        'MinInstancesInService': properties.Schema(properties.Schema.NUMBER,
                                                   default=0),
        'MaxBatchSize': properties.Schema(properties.Schema.NUMBER, default=1),
        'PauseTime': properties.Schema(properties.Schema.STRING,
                                       default='PT0S')
    }
    update_policy_schema = {
        'RollingUpdate': properties.Schema(properties.Schema.MAP,
                                           schema=rolling_update_schema)
    }

    def __init__(self, name, json_snippet, stack):
        """
        UpdatePolicy is currently only specific to InstanceGroup and
        AutoScalingGroup. Therefore, init is overridden to parse for the
        UpdatePolicy.
        """
        super(InstanceGroup, self).__init__(name, json_snippet, stack)
        self.update_policy = Properties(self.update_policy_schema,
                                        self.t.get('UpdatePolicy', {}),
                                        parent_name=self.name,
                                        context=self.context)

    def validate(self):
        """
        Add validation for update_policy
        """
        super(InstanceGroup, self).validate()

        if self.update_policy:
            self.update_policy.validate()
            policy_name = self.update_policy_schema.keys()[0]
            if self.update_policy[policy_name]:
                pause_time = self.update_policy[policy_name]['PauseTime']
                if iso8601utils.parse_isoduration(pause_time) > 3600:
                    raise ValueError('Maximum PauseTime is 1 hour.')

    def get_instance_names(self):
        """Get a list of resource names of the instances in this InstanceGroup.

        Failed resources will be ignored.
        """
        return [r.name for r in self.get_instances()]

    def get_instances(self):
        """Get a list of all the instance resources managed by this group.

        Sort the list of instances first by created_time then by name.
        """
        resources = []
        if self.nested():
            resources = [resource for resource in self.nested().itervalues()
                         if resource.status != resource.FAILED]
        return sorted(resources, key=lambda r: (r.created_time, r.name))

    def _environment(self):
        """Return the environment for the nested stack."""
        return {
            environment.PARAMETERS: {},
            environment.RESOURCE_REGISTRY: {
                SCALED_RESOURCE_TYPE: 'AWS::EC2::Instance',
            },
        }

    def handle_create(self):
        """Create a nested stack and add the initial resources to it."""
        num_instances = int(self.properties[self.SIZE])
        initial_template = self._create_template(num_instances)
        return self.create_with_template(initial_template, self._environment())

    def check_create_complete(self, task):
        """
        When stack creation is done, update the load balancer.

        If any instances failed to be created, delete them.
        """
        done = super(InstanceGroup, self).check_create_complete(task)
        if done:
            self._lb_reload()
        return done

    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 tmpl_diff:
            # parse update policy
            if 'UpdatePolicy' in tmpl_diff:
                self.update_policy = Properties(
                    self.update_policy_schema,
                    json_snippet.get('UpdatePolicy', {}),
                    parent_name=self.name,
                    context=self.context)

        if prop_diff:
            self.properties = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         self.stack.resolve_runtime_data,
                                         self.name,
                                         self.context)

            # Replace instances first if launch configuration has changed
            if (self.update_policy['RollingUpdate'] and
                    self.LAUNCH_CONFIGURATION_NAME in prop_diff):
                policy = self.update_policy['RollingUpdate']
                self._replace(int(policy['MinInstancesInService']),
                              int(policy['MaxBatchSize']),
                              policy['PauseTime'])

            # Get the current capacity, we may need to adjust if
            # Size has changed
            if self.SIZE in prop_diff:
                inst_list = self.get_instances()
                if len(inst_list) != int(self.properties[self.SIZE]):
                    self.resize(int(self.properties[self.SIZE]))

    def _tags(self):
        """
        Make sure that we add a tag that Ceilometer can pick up.
        These need to be prepended with 'metering.'.
        """
        tags = self.properties.get(self.TAGS) or []
        for t in tags:
            if t[self.TAG_KEY].startswith('metering.'):
                # the user has added one, don't add another.
                return tags
        return tags + [{self.TAG_KEY: 'metering.groupname',
                        self.TAG_VALUE: self.FnGetRefId()}]

    def handle_delete(self):
        return self.delete_nested()

    def _get_instance_definition(self):
        conf_name = self.properties[self.LAUNCH_CONFIGURATION_NAME]
        conf = self.stack.resource_by_refid(conf_name)
        instance_definition = copy.deepcopy(conf.t)
        instance_definition['Type'] = SCALED_RESOURCE_TYPE
        instance_definition['Properties']['Tags'] = self._tags()
        if self.properties.get('VPCZoneIdentifier'):
            instance_definition['Properties']['SubnetId'] = \
                self.properties['VPCZoneIdentifier'][0]
        # resolve references within the context of this stack.
        return self.stack.resolve_runtime_data(instance_definition)

    def _create_template(self, num_instances, num_replace=0):
        """
        Create a template to represent autoscaled instances.

        Also see heat.scaling.template.resource_templates.
        """
        instance_definition = self._get_instance_definition()
        old_resources = [(instance.name, instance.t)
                         for instance in self.get_instances()]
        templates = template.resource_templates(
            old_resources, instance_definition, num_instances, num_replace)
        return {"Resources": dict(templates)}

    def _replace(self, min_in_service, batch_size, pause_time):
        """
        Replace the instances in the group using updated launch configuration
        """
        def changing_instances(tmpl):
            instances = self.get_instances()
            serialize_template = functools.partial(json.dumps, sort_keys=True)
            current = set((i.name, serialize_template(i.t)) for i in instances)
            updated = set((k, serialize_template(v))
                          for k, v in tmpl['Resources'].items())
            # includes instances to be updated and deleted
            affected = set(k for k, v in current ^ updated)
            return set(i.FnGetRefId() for i in instances if i.name in affected)

        def pause_between_batch():
            while True:
                try:
                    yield
                except scheduler.Timeout:
                    return

        capacity = len(self.nested()) if self.nested() else 0
        efft_bat_sz = min(batch_size, capacity)
        efft_min_sz = min(min_in_service, capacity)
        pause_sec = iso8601utils.parse_isoduration(pause_time)

        batch_cnt = (capacity + efft_bat_sz - 1) // efft_bat_sz
        if pause_sec * (batch_cnt - 1) >= self.stack.timeout_mins * 60:
            raise ValueError('The current UpdatePolicy will result '
                             'in stack update timeout.')

        # effective capacity includes temporary capacity added to accommodate
        # the minimum number of instances in service during update
        efft_capacity = max(capacity - efft_bat_sz, efft_min_sz) + efft_bat_sz

        try:
            remainder = capacity
            while remainder > 0 or efft_capacity > capacity:
                if capacity - remainder >= efft_min_sz:
                    efft_capacity = capacity
                template = self._create_template(efft_capacity, efft_bat_sz)
                self._lb_reload(exclude=changing_instances(template))
                updater = self.update_with_template(template,
                                                    self._environment())
                updater.run_to_completion()
                self.check_update_complete(updater)
                remainder -= efft_bat_sz
                if remainder > 0 and pause_sec > 0:
                    self._lb_reload()
                    waiter = scheduler.TaskRunner(pause_between_batch)
                    waiter(timeout=pause_sec)
        finally:
            self._lb_reload()

    def resize(self, new_capacity):
        """
        Resize the instance group to the new capacity.

        When shrinking, the oldest instances will be removed.
        """
        new_template = self._create_template(new_capacity)
        try:
            updater = self.update_with_template(new_template,
                                                self._environment())
            updater.run_to_completion()
            self.check_update_complete(updater)
        finally:
            # Reload the LB in any case, so it's only pointing at healthy
            # nodes.
            self._lb_reload()

    def _lb_reload(self, exclude=[]):
        '''
        Notify the LoadBalancer to reload its config to include
        the changes in instances we have just made.

        This must be done after activation (instance in ACTIVE state),
        otherwise the instances' IP addresses may not be available.
        '''
        if self.properties[self.LOAD_BALANCER_NAMES]:
            id_list = [inst.FnGetRefId() for inst in self.get_instances()
                       if inst.FnGetRefId() not in exclude]
            for lb in self.properties[self.LOAD_BALANCER_NAMES]:
                lb_resource = self.stack[lb]
                if 'Instances' in lb_resource.properties_schema:
                    lb_resource.json_snippet['Properties']['Instances'] = (
                        id_list)
                elif 'members' in lb_resource.properties_schema:
                    lb_resource.json_snippet['Properties']['members'] = (
                        id_list)
                else:
                    raise exception.Error(
                        "Unsupported resource '%s' in LoadBalancerNames" %
                        (lb,))
                resolved_snippet = self.stack.resolve_static_data(
                    lb_resource.json_snippet)
                scheduler.TaskRunner(lb_resource.update, resolved_snippet)()

    def FnGetRefId(self):
        return self.physical_resource_name()

    def _resolve_attribute(self, name):
        '''
        heat extension: "InstanceList" returns comma delimited list of server
        ip addresses.
        '''
        if name == 'InstanceList':
            return u','.join(inst.FnGetAtt('PublicIp')
                             for inst in self.get_instances()) or None
Пример #5
0
class AutoScalingGroup(InstanceGroup, CooldownMixin):

    PROPERTIES = (
        AVAILABILITY_ZONES,
        LAUNCH_CONFIGURATION_NAME,
        MAX_SIZE,
        MIN_SIZE,
        COOLDOWN,
        DESIRED_CAPACITY,
        HEALTH_CHECK_GRACE_PERIOD,
        HEALTH_CHECK_TYPE,
        LOAD_BALANCER_NAMES,
        VPCZONE_IDENTIFIER,
        TAGS,
    ) = (
        'AvailabilityZones',
        'LaunchConfigurationName',
        'MaxSize',
        'MinSize',
        'Cooldown',
        'DesiredCapacity',
        'HealthCheckGracePeriod',
        'HealthCheckType',
        'LoadBalancerNames',
        'VPCZoneIdentifier',
        'Tags',
    )

    _TAG_KEYS = (
        TAG_KEY,
        TAG_VALUE,
    ) = (
        'Key',
        'Value',
    )

    _UPDATE_POLICY_SCHEMA_KEYS = (ROLLING_UPDATE) = (
        'AutoScalingRollingUpdate')

    _ROLLING_UPDATE_SCHEMA_KEYS = (MIN_INSTANCES_IN_SERVICE, MAX_BATCH_SIZE,
                                   PAUSE_TIME) = ('MinInstancesInService',
                                                  'MaxBatchSize', 'PauseTime')

    properties_schema = {
        AVAILABILITY_ZONES:
        properties.Schema(properties.Schema.LIST,
                          _('Not Implemented.'),
                          required=True),
        LAUNCH_CONFIGURATION_NAME:
        properties.Schema(properties.Schema.STRING,
                          _('Name of LaunchConfiguration resource.'),
                          required=True,
                          update_allowed=True),
        MAX_SIZE:
        properties.Schema(properties.Schema.INTEGER,
                          _('Maximum number of instances in the group.'),
                          required=True,
                          update_allowed=True),
        MIN_SIZE:
        properties.Schema(properties.Schema.INTEGER,
                          _('Minimum number of instances in the group.'),
                          required=True,
                          update_allowed=True),
        COOLDOWN:
        properties.Schema(properties.Schema.NUMBER,
                          _('Cooldown period, in seconds.'),
                          update_allowed=True),
        DESIRED_CAPACITY:
        properties.Schema(properties.Schema.INTEGER,
                          _('Desired initial number of instances.'),
                          update_allowed=True),
        HEALTH_CHECK_GRACE_PERIOD:
        properties.Schema(properties.Schema.INTEGER,
                          _('Not Implemented.'),
                          implemented=False),
        HEALTH_CHECK_TYPE:
        properties.Schema(properties.Schema.STRING,
                          _('Not Implemented.'),
                          constraints=[
                              constraints.AllowedValues(['EC2', 'ELB']),
                          ],
                          implemented=False),
        LOAD_BALANCER_NAMES:
        properties.Schema(properties.Schema.LIST,
                          _('List of LoadBalancer resources.')),
        VPCZONE_IDENTIFIER:
        properties.Schema(
            properties.Schema.LIST,
            _('Use only with Neutron, to list the internal subnet to '
              'which the instance will be attached; '
              'needed only if multiple exist; '
              'list length must be exactly 1.'),
            schema=properties.Schema(
                properties.Schema.STRING,
                _('UUID of the internal subnet to which the instance '
                  'will be attached.'))),
        TAGS:
        properties.Schema(properties.Schema.LIST,
                          _('Tags to attach to this group.'),
                          schema=properties.Schema(
                              properties.Schema.MAP,
                              schema={
                                  TAG_KEY:
                                  properties.Schema(properties.Schema.STRING,
                                                    required=True),
                                  TAG_VALUE:
                                  properties.Schema(properties.Schema.STRING,
                                                    required=True),
                              },
                          )),
    }

    rolling_update_schema = {
        MIN_INSTANCES_IN_SERVICE:
        properties.Schema(properties.Schema.INTEGER, default=0),
        MAX_BATCH_SIZE:
        properties.Schema(properties.Schema.INTEGER, default=1),
        PAUSE_TIME:
        properties.Schema(properties.Schema.STRING, default='PT0S')
    }

    update_policy_schema = {
        ROLLING_UPDATE:
        properties.Schema(properties.Schema.MAP, schema=rolling_update_schema)
    }

    def handle_create(self):
        if self.properties[self.DESIRED_CAPACITY]:
            num_to_create = self.properties[self.DESIRED_CAPACITY]
        else:
            num_to_create = self.properties[self.MIN_SIZE]
        initial_template = self._create_template(num_to_create)
        return self.create_with_template(initial_template, self._environment())

    def check_create_complete(self, task):
        """Invoke the cooldown after creation succeeds."""
        done = super(AutoScalingGroup, self).check_create_complete(task)
        if done:
            self._cooldown_timestamp(
                "%s : %s" % (EXACT_CAPACITY, len(self.get_instances())))
        return done

    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 tmpl_diff:
            # parse update policy
            if 'UpdatePolicy' in tmpl_diff:
                self.update_policy = Properties(self.update_policy_schema,
                                                json_snippet.get(
                                                    'UpdatePolicy', {}),
                                                parent_name=self.name,
                                                context=self.context)

        if prop_diff:
            self.properties = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         function.resolve, self.name,
                                         self.context)

            # Replace instances first if launch configuration has changed
            self._try_rolling_update(prop_diff)

            # Get the current capacity, we may need to adjust if
            # MinSize or MaxSize has changed
            capacity = len(self.get_instances())

            # Figure out if an adjustment is required
            new_capacity = None
            if self.MIN_SIZE in prop_diff:
                if capacity < self.properties[self.MIN_SIZE]:
                    new_capacity = self.properties[self.MIN_SIZE]
            if self.MAX_SIZE in prop_diff:
                if capacity > self.properties[self.MAX_SIZE]:
                    new_capacity = self.properties[self.MAX_SIZE]
            if self.DESIRED_CAPACITY in prop_diff:
                if self.properties[self.DESIRED_CAPACITY] is not None:
                    new_capacity = self.properties[self.DESIRED_CAPACITY]

            if new_capacity is not None:
                self.adjust(new_capacity, adjustment_type=EXACT_CAPACITY)

    def adjust(self, adjustment, adjustment_type=CHANGE_IN_CAPACITY):
        """
        Adjust the size of the scaling group if the cooldown permits.
        """
        if self._cooldown_inprogress():
            logger.info(
                _("%(name)s NOT performing scaling adjustment, "
                  "cooldown %(cooldown)s") % {
                      'name': self.name,
                      'cooldown': self.properties[self.COOLDOWN]
                  })
            return

        capacity = len(self.get_instances())
        if adjustment_type == CHANGE_IN_CAPACITY:
            new_capacity = capacity + adjustment
        elif adjustment_type == EXACT_CAPACITY:
            new_capacity = adjustment
        else:
            # PercentChangeInCapacity
            delta = capacity * adjustment / 100.0
            if math.fabs(delta) < 1.0:
                rounded = int(
                    math.ceil(delta) if delta > 0.0 else math.floor(delta))
            else:
                rounded = int(
                    math.floor(delta) if delta > 0.0 else math.ceil(delta))
            new_capacity = capacity + rounded

        upper = self.properties[self.MAX_SIZE]
        lower = self.properties[self.MIN_SIZE]

        if new_capacity > upper:
            if upper > capacity:
                logger.info(_('truncating growth to %s') % upper)
                new_capacity = upper
            else:
                logger.warn(_('can not exceed %s') % upper)
                return
        if new_capacity < lower:
            if lower < capacity:
                logger.info(_('truncating shrinkage to %s') % lower)
                new_capacity = lower
            else:
                logger.warn(_('can not be less than %s') % lower)
                return

        if new_capacity == capacity:
            logger.debug('no change in capacity %d' % capacity)
            return

        # send a notification before, on-error and on-success.
        notif = {
            'stack': self.stack,
            'adjustment': adjustment,
            'adjustment_type': adjustment_type,
            'capacity': capacity,
            'groupname': self.FnGetRefId(),
            'message': _("Start resizing the group %(group)s") % {
                'group': self.FnGetRefId()
            },
            'suffix': 'start',
        }
        notification.send(**notif)
        try:
            self.resize(new_capacity)
        except Exception as resize_ex:
            with excutils.save_and_reraise_exception():
                try:
                    notif.update({
                        'suffix': 'error',
                        'message': six.text_type(resize_ex),
                    })
                    notification.send(**notif)
                except Exception:
                    logger.exception(_('Failed sending error notification'))
        else:
            notif.update({
                'suffix': 'end',
                'capacity': new_capacity,
                'message': _("End resizing the group %(group)s") % {
                    'group': notif['groupname']
                },
            })
            notification.send(**notif)

        self._cooldown_timestamp("%s : %s" % (adjustment_type, adjustment))

    def _tags(self):
        """Add Identifing Tags to all servers in the group.

        This is so the Dimensions received from cfn-push-stats all include
        the groupname and stack id.
        Note: the group name must match what is returned from FnGetRefId
        """
        autoscaling_tag = [{
            self.TAG_KEY: 'AutoScalingGroupName',
            self.TAG_VALUE: self.FnGetRefId()
        }]
        return super(AutoScalingGroup, self)._tags() + autoscaling_tag

    def validate(self):
        res = super(AutoScalingGroup, self).validate()
        if res:
            return res

        # check validity of group size
        min_size = self.properties[self.MIN_SIZE]
        max_size = self.properties[self.MAX_SIZE]

        if max_size < min_size:
            msg = _("MinSize can not be greater than MaxSize")
            raise exception.StackValidationFailed(message=msg)

        if min_size < 0:
            msg = _("The size of AutoScalingGroup can not be less than zero")
            raise exception.StackValidationFailed(message=msg)

        if self.properties[self.DESIRED_CAPACITY] is not None:
            desired_capacity = self.properties[self.DESIRED_CAPACITY]
            if desired_capacity < min_size or desired_capacity > max_size:
                msg = _("DesiredCapacity must be between MinSize and MaxSize")
                raise exception.StackValidationFailed(message=msg)

        # TODO(pasquier-s): once Neutron is able to assign subnets to
        # availability zones, it will be possible to specify multiple subnets.
        # For now, only one subnet can be specified. The bug #1096017 tracks
        # this issue.
        if self.properties.get(self.VPCZONE_IDENTIFIER) and \
                len(self.properties[self.VPCZONE_IDENTIFIER]) != 1:
            raise exception.NotSupported(feature=_("Anything other than one "
                                                   "VPCZoneIdentifier"))
Пример #6
0
class InstanceGroup(stack_resource.StackResource):

    PROPERTIES = (
        AVAILABILITY_ZONES,
        LAUNCH_CONFIGURATION_NAME,
        SIZE,
        LOAD_BALANCER_NAMES,
        TAGS,
    ) = (
        'AvailabilityZones',
        'LaunchConfigurationName',
        'Size',
        'LoadBalancerNames',
        'Tags',
    )

    _TAG_KEYS = (
        TAG_KEY,
        TAG_VALUE,
    ) = (
        'Key',
        'Value',
    )

    _ROLLING_UPDATE_SCHEMA_KEYS = (MIN_INSTANCES_IN_SERVICE, MAX_BATCH_SIZE,
                                   PAUSE_TIME) = ('MinInstancesInService',
                                                  'MaxBatchSize', 'PauseTime')

    _UPDATE_POLICY_SCHEMA_KEYS = (ROLLING_UPDATE, ) = ('RollingUpdate', )

    properties_schema = {
        AVAILABILITY_ZONES:
        properties.Schema(properties.Schema.LIST,
                          _('Not Implemented.'),
                          required=True),
        LAUNCH_CONFIGURATION_NAME:
        properties.Schema(properties.Schema.STRING,
                          _('Name of LaunchConfiguration resource.'),
                          required=True,
                          update_allowed=True),
        SIZE:
        properties.Schema(properties.Schema.INTEGER,
                          _('Desired number of instances.'),
                          required=True,
                          update_allowed=True),
        LOAD_BALANCER_NAMES:
        properties.Schema(properties.Schema.LIST,
                          _('List of LoadBalancer resources.')),
        TAGS:
        properties.Schema(properties.Schema.LIST,
                          _('Tags to attach to this group.'),
                          schema=properties.Schema(
                              properties.Schema.MAP,
                              schema={
                                  TAG_KEY:
                                  properties.Schema(properties.Schema.STRING,
                                                    required=True),
                                  TAG_VALUE:
                                  properties.Schema(properties.Schema.STRING,
                                                    required=True),
                              },
                          )),
    }

    attributes_schema = {
        "InstanceList":
        _("A comma-delimited list of server ip addresses. "
          "(Heat extension).")
    }
    rolling_update_schema = {
        MIN_INSTANCES_IN_SERVICE:
        properties.Schema(properties.Schema.NUMBER, default=0),
        MAX_BATCH_SIZE:
        properties.Schema(properties.Schema.NUMBER, default=1),
        PAUSE_TIME:
        properties.Schema(properties.Schema.STRING, default='PT0S')
    }
    update_policy_schema = {
        ROLLING_UPDATE:
        properties.Schema(properties.Schema.MAP, schema=rolling_update_schema)
    }

    def __init__(self, name, json_snippet, stack):
        """
        UpdatePolicy is currently only specific to InstanceGroup and
        AutoScalingGroup. Therefore, init is overridden to parse for the
        UpdatePolicy.
        """
        super(InstanceGroup, self).__init__(name, json_snippet, stack)
        self.update_policy = Properties(self.update_policy_schema,
                                        self.t.get('UpdatePolicy', {}),
                                        parent_name=self.name,
                                        context=self.context)

    def validate(self):
        """
        Add validation for update_policy
        """
        super(InstanceGroup, self).validate()

        if self.update_policy:
            self.update_policy.validate()
            policy_name = self.update_policy_schema.keys()[0]
            if self.update_policy[policy_name]:
                pause_time = self.update_policy[policy_name][self.PAUSE_TIME]
                if iso8601utils.parse_isoduration(pause_time) > 3600:
                    raise ValueError('Maximum PauseTime is 1 hour.')

    def get_instance_names(self):
        """Get a list of resource names of the instances in this InstanceGroup.

        Failed resources will be ignored.
        """
        return [r.name for r in self.get_instances()]

    def get_instances(self):
        """Get a list of all the instance resources managed by this group.

        Sort the list of instances first by created_time then by name.
        """
        resources = []
        if self.nested():
            resources = [
                resource for resource in self.nested().itervalues()
                if resource.status != resource.FAILED
            ]
        return sorted(resources, key=lambda r: (r.created_time, r.name))

    def _environment(self):
        """Return the environment for the nested stack."""
        return {
            environment.PARAMETERS: {},
            environment.RESOURCE_REGISTRY: {
                SCALED_RESOURCE_TYPE: 'AWS::EC2::Instance',
            },
        }

    def handle_create(self):
        """Create a nested stack and add the initial resources to it."""
        num_instances = self.properties[self.SIZE]
        initial_template = self._create_template(num_instances)
        return self.create_with_template(initial_template, self._environment())

    def check_create_complete(self, task):
        """
        When stack creation is done, update the load balancer.

        If any instances failed to be created, delete them.
        """
        done = super(InstanceGroup, self).check_create_complete(task)
        if done:
            self._lb_reload()
        return done

    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 tmpl_diff:
            # parse update policy
            if 'UpdatePolicy' in tmpl_diff:
                self.update_policy = Properties(self.update_policy_schema,
                                                json_snippet.get(
                                                    'UpdatePolicy', {}),
                                                parent_name=self.name,
                                                context=self.context)

        if prop_diff:
            self.properties = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         function.resolve, self.name,
                                         self.context)

            # Replace instances first if launch configuration has changed
            self._try_rolling_update(prop_diff)

            # Get the current capacity, we may need to adjust if
            # Size has changed
            if self.SIZE in prop_diff:
                inst_list = self.get_instances()
                if len(inst_list) != self.properties[self.SIZE]:
                    self.resize(self.properties[self.SIZE])

    def _tags(self):
        """
        Make sure that we add a tag that Ceilometer can pick up.
        These need to be prepended with 'metering.'.
        """
        tags = self.properties.get(self.TAGS) or []
        for t in tags:
            if t[self.TAG_KEY].startswith('metering.'):
                # the user has added one, don't add another.
                return tags
        return tags + [{
            self.TAG_KEY: 'metering.groupname',
            self.TAG_VALUE: self.FnGetRefId()
        }]

    def handle_delete(self):
        return self.delete_nested()

    def _get_instance_definition(self):
        conf_name = self.properties[self.LAUNCH_CONFIGURATION_NAME]
        conf = self.stack.resource_by_refid(conf_name)
        instance_definition = function.resolve(conf.t)
        instance_definition['Type'] = SCALED_RESOURCE_TYPE
        instance_definition['Properties']['Tags'] = self._tags()
        if self.properties.get('VPCZoneIdentifier'):
            instance_definition['Properties']['SubnetId'] = \
                self.properties['VPCZoneIdentifier'][0]
        return instance_definition

    def _get_instance_templates(self):
        """Get templates for resource instances."""
        return [(instance.name, instance.t)
                for instance in self.get_instances()]

    def _create_template(self, num_instances, num_replace=0):
        """
        Create a template to represent autoscaled instances.

        Also see heat.scaling.template.resource_templates.
        """
        instance_definition = self._get_instance_definition()
        old_resources = self._get_instance_templates()
        templates = template.resource_templates(old_resources,
                                                instance_definition,
                                                num_instances, num_replace)
        return {
            "HeatTemplateFormatVersion": "2012-12-12",
            "Resources": dict(templates)
        }

    def _try_rolling_update(self, prop_diff):
        if (self.update_policy[self.ROLLING_UPDATE]
                and self.LAUNCH_CONFIGURATION_NAME in prop_diff):
            policy = self.update_policy[self.ROLLING_UPDATE]
            pause_sec = iso8601utils.parse_isoduration(policy[self.PAUSE_TIME])
            self._replace(policy[self.MIN_INSTANCES_IN_SERVICE],
                          policy[self.MAX_BATCH_SIZE], pause_sec)

    def _replace(self, min_in_service, batch_size, pause_sec):
        """
        Replace the instances in the group using updated launch configuration
        """
        def changing_instances(tmpl):
            def serialize_template(t):
                return json.dumps(function.resolve(t), sort_keys=True)

            instances = self.get_instances()
            # To support both HOT and CFN, need to find out what the name of
            # the resources key is.
            resources_key = self.nested().t.RESOURCES
            current = set((i.name, serialize_template(i.t)) for i in instances)
            updated = set((k, serialize_template(v))
                          for k, v in tmpl[resources_key].items())
            # includes instances to be updated and deleted
            affected = set(k for k, v in current ^ updated)
            return set(i.FnGetRefId() for i in instances if i.name in affected)

        def pause_between_batch():
            while True:
                try:
                    yield
                except scheduler.Timeout:
                    return

        capacity = len(self.nested()) if self.nested() else 0
        efft_bat_sz = min(batch_size, capacity)
        efft_min_sz = min(min_in_service, capacity)

        batch_cnt = (capacity + efft_bat_sz - 1) // efft_bat_sz
        if pause_sec * (batch_cnt - 1) >= self.stack.timeout_secs():
            raise ValueError('The current UpdatePolicy will result '
                             'in stack update timeout.')

        # effective capacity includes temporary capacity added to accommodate
        # the minimum number of instances in service during update
        efft_capacity = max(capacity - efft_bat_sz, efft_min_sz) + efft_bat_sz

        try:
            remainder = capacity
            while remainder > 0 or efft_capacity > capacity:
                if capacity - remainder >= efft_min_sz:
                    efft_capacity = capacity
                template = self._create_template(efft_capacity, efft_bat_sz)
                self._lb_reload(exclude=changing_instances(template))
                updater = self.update_with_template(template,
                                                    self._environment())
                updater.run_to_completion()
                self.check_update_complete(updater)
                remainder -= efft_bat_sz
                if remainder > 0 and pause_sec > 0:
                    self._lb_reload()
                    waiter = scheduler.TaskRunner(pause_between_batch)
                    waiter(timeout=pause_sec)
        finally:
            self._lb_reload()

    def resize(self, new_capacity):
        """
        Resize the instance group to the new capacity.

        When shrinking, the oldest instances will be removed.
        """
        new_template = self._create_template(new_capacity)
        try:
            updater = self.update_with_template(new_template,
                                                self._environment())
            updater.run_to_completion()
            self.check_update_complete(updater)
        finally:
            # Reload the LB in any case, so it's only pointing at healthy
            # nodes.
            self._lb_reload()

    def _lb_reload(self, exclude=[]):
        '''
        Notify the LoadBalancer to reload its config to include
        the changes in instances we have just made.

        This must be done after activation (instance in ACTIVE state),
        otherwise the instances' IP addresses may not be available.
        '''
        if self.properties[self.LOAD_BALANCER_NAMES]:
            id_list = [
                inst.FnGetRefId() for inst in self.get_instances()
                if inst.FnGetRefId() not in exclude
            ]
            for lb in self.properties[self.LOAD_BALANCER_NAMES]:
                lb_resource = self.stack[lb]
                lb_defn = copy.deepcopy(lb_resource.t)
                if 'Instances' in lb_resource.properties_schema:
                    lb_defn['Properties']['Instances'] = id_list
                elif 'members' in lb_resource.properties_schema:
                    lb_defn['Properties']['members'] = id_list
                else:
                    raise exception.Error(
                        _("Unsupported resource '%s' in LoadBalancerNames") %
                        (lb, ))
                resolved_snippet = self.stack.resolve_static_data(lb_defn)
                scheduler.TaskRunner(lb_resource.update, resolved_snippet)()

    def FnGetRefId(self):
        return self.physical_resource_name()

    def _resolve_attribute(self, name):
        '''
        heat extension: "InstanceList" returns comma delimited list of server
        ip addresses.
        '''
        if name == 'InstanceList':
            return u','.join(
                inst.FnGetAtt('PublicIp')
                for inst in self.get_instances()) or None

    def child_template(self):
        num_instances = int(self.properties[self.SIZE])
        return self._create_template(num_instances)

    def child_params(self):
        return self._environment()
Пример #7
0
class InstanceGroup(stack_resource.StackResource):
    tags_schema = {
        'Key': {
            'Type': 'String',
            'Required': True
        },
        'Value': {
            'Type': 'String',
            'Required': True
        }
    }
    properties_schema = {
        'AvailabilityZones': {
            'Required': True,
            'Type': 'List'
        },
        'LaunchConfigurationName': {
            'Required': True,
            'Type': 'String'
        },
        'Size': {
            'Required': True,
            'Type': 'Number'
        },
        'LoadBalancerNames': {
            'Type': 'List'
        },
        'Tags': {
            'Type': 'List',
            'Schema': {
                'Type': 'Map',
                'Schema': tags_schema
            }
        }
    }
    update_allowed_keys = (
        'Properties',
        'UpdatePolicy',
    )
    update_allowed_properties = (
        'Size',
        'LaunchConfigurationName',
    )
    attributes_schema = {
        "InstanceList": ("A comma-delimited list of server ip addresses. "
                         "(Heat extension)")
    }
    rolling_update_schema = {
        'MinInstancesInService': properties.Schema(properties.NUMBER,
                                                   default=0),
        'MaxBatchSize': properties.Schema(properties.NUMBER, default=1),
        'PauseTime': properties.Schema(properties.STRING, default='PT0S')
    }
    update_policy_schema = {
        'RollingUpdate':
        properties.Schema(properties.MAP, schema=rolling_update_schema)
    }

    def __init__(self, name, json_snippet, stack):
        """
        UpdatePolicy is currently only specific to InstanceGroup and
        AutoScalingGroup. Therefore, init is overridden to parse for the
        UpdatePolicy.
        """
        super(InstanceGroup, self).__init__(name, json_snippet, stack)
        self.update_policy = Properties(self.update_policy_schema,
                                        self.t.get('UpdatePolicy', {}),
                                        parent_name=self.name)

    def validate(self):
        """
        Add validation for update_policy
        """
        super(InstanceGroup, self).validate()
        if self.update_policy:
            self.update_policy.validate()

    def get_instance_names(self):
        """Get a list of resource names of the instances in this InstanceGroup.

        Failed resources will be ignored.
        """
        return sorted(x.name for x in self.get_instances())

    def get_instances(self):
        """Get a set of all the instance resources managed by this group."""
        return [
            resource for resource in self.nested()
            if resource.state[1] != resource.FAILED
        ]

    def handle_create(self):
        """Create a nested stack and add the initial resources to it."""
        num_instances = int(self.properties['Size'])
        initial_template = self._create_template(num_instances)
        return self.create_with_template(initial_template, {})

    def check_create_complete(self, task):
        """
        When stack creation is done, update the load balancer.

        If any instances failed to be created, delete them.
        """
        done = super(InstanceGroup, self).check_create_complete(task)
        if done:
            self._lb_reload()
        return done

    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 tmpl_diff:
            # parse update policy
            if 'UpdatePolicy' in tmpl_diff:
                self.update_policy = Properties(self.update_policy_schema,
                                                json_snippet.get(
                                                    'UpdatePolicy', {}),
                                                parent_name=self.name)

        if prop_diff:
            self.properties = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         self.stack.resolve_runtime_data,
                                         self.name)

            # Get the current capacity, we may need to adjust if
            # Size has changed
            if 'Size' in prop_diff:
                inst_list = self.get_instances()
                if len(inst_list) != int(self.properties['Size']):
                    self.resize(int(self.properties['Size']))

    def _tags(self):
        """
        Make sure that we add a tag that Ceilometer can pick up.
        These need to be prepended with 'metering.'.
        """
        tags = self.properties.get('Tags') or []
        for t in tags:
            if t['Key'].startswith('metering.'):
                # the user has added one, don't add another.
                return tags
        return tags + [{
            'Key': 'metering.groupname',
            'Value': self.FnGetRefId()
        }]

    def handle_delete(self):
        return self.delete_nested()

    def _create_template(self, num_instances):
        """
        Create a template with a number of instance definitions based on the
        launch configuration.
        """
        conf_name = self.properties['LaunchConfigurationName']
        conf = self.stack.resource_by_refid(conf_name)
        instance_definition = copy.deepcopy(conf.t)
        instance_definition['Type'] = 'AWS::EC2::Instance'
        instance_definition['Properties']['Tags'] = self._tags()
        if self.properties.get('VPCZoneIdentifier'):
            instance_definition['Properties']['SubnetId'] = \
                self.properties['VPCZoneIdentifier'][0]
        # resolve references within the context of this stack.
        fully_parsed = self.stack.resolve_runtime_data(instance_definition)

        resources = {}
        for i in range(num_instances):
            resources["%s-%d" % (self.name, i)] = fully_parsed
        return {"Resources": resources}

    def resize(self, new_capacity):
        """
        Resize the instance group to the new capacity.

        When shrinking, the newest instances will be removed.
        """
        new_template = self._create_template(new_capacity)
        try:
            self.update_with_template(new_template, {})
        finally:
            # Reload the LB in any case, so it's only pointing at healthy
            # nodes.
            self._lb_reload()

    def _lb_reload(self):
        '''
        Notify the LoadBalancer to reload its config to include
        the changes in instances we have just made.

        This must be done after activation (instance in ACTIVE state),
        otherwise the instances' IP addresses may not be available.
        '''
        if self.properties['LoadBalancerNames']:
            id_list = [inst.FnGetRefId() for inst in self.get_instances()]
            for lb in self.properties['LoadBalancerNames']:
                lb_resource = self.stack[lb]
                if 'Instances' in lb_resource.properties_schema:
                    lb_resource.json_snippet['Properties']['Instances'] = (
                        id_list)
                elif 'members' in lb_resource.properties_schema:
                    lb_resource.json_snippet['Properties']['members'] = (
                        id_list)
                else:
                    raise exception.Error(
                        "Unsupported resource '%s' in LoadBalancerNames" %
                        (lb, ))
                resolved_snippet = self.stack.resolve_static_data(
                    lb_resource.json_snippet)
                scheduler.TaskRunner(lb_resource.update, resolved_snippet)()

    def FnGetRefId(self):
        return unicode(self.name)

    def _resolve_attribute(self, name):
        '''
        heat extension: "InstanceList" returns comma delimited list of server
        ip addresses.
        '''
        if name == 'InstanceList':
            ips = [
                inst.FnGetAtt('PublicIp')
                for inst in self._nested.resources.values()
            ]
            if ips:
                return unicode(','.join(ips))
Пример #8
0
class AutoScalingGroup(InstanceGroup, CooldownMixin):
    tags_schema = {
        'Key': {
            'Type': 'String',
            'Required': True
        },
        'Value': {
            'Type': 'String',
            'Required': True
        }
    }
    properties_schema = {
        'AvailabilityZones': {
            'Required': True,
            'Type': 'List'
        },
        'LaunchConfigurationName': {
            'Required': True,
            'Type': 'String'
        },
        'MaxSize': {
            'Required': True,
            'Type': 'String'
        },
        'MinSize': {
            'Required': True,
            'Type': 'String'
        },
        'Cooldown': {
            'Type': 'String'
        },
        'DesiredCapacity': {
            'Type': 'Number'
        },
        'HealthCheckGracePeriod': {
            'Type': 'Integer',
            'Implemented': False
        },
        'HealthCheckType': {
            'Type': 'String',
            'AllowedValues': ['EC2', 'ELB'],
            'Implemented': False
        },
        'LoadBalancerNames': {
            'Type': 'List'
        },
        'VPCZoneIdentifier': {
            'Type': 'List'
        },
        'Tags': {
            'Type': 'List',
            'Schema': {
                'Type': 'Map',
                'Schema': tags_schema
            }
        }
    }
    rolling_update_schema = {
        'MinInstancesInService': properties.Schema(properties.NUMBER,
                                                   default=0),
        'MaxBatchSize': properties.Schema(properties.NUMBER, default=1),
        'PauseTime': properties.Schema(properties.STRING, default='PT0S')
    }
    update_policy_schema = {
        'AutoScalingRollingUpdate':
        properties.Schema(properties.MAP, schema=rolling_update_schema)
    }

    # template keys and properties supported for handle_update,
    # note trailing comma is required for a single item to get a tuple
    update_allowed_keys = (
        'Properties',
        'UpdatePolicy',
    )
    update_allowed_properties = (
        'LaunchConfigurationName',
        'MaxSize',
        'MinSize',
        'Cooldown',
        'DesiredCapacity',
    )

    def handle_create(self):
        if self.properties['DesiredCapacity']:
            num_to_create = int(self.properties['DesiredCapacity'])
        else:
            num_to_create = int(self.properties['MinSize'])
        initial_template = self._create_template(num_to_create)
        return self.create_with_template(initial_template, {})

    def check_create_complete(self, task):
        """Invoke the cooldown after creation succeeds."""
        done = super(AutoScalingGroup, self).check_create_complete(task)
        if done:
            self._cooldown_timestamp(
                "%s : %s" % ('ExactCapacity', len(self.get_instances())))
        return done

    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 tmpl_diff:
            # parse update policy
            if 'UpdatePolicy' in tmpl_diff:
                self.update_policy = Properties(self.update_policy_schema,
                                                json_snippet.get(
                                                    'UpdatePolicy', {}),
                                                parent_name=self.name)

        if prop_diff:
            self.properties = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         self.stack.resolve_runtime_data,
                                         self.name)

            # Get the current capacity, we may need to adjust if
            # MinSize or MaxSize has changed
            capacity = len(self.get_instances())

            # Figure out if an adjustment is required
            new_capacity = None
            if 'MinSize' in prop_diff:
                if capacity < int(self.properties['MinSize']):
                    new_capacity = int(self.properties['MinSize'])
            if 'MaxSize' in prop_diff:
                if capacity > int(self.properties['MaxSize']):
                    new_capacity = int(self.properties['MaxSize'])
            if 'DesiredCapacity' in prop_diff:
                if self.properties['DesiredCapacity']:
                    new_capacity = int(self.properties['DesiredCapacity'])

            if new_capacity is not None:
                self.adjust(new_capacity, adjustment_type='ExactCapacity')

    def adjust(self, adjustment, adjustment_type='ChangeInCapacity'):
        """
        Adjust the size of the scaling group if the cooldown permits.
        """
        if self._cooldown_inprogress():
            logger.info("%s NOT performing scaling adjustment, cooldown %s" %
                        (self.name, self.properties['Cooldown']))
            return

        capacity = len(self.get_instances())
        if adjustment_type == 'ChangeInCapacity':
            new_capacity = capacity + adjustment
        elif adjustment_type == 'ExactCapacity':
            new_capacity = adjustment
        else:
            # PercentChangeInCapacity
            new_capacity = capacity + (capacity * adjustment / 100)

        if new_capacity > int(self.properties['MaxSize']):
            logger.warn('can not exceed %s' % self.properties['MaxSize'])
            return
        if new_capacity < int(self.properties['MinSize']):
            logger.warn('can not be less than %s' % self.properties['MinSize'])
            return

        if new_capacity == capacity:
            logger.debug('no change in capacity %d' % capacity)
            return

        result = self.resize(new_capacity)

        self._cooldown_timestamp("%s : %s" % (adjustment_type, adjustment))

        return result

    def _tags(self):
        """Add Identifing Tags to all servers in the group.

        This is so the Dimensions received from cfn-push-stats all include
        the groupname and stack id.
        Note: the group name must match what is returned from FnGetRefId
        """
        autoscaling_tag = [{
            'Key': 'AutoScalingGroupName',
            'Value': self.FnGetRefId()
        }]
        return super(AutoScalingGroup, self)._tags() + autoscaling_tag

    def FnGetRefId(self):
        return unicode(self.name)

    def validate(self):
        res = super(AutoScalingGroup, self).validate()
        if res:
            return res

        # TODO(pasquier-s): once Neutron is able to assign subnets to
        # availability zones, it will be possible to specify multiple subnets.
        # For now, only one subnet can be specified. The bug #1096017 tracks
        # this issue.
        if self.properties.get('VPCZoneIdentifier') and \
                len(self.properties['VPCZoneIdentifier']) != 1:
            raise exception.NotSupported(feature=_("Anything other than one "
                                                   "VPCZoneIdentifier"))
Пример #9
0
class InstanceGroup(stack_resource.StackResource):
    tags_schema = {'Key': {'Type': 'String',
                           'Required': True},
                   'Value': {'Type': 'String',
                             'Required': True}}
    properties_schema = {
        'AvailabilityZones': {'Required': True,
                              'Type': 'List'},
        'LaunchConfigurationName': {'Required': True,
                                    'Type': 'String'},
        'Size': {'Required': True,
                 'Type': 'Number'},
        'LoadBalancerNames': {'Type': 'List'},
        'Tags': {'Type': 'List',
                 'Schema': {'Type': 'Map',
                            'Schema': tags_schema}}
    }
    update_allowed_keys = ('Properties',)
    update_allowed_properties = ('Size',)
    attributes_schema = {
        "InstanceList": ("A comma-delimited list of server ip addresses. "
                         "(Heat extension)")
    }

    def get_instance_names(self):
        """Get a list of resource names of the instances in this InstanceGroup.

        Deleted resources will be ignored.
        """
        return sorted(x.name for x in self.get_instances())

    def get_instances(self):
        """Get a set of all the instance resources managed by this group."""
        return [resource for resource in self.nested()
                if resource.state[0] != resource.DELETE]

    def handle_create(self):
        """Create a nested stack and add the initial resources to it."""
        num_instances = int(self.properties['Size'])
        initial_template = self._create_template(num_instances)
        return self.create_with_template(initial_template, {})

    def check_create_complete(self, task):
        """
        When stack creation is done, update the load balancer.

        If any instances failed to be created, delete them.
        """
        try:
            done = super(InstanceGroup, self).check_create_complete(task)
        except exception.Error as exc:
            for resource in self.nested():
                if resource.state == ('CREATE', 'FAILED'):
                    resource.destroy()
            raise
        if done and len(self.get_instances()):
            self._lb_reload()
        return done

    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 = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         self.stack.resolve_runtime_data,
                                         self.name)

            # Get the current capacity, we may need to adjust if
            # Size has changed
            if 'Size' in prop_diff:
                inst_list = self.get_instances()
                if len(inst_list) != int(self.properties['Size']):
                    self.resize(int(self.properties['Size']))

    def _tags(self):
        """
        Make sure that we add a tag that Ceilometer can pick up.
        These need to be prepended with 'metering.'.
        """
        tags = self.properties.get('Tags') or []
        for t in tags:
            if t['Key'].startswith('metering.'):
                # the user has added one, don't add another.
                return tags
        return tags + [{'Key': 'metering.groupname',
                        'Value': self.FnGetRefId()}]

    def handle_delete(self):
        return self.delete_nested()

    def _create_template(self, num_instances):
        """
        Create a template with a number of instance definitions based on the
        launch configuration.
        """
        conf_name = self.properties['LaunchConfigurationName']
        instance_definition = self.stack.t['Resources'][conf_name].copy()
        instance_definition['Type'] = 'AWS::EC2::Instance'
        instance_definition['Properties']['Tags'] = self._tags()
        resources = {}
        for i in range(num_instances):
            resources["%s-%d" % (self.name, i)] = instance_definition
        return {"Resources": resources}

    def resize(self, new_capacity):
        """
        Resize the instance group to the new capacity.

        When shrinking, the newest instances will be removed.
        """
        new_template = self._create_template(new_capacity)
        result = self.update_with_template(new_template, {})
        for resource in self.nested():
            if resource.state == ('CREATE', 'FAILED'):
                resource.destroy()
        self._lb_reload()
        return result

    def _lb_reload(self):
        '''
        Notify the LoadBalancer to reload its config to include
        the changes in instances we have just made.

        This must be done after activation (instance in ACTIVE state),
        otherwise the instances' IP addresses may not be available.
        '''
        if self.properties['LoadBalancerNames']:
            id_list = [inst.FnGetRefId() for inst in self.get_instances()]
            for lb in self.properties['LoadBalancerNames']:
                self.stack[lb].json_snippet['Properties']['Instances'] = \
                    id_list
                resolved_snippet = self.stack.resolve_static_data(
                    self.stack[lb].json_snippet)
                self.stack[lb].update(resolved_snippet)

    def FnGetRefId(self):
        return unicode(self.name)

    def _resolve_attribute(self, name):
        '''
        heat extension: "InstanceList" returns comma delimited list of server
        ip addresses.
        '''
        if name == 'InstanceList':
            ips = [inst.FnGetAtt('PublicIp')
                   for inst in self._nested.resources.values()]
            if ips:
                return unicode(','.join(ips))
Пример #10
0
class InstanceGroup(stack_resource.StackResource):
    tags_schema = {
        'Key': {
            'Type': 'String',
            'Required': True
        },
        'Value': {
            'Type': 'String',
            'Required': True
        }
    }
    properties_schema = {
        'AvailabilityZones': {
            'Required': True,
            'Type': 'List',
            'Description': _('Not Implemented.')
        },
        'LaunchConfigurationName': {
            'Required': True,
            'Type': 'String',
            'UpdateAllowed': True,
            'Description': _('Name of LaunchConfiguration resource.')
        },
        'Size': {
            'Required': True,
            'Type': 'Number',
            'UpdateAllowed': True,
            'Description': _('Desired number of instances.')
        },
        'LoadBalancerNames': {
            'Type': 'List',
            'Description': _('List of LoadBalancer resources.')
        },
        'Tags': {
            'Type': 'List',
            'Schema': {
                'Type': 'Map',
                'Schema': tags_schema
            },
            'Description': _('Tags to attach to this group.')
        }
    }
    update_allowed_keys = (
        'Properties',
        'UpdatePolicy',
    )
    attributes_schema = {
        "InstanceList":
        _("A comma-delimited list of server ip addresses. "
          "(Heat extension).")
    }
    rolling_update_schema = {
        'MinInstancesInService':
        properties.Schema(properties.Schema.NUMBER, default=0),
        'MaxBatchSize':
        properties.Schema(properties.Schema.NUMBER, default=1),
        'PauseTime':
        properties.Schema(properties.Schema.STRING, default='PT0S')
    }
    update_policy_schema = {
        'RollingUpdate':
        properties.Schema(properties.Schema.MAP, schema=rolling_update_schema)
    }

    def __init__(self, name, json_snippet, stack):
        """
        UpdatePolicy is currently only specific to InstanceGroup and
        AutoScalingGroup. Therefore, init is overridden to parse for the
        UpdatePolicy.
        """
        super(InstanceGroup, self).__init__(name, json_snippet, stack)
        self.update_policy = Properties(self.update_policy_schema,
                                        self.t.get('UpdatePolicy', {}),
                                        parent_name=self.name)

    def validate(self):
        """
        Add validation for update_policy
        """
        super(InstanceGroup, self).validate()

        if self.update_policy:
            self.update_policy.validate()
            policy_name = self.update_policy_schema.keys()[0]
            if self.update_policy[policy_name]:
                pause_time = self.update_policy[policy_name]['PauseTime']
                if iso8601utils.parse_isoduration(pause_time) > 3600:
                    raise ValueError('Maximum PauseTime is 1 hour.')

    def get_instance_names(self):
        """Get a list of resource names of the instances in this InstanceGroup.

        Failed resources will be ignored.
        """
        return [r.name for r in self.get_instances()]

    def get_instances(self):
        """Get a list of all the instance resources managed by this group.

        Sort the list of instances first by created_time then by name.
        """
        resources = []
        if self.nested():
            resources = [
                resource for resource in self.nested().itervalues()
                if resource.status != resource.FAILED
            ]
        return sorted(resources, key=lambda r: (r.created_time, r.name))

    def handle_create(self):
        """Create a nested stack and add the initial resources to it."""
        num_instances = int(self.properties['Size'])
        initial_template = self._create_template(num_instances)
        return self.create_with_template(initial_template, {})

    def check_create_complete(self, task):
        """
        When stack creation is done, update the load balancer.

        If any instances failed to be created, delete them.
        """
        done = super(InstanceGroup, self).check_create_complete(task)
        if done:
            self._lb_reload()
        return done

    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 tmpl_diff:
            # parse update policy
            if 'UpdatePolicy' in tmpl_diff:
                self.update_policy = Properties(self.update_policy_schema,
                                                json_snippet.get(
                                                    'UpdatePolicy', {}),
                                                parent_name=self.name)

        if prop_diff:
            self.properties = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         self.stack.resolve_runtime_data,
                                         self.name)

            # Replace instances first if launch configuration has changed
            if (self.update_policy['RollingUpdate']
                    and 'LaunchConfigurationName' in prop_diff):
                policy = self.update_policy['RollingUpdate']
                self._replace(int(policy['MinInstancesInService']),
                              int(policy['MaxBatchSize']), policy['PauseTime'])

            # Get the current capacity, we may need to adjust if
            # Size has changed
            if 'Size' in prop_diff:
                inst_list = self.get_instances()
                if len(inst_list) != int(self.properties['Size']):
                    self.resize(int(self.properties['Size']))

    def _tags(self):
        """
        Make sure that we add a tag that Ceilometer can pick up.
        These need to be prepended with 'metering.'.
        """
        tags = self.properties.get('Tags') or []
        for t in tags:
            if t['Key'].startswith('metering.'):
                # the user has added one, don't add another.
                return tags
        return tags + [{
            'Key': 'metering.groupname',
            'Value': self.FnGetRefId()
        }]

    def handle_delete(self):
        return self.delete_nested()

    def _get_instance_definition(self):
        conf_name = self.properties['LaunchConfigurationName']
        conf = self.stack.resource_by_refid(conf_name)
        instance_definition = copy.deepcopy(conf.t)
        instance_definition['Type'] = 'AWS::EC2::Instance'
        instance_definition['Properties']['Tags'] = self._tags()
        if self.properties.get('VPCZoneIdentifier'):
            instance_definition['Properties']['SubnetId'] = \
                self.properties['VPCZoneIdentifier'][0]
        # resolve references within the context of this stack.
        return self.stack.resolve_runtime_data(instance_definition)

    def _create_template(self, num_instances, num_replace=0):
        """
        Create the template for the nested stack of existing and new instances

        For rolling update, if launch configuration is different, the
        instance definition should come from the existing instance instead
        of using the new launch configuration.
        """
        instances = self.get_instances()[-num_instances:]
        instance_definition = self._get_instance_definition()
        num_create = num_instances - len(instances)
        num_replace -= num_create

        def instance_templates(num_replace):
            for i in range(num_instances):
                if i < len(instances):
                    inst = instances[i]
                    if inst.t != instance_definition and num_replace > 0:
                        num_replace -= 1
                        yield inst.name, instance_definition
                    else:
                        yield inst.name, inst.t
                else:
                    yield short_id.generate_id(), instance_definition

        return {"Resources": dict(instance_templates(num_replace))}

    def _replace(self, min_in_service, batch_size, pause_time):
        """
        Replace the instances in the group using updated launch configuration
        """
        def changing_instances(tmpl):
            instances = self.get_instances()
            current = set((i.name, str(i.t)) for i in instances)
            updated = set((k, str(v)) for k, v in tmpl['Resources'].items())
            # includes instances to be updated and deleted
            affected = set(k for k, v in current ^ updated)
            return set(i.FnGetRefId() for i in instances if i.name in affected)

        def pause_between_batch():
            while True:
                try:
                    yield
                except scheduler.Timeout:
                    return

        capacity = len(self.nested()) if self.nested() else 0
        efft_bat_sz = min(batch_size, capacity)
        efft_min_sz = min(min_in_service, capacity)
        pause_sec = iso8601utils.parse_isoduration(pause_time)

        batch_cnt = (capacity + efft_bat_sz - 1) // efft_bat_sz
        if pause_sec * (batch_cnt - 1) >= self.stack.timeout_mins * 60:
            raise ValueError('The current UpdatePolicy will result '
                             'in stack update timeout.')

        # effective capacity includes temporary capacity added to accomodate
        # the minimum number of instances in service during update
        efft_capacity = max(capacity - efft_bat_sz, efft_min_sz) + efft_bat_sz

        try:
            remainder = capacity
            while remainder > 0 or efft_capacity > capacity:
                if capacity - remainder >= efft_min_sz:
                    efft_capacity = capacity
                template = self._create_template(efft_capacity, efft_bat_sz)
                self._lb_reload(exclude=changing_instances(template))
                updater = self.update_with_template(template, {})
                updater.run_to_completion()
                self.check_update_complete(updater)
                remainder -= efft_bat_sz
                if remainder > 0 and pause_sec > 0:
                    self._lb_reload()
                    waiter = scheduler.TaskRunner(pause_between_batch)
                    waiter(timeout=pause_sec)
        finally:
            self._lb_reload()

    def resize(self, new_capacity):
        """
        Resize the instance group to the new capacity.

        When shrinking, the oldest instances will be removed.
        """
        new_template = self._create_template(new_capacity)
        try:
            updater = self.update_with_template(new_template, {})
            updater.run_to_completion()
            self.check_update_complete(updater)
        finally:
            # Reload the LB in any case, so it's only pointing at healthy
            # nodes.
            self._lb_reload()

    def _lb_reload(self, exclude=[]):
        '''
        Notify the LoadBalancer to reload its config to include
        the changes in instances we have just made.

        This must be done after activation (instance in ACTIVE state),
        otherwise the instances' IP addresses may not be available.
        '''
        if self.properties['LoadBalancerNames']:
            id_list = [
                inst.FnGetRefId() for inst in self.get_instances()
                if inst.FnGetRefId() not in exclude
            ]
            for lb in self.properties['LoadBalancerNames']:
                lb_resource = self.stack[lb]
                if 'Instances' in lb_resource.properties_schema:
                    lb_resource.json_snippet['Properties']['Instances'] = (
                        id_list)
                elif 'members' in lb_resource.properties_schema:
                    lb_resource.json_snippet['Properties']['members'] = (
                        id_list)
                else:
                    raise exception.Error(
                        "Unsupported resource '%s' in LoadBalancerNames" %
                        (lb, ))
                resolved_snippet = self.stack.resolve_static_data(
                    lb_resource.json_snippet)
                scheduler.TaskRunner(lb_resource.update, resolved_snippet)()

    def FnGetRefId(self):
        return self.physical_resource_name()

    def _resolve_attribute(self, name):
        '''
        heat extension: "InstanceList" returns comma delimited list of server
        ip addresses.
        '''
        if name == 'InstanceList':
            return u','.join(
                inst.FnGetAtt('PublicIp')
                for inst in self.get_instances()) or None
Пример #11
0
class InstanceGroup(stack_resource.StackResource):
    tags_schema = {'Key': {'Type': 'String',
                           'Required': True},
                   'Value': {'Type': 'String',
                             'Required': True}}
    properties_schema = {
        'AvailabilityZones': {'Required': True,
                              'Type': 'List'},
        'LaunchConfigurationName': {'Required': True,
                                    'Type': 'String'},
        'Size': {'Required': True,
                 'Type': 'Number'},
        'LoadBalancerNames': {'Type': 'List'},
        'Tags': {'Type': 'List',
                 'Schema': {'Type': 'Map',
                            'Schema': tags_schema}}
    }
    update_allowed_keys = ('Properties',)
    update_allowed_properties = ('Size', 'LaunchConfigurationName',)
    attributes_schema = {
        "InstanceList": ("A comma-delimited list of server ip addresses. "
                         "(Heat extension)")
    }

    def get_instance_names(self):
        """Get a list of resource names of the instances in this InstanceGroup.

        Deleted resources will be ignored.
        """
        return sorted(x.name for x in self.get_instances())

    def get_instances(self):
        """Get a set of all the instance resources managed by this group."""
        return [resource for resource in self.nested()
                if resource.state[0] != resource.DELETE]

    def handle_create(self):
        """Create a nested stack and add the initial resources to it."""
        num_instances = int(self.properties['Size'])
        initial_template = self._create_template(num_instances)
        return self.create_with_template(initial_template, {})

    def check_create_complete(self, task):
        """
        When stack creation is done, update the load balancer.

        If any instances failed to be created, delete them.
        """
        try:
            done = super(InstanceGroup, self).check_create_complete(task)
        except exception.Error as exc:
            for resource in self.nested():
                if resource.state == ('CREATE', 'FAILED'):
                    resource.destroy()
            raise
        if done and len(self.get_instances()):
            self._lb_reload()
        return done

    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 = Properties(self.properties_schema,
                                         json_snippet.get('Properties', {}),
                                         self.stack.resolve_runtime_data,
                                         self.name)

            # Get the current capacity, we may need to adjust if
            # Size has changed
            if 'Size' in prop_diff:
                inst_list = self.get_instances()
                if len(inst_list) != int(self.properties['Size']):
                    self.resize(int(self.properties['Size']))

    def _tags(self):
        """
        Make sure that we add a tag that Ceilometer can pick up.
        These need to be prepended with 'metering.'.
        """
        tags = self.properties.get('Tags') or []
        for t in tags:
            if t['Key'].startswith('metering.'):
                # the user has added one, don't add another.
                return tags
        return tags + [{'Key': 'metering.groupname',
                        'Value': self.FnGetRefId()}]

    def handle_delete(self):
        return self.delete_nested()

    def _create_template(self, num_instances):
        """
        Create a template with a number of instance definitions based on the
        launch configuration.
        """
        conf_name = self.properties['LaunchConfigurationName']
        conf = self.stack.resource_by_refid(conf_name)
        instance_definition = copy.deepcopy(conf.t)
        instance_definition['Type'] = 'AWS::EC2::Instance'
        instance_definition['Properties']['Tags'] = self._tags()
        # resolve references within the context of this stack.
        fully_parsed = self.stack.resolve_runtime_data(instance_definition)

        resources = {}
        for i in range(num_instances):
            resources["%s-%d" % (self.name, i)] = fully_parsed
        return {"Resources": resources}

    def resize(self, new_capacity):
        """
        Resize the instance group to the new capacity.

        When shrinking, the newest instances will be removed.
        """
        new_template = self._create_template(new_capacity)
        result = self.update_with_template(new_template, {})
        for resource in self.nested():
            if resource.state == ('CREATE', 'FAILED'):
                resource.destroy()
        self._lb_reload()
        return result

    def _lb_reload(self):
        '''
        Notify the LoadBalancer to reload its config to include
        the changes in instances we have just made.

        This must be done after activation (instance in ACTIVE state),
        otherwise the instances' IP addresses may not be available.
        '''
        if self.properties['LoadBalancerNames']:
            id_list = [inst.FnGetRefId() for inst in self.get_instances()]
            for lb in self.properties['LoadBalancerNames']:
                self.stack[lb].json_snippet['Properties']['Instances'] = \
                    id_list
                resolved_snippet = self.stack.resolve_static_data(
                    self.stack[lb].json_snippet)
                self.stack[lb].update(resolved_snippet)

    def FnGetRefId(self):
        return unicode(self.name)

    def _resolve_attribute(self, name):
        '''
        heat extension: "InstanceList" returns comma delimited list of server
        ip addresses.
        '''
        if name == 'InstanceList':
            ips = [inst.FnGetAtt('PublicIp')
                   for inst in self._nested.resources.values()]
            if ips:
                return unicode(','.join(ips))