diff --git a/heat/engine/autoscaling.py b/heat/engine/autoscaling.py new file mode 100644 index 0000000000..212487904d --- /dev/null +++ b/heat/engine/autoscaling.py @@ -0,0 +1,148 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import eventlet +import logging +import json +import os + +from heat.common import exception +from heat.db import api as db_api +from heat.engine import instance +from heat.engine.resources import Resource + +logger = logging.getLogger('heat.engine.autoscaling') + + +class AutoScalingGroup(Resource): + 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': 'String', + 'Implemented': False}, + 'HealthCheckGracePeriod': {'Type': 'Integer', + 'Implemented': False}, + 'HealthCheckType': {'Type': 'String', + 'AllowedValues': ['EC2', 'ELB'], + 'Implemented': False}, + 'LoadBalancerNames': {'Type': 'List'}, + 'Tags': {'Type': 'List', + 'Schema': tags_schema} + } + + def __init__(self, name, json_snippet, stack): + super(AutoScalingGroup, self).__init__(name, json_snippet, stack) + # instance_id is a list of resources + + def handle_create(self): + self.adjust(int(self.properties['MinSize']), + adjustment_type='ExactCapacity') + + def handle_delete(self): + if self.instance_id is not None: + conf = self.properties['LaunchConfigurationName'] + inst_list = self.instance_id.split(',') + logger.debug('handle_delete %s' % str(inst_list)) + for victim in inst_list: + logger.debug('handle_delete %s' % victim) + inst = instance.Instance(victim, + self.stack.t['Resources'][conf], + self.stack) + inst.destroy() + + def adjust(self, adjustment, adjustment_type='ChangeInCapacity'): + self.calculate_properties() + + inst_list = [] + if self.instance_id is not None: + inst_list = sorted(self.instance_id.split(',')) + + capacity = len(inst_list) + 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: + return + + conf = self.properties['LaunchConfigurationName'] + if new_capacity > capacity: + # grow + for x in range(capacity, new_capacity): + inst = instance.Instance('%s-%d' % (self.name, x), + self.stack.t['Resources'][conf], + self.stack) + inst_list.append('%s-%d' % (self.name, x)) + self.instance_id_set(','.join(inst_list)) + inst.create() + else: + # shrink (kill largest numbered first) + del_list = inst_list[:] + for victim in reversed(del_list): + inst = instance.Instance(victim, + self.stack.t['Resources'][conf], + self.stack) + inst.destroy() + inst_list.remove(victim) + self.instance_id_set(','.join(inst_list)) + + +class LaunchConfiguration(Resource): + tags_schema = {'Key': {'Type': 'String', + 'Required': True}, + 'Value': {'Type': 'String', + 'Required': True}} + properties_schema = { + 'ImageId': {'Type': 'String', + 'Required': True}, + 'InstanceType': {'Type': 'String', + 'Required': True}, + 'KeyName': {'Type': 'String'}, + 'UserData': {'Type': 'String'}, + 'SecurityGroups': {'Type': 'String'}, + 'KernelId': {'Type': 'String', + 'Implemented': False}, + 'RamDiskId': {'Type': 'String', + 'Implemented': False}, + 'BlockDeviceMappings': {'Type': 'String', + 'Implemented': False}, + 'NovaSchedulerHints': {'Type': 'List', + 'Schema': tags_schema}, + } + + def __init__(self, name, json_snippet, stack): + super(LaunchConfiguration, self).__init__(name, json_snippet, stack) diff --git a/heat/engine/resource_types.py b/heat/engine/resource_types.py index dbd8fd2b37..7c2371102b 100644 --- a/heat/engine/resource_types.py +++ b/heat/engine/resource_types.py @@ -20,6 +20,7 @@ Register of resource types and their mapping to Resource classes. from heat.engine import resources +from heat.engine import autoscaling from heat.engine import cloud_watch from heat.engine import eip from heat.engine import instance @@ -47,6 +48,8 @@ _resource_classes = { 'AWS::IAM::User': user.User, 'AWS::IAM::AccessKey': user.AccessKey, 'HEAT::HA::Restarter': instance.Restarter, + 'AWS::AutoScaling::LaunchConfiguration': autoscaling.LaunchConfiguration, + 'AWS::AutoScaling::AutoScalingGroup': autoscaling.AutoScalingGroup, } diff --git a/heat/engine/resources.py b/heat/engine/resources.py index 39f4ebfc79..381ac1381d 100644 --- a/heat/engine/resources.py +++ b/heat/engine/resources.py @@ -214,6 +214,12 @@ class Resource(object): def instance_id_set(self, inst): self.instance_id = inst + if self.id is not None: + try: + rs = db_api.resource_get(self.stack.context, self.id) + rs.update_and_save({'nova_instance': self.instance_id}) + except Exception as ex: + logger.warn('db error %s' % str(ex)) def _create_db(self, metadata=None): '''Create the resource in the database''' diff --git a/templates/HAProxy_Single_Instance.template b/templates/HAProxy_Single_Instance.template index 47f6d8efbc..25f6400379 100644 --- a/templates/HAProxy_Single_Instance.template +++ b/templates/HAProxy_Single_Instance.template @@ -75,8 +75,6 @@ "ImageId": "F16-x86_64-cfntools", "InstanceType": { "Ref": "InstanceType" }, "KeyName": { "Ref": "KeyName" }, - "Tags": [ {"Key": "Part", "Value": "long"}, - {"Key": "ready", "Value": "short"} ], "UserData": { "Fn::Base64": { "Fn::Join": ["", [ "#!/bin/bash -v\n", "/opt/aws/bin/cfn-init -s ", diff --git a/templates/ppetit.template b/templates/ppetit.template new file mode 100644 index 0000000000..95f4c3b9be --- /dev/null +++ b/templates/ppetit.template @@ -0,0 +1,57 @@ +{ + "AWSTemplateFormatVersion" : "2010-09-09", + + "Description" : "Template to create multiple instances.", + + "Parameters" : { + "KeyName" : { + "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances", + "Type" : "String" + }, + "InstanceType" : { + "Description" : "Instance type", + "Type" : "String", + "Default" : "m1.small", + "AllowedValues" : [ "t1.micro", "m1.small", "m1.large", "m1.xlarge", "m2.xlarge", "m2.2xlarge", "m2.4xlarge", "c1.medium", "c1.xlarge", "cc1.4xlarge" ], + "ConstraintDescription" : "must be a valid EC2 instance type." + }, + "ImageId" : { + "Description" : "Name of the image to use", + "Type" : "String", + "Default" : "F16-x86_64-cfntools" + }, + "NumInstances": { + "Default": "1", + "MinValue": "1", + "MaxValue": "100", + "Description" : "Number of instances to create", + "Type": "Integer" + } + }, + + "Resources" : { + "JobServerGroup" : { + "Type" : "AWS::AutoScaling::AutoScalingGroup", + "Properties" : { + "LaunchConfigurationName" : { "Ref" : "JobServerConfig" }, + "MinSize" : {"Ref": "NumInstances"}, + "MaxSize" : {"Ref": "NumInstances"}, + "AvailabilityZones" : { "Fn::GetAZs" : "" } + } + }, + + "JobServerConfig" : { + "Type" : "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId" : { "Ref" : "ImageId" }, + "InstanceType" : { "Ref" : "InstanceType" }, + "KeyName" : { "Ref" : "KeyName" }, + "NovaSchedulerHints": [ {"Key": "part", "Value": "long"}, + {"Key": "ready", "Value": "short"} ], + "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [ + "#!/bin/bash -v\n" + ]]}} + } + } + } +}