diff --git a/lib/fog/aws.rb b/lib/fog/aws.rb index b458c0360d..49f3b70195 100644 --- a/lib/fog/aws.rb +++ b/lib/fog/aws.rb @@ -89,6 +89,113 @@ module AWS service(:sts, 'STS') service(:support, 'Support') + # Transforms hash keys according to the passed mapping or given block + # + # ==== Parameters + # * object<~Hash> - A hash to apply transformation to + # * mappings<~Hash> - A hash of mappings for keys. Keys of the mappings object should match desired keys of the + # object which will be transformed. If object contains values that are hashes or arrays of hashes which should + # be transformed as well mappings object's value should be configured with a specific form, for example: + # { + # :key => { + # 'Key' => { + # :nested_key => 'NestedKey' + # } + # } + # } + # This form will transform object's key :key to 'Key' + # and transform corresponding value: if it's a hash then its key :nested_key will be transformed into 'NestedKey' + # if it's an array of hashes, each hash element of the array will have it's key :nested_key transformed into 'NestedKey' + # * block<~Proc> - block which is applied if mappings object does not contain key. Block receives key as it's argument + # and should return new key as the one that will be used to replace the original one + # ==== Returns + # * object<~Hash> - hash containing transformed keys + # + def self.map_keys(object, mappings = nil, &block) + case object + when ::Hash + object.reduce({}) do |acc, (key, val)| + mapping = mappings[key] if mappings + new_key, new_value = begin + if mapping + case mapping + when ::Hash + mapped_key = mapping.keys[0] + [mapped_key, map_keys(val, mapping[mapped_key], &block)] + else + [mapping, map_keys(val, &block)] + end + else + mapped_value = map_keys(val, &block) + if block_given? + [block.call(key), mapped_value] + else + [key, mapped_value] + end + end + end + acc[new_key] = new_value + acc + end + when ::Array + object.map { |item| map_keys(item, mappings, &block) } + else + object + end + end + + # Maps object keys to aws compatible: underscore keys are transformed in camel case strings + # with capitalized first word (aka :ab_cd transforms into AbCd). Already compatible keys remain the same + # + # ==== Parameters + # * object<~Hash> - A hash to apply transformation to + # * mappings<~Hash> - An optional hash of mappings for keys + # ==== Returns + # * object<~Hash> - hash containing transformed keys + # + def self.map_to_aws(object, mappings = nil) + map_keys(object, mappings) do |key| + words = key.to_s.split('_') + if words.length > 1 + words.collect(&:capitalize).join + else + words[0].split(/(?=[A-Z])/).collect(&:capitalize).join + end + end + end + + # Maps object keys from aws to ruby compatible form aka snake case symbols + # + # ==== Parameters + # * object<~Hash> - A hash to apply transformation to + # * mappings<~Hash> - An optional hash of mappings for keys + # ==== Returns + # * object<~Hash> - hash containing transformed keys + # + def self.map_from_aws(object, mappings = nil) + map_keys(object, mappings) { |key| key.to_s.split(/(?=[A-Z])/).join('_').downcase.to_sym } + end + + # Helper function to invert mappings: recursively replaces mapping keys and values. + # + # ==== Parameters + # * mappings<~Hash> - mappings hash + # ==== Returns + # * object<~Hash> - inverted mappings + # + def self.invert_mappings(mappings) + mappings.reduce({}) do |acc, (key, val)| + mapped_key, mapped_value = case val + when ::Hash + [val.keys[0], val.values[0]] + else + [val, nil] + end + acc[mapped_key] = mapped_value ? { key => invert_mappings(mapped_value) } : key + acc + end + end + def self.indexed_param(key, values) params = {} unless key.include?('%d') diff --git a/lib/fog/aws/models/auto_scaling/group.rb b/lib/fog/aws/models/auto_scaling/group.rb index 819b176e03..483f641746 100644 --- a/lib/fog/aws/models/auto_scaling/group.rb +++ b/lib/fog/aws/models/auto_scaling/group.rb @@ -99,6 +99,10 @@ def enable_metrics_collection(granularity = '1Minute', metrics = {}) reload end + def policies + service.policies.all('AutoScalingGroupName' => id) + end + def instances Fog::AWS::AutoScaling::Instances.new(:service => service).load(attributes[:instances]) end diff --git a/lib/fog/aws/models/auto_scaling/policy.rb b/lib/fog/aws/models/auto_scaling/policy.rb index e620aa5ae2..7ff026a947 100644 --- a/lib/fog/aws/models/auto_scaling/policy.rb +++ b/lib/fog/aws/models/auto_scaling/policy.rb @@ -2,34 +2,103 @@ module Fog module AWS class AutoScaling class Policy < Fog::Model - identity :id, :aliases => 'PolicyName' - attribute :arn, :aliases => 'PolicyARN' - attribute :adjustment_type, :aliases => 'AdjustmentType' - attribute :alarms, :aliases => 'Alarms' - attribute :auto_scaling_group_name, :aliases => 'AutoScalingGroupName' - attribute :cooldown, :aliases => 'Cooldown' - attribute :min_adjustment_step, :aliases => 'MinAdjustmentStep' - attribute :scaling_adjustment, :aliases => 'ScalingAdjustment' + identity :id, :aliases => 'PolicyName' + attribute :arn, :aliases => 'PolicyARN' + attribute :type, :aliases => 'PolicyType' + attribute :adjustment_type, :aliases => 'AdjustmentType' + attribute :scaling_adjustment, :aliases => 'ScalingAdjustment' + attribute :step_adjustments, :aliases => 'StepAdjustments' + attribute :target_tracking_configuration, :aliases => 'TargetTrackingConfiguration' + attribute :alarms, :aliases => 'Alarms' + attribute :auto_scaling_group_name, :aliases => 'AutoScalingGroupName' + attribute :cooldown, :aliases => 'Cooldown' + attribute :estimated_instance_warmup, :aliases => 'EstimatedInstanceWarmup' + attribute :metric_aggregation_type, :aliases => 'MetricAggregationType' + attribute :min_adjustment_magnitude, :aliases => 'MinAdjustmentMagnitude' + attribute :min_adjustment_step, :aliases => 'MinAdjustmentStep' + + STEP_ADJUSTMENTS_MAPPING = { + :metric_interval_lower_bound => 'MetricIntervalLowerBound', + :metric_interval_upper_bound => 'MetricIntervalUpperBound', + :scaling_adjustment => 'ScalingAdjustment' + }.freeze + + TARGET_TRACKING_MAPPING = { + :customized_metric_specification => { + 'CustomizedMetricSpecification' => { + :metric_name => 'MetricName', + :namespace => 'Namespace', + :statistics => 'Statistics', + :unit => 'Unit', + :dimensions => { + 'Dimensions' => { + :name => 'Name', + :value => 'Value' + } + } + } + }, + :disable_scale_in => 'DisableScaleIn', + :target_value => 'TargetValue', + :predefined_metric_specification => { + 'PredefinedMetricSpecification' => { + :predefined_metric_type => 'PredefinedMetricType', + :resource_label => 'ResourceLabel' + } + } + }.freeze + + # Returns attribute names specific for different policy types + # + # ==== Parameters + # * policy_type<~String> - type of the auto scaling policy + # + # ==== Returns + # * options<~Array> Array of string containing policy specific options + # + def self.preserve_options(policy_type) + case policy_type + when 'StepScaling' + %w(EstimatedInstanceWarmup PolicyType MinAdjustmentMagnitude MetricAggregationType AdjustmentType StepAdjustments) + when 'TargetTrackingScaling' + %w(EstimatedInstanceWarmup PolicyType TargetTrackingConfiguration) + else + %w(AdjustmentType ScalingAdjustment PolicyType Cooldown MinAdjustmentMagnitude MinAdjustmentStep) + end + end def initialize(attributes) - attributes['AdjustmentType'] ||= 'ChangeInCapacity' - attributes['ScalingAdjustment'] ||= 1 super + case self.type + when 'StepScaling' + prepare_step_policy + when 'TargetTrackingScaling' + prepare_target_policy + else + prepare_simple_policy + end end # TODO: implement #alarms - # TODO: implement #auto_scaling_group + + def auto_scaling_group + service.groups.get(self.auto_scaling_group_name) + end def save - requires :id - requires :adjustment_type - requires :auto_scaling_group_name - requires :scaling_adjustment + type_requirements options = Hash[self.class.aliases.map { |key, value| [key, send(value)] }] - options.delete_if { |key, value| value.nil? } + if options['TargetTrackingConfiguration'] + options['TargetTrackingConfiguration'] = Fog::AWS.map_to_aws(options['TargetTrackingConfiguration'], TARGET_TRACKING_MAPPING) + end + if options['StepAdjustments'] + options['StepAdjustments'] = Fog::AWS.map_to_aws(options['StepAdjustments'], STEP_ADJUSTMENTS_MAPPING) + end + options_keys = self.class.preserve_options(self.type) + options.delete_if { |key, value| value.nil? || !options_keys.include?(key) } - service.put_scaling_policy(adjustment_type, auto_scaling_group_name, id, scaling_adjustment, options) + service.put_scaling_policy(auto_scaling_group_name, id, options) reload end @@ -38,6 +107,41 @@ def destroy requires :auto_scaling_group_name service.delete_policy(auto_scaling_group_name, id) end + + private + + def prepare_simple_policy + self.adjustment_type ||= 'ChangeInCapacity' + self.scaling_adjustment ||= 1 + end + + def prepare_target_policy + # do we need default tracking configuration or should we just allow it to fail? + if target_tracking_configuration + self.target_tracking_configuration = Fog::AWS.map_from_aws(target_tracking_configuration, TARGET_TRACKING_MAPPING) + end + end + + def prepare_step_policy + # do we need any default scaling steps or should we just allow it to fail? + self.adjustment_type ||= 'ChangeInCapacity' + if step_adjustments + self.step_adjustments = Fog::AWS.map_from_aws(step_adjustments, STEP_ADJUSTMENTS_MAPPING) + end + end + + def type_requirements + requires :id + requires :auto_scaling_group_name + case self.type + when 'StepScaling' + requires :step_adjustments + when 'TargetTrackingScaling' + requires :target_tracking_configuration + else + requires :scaling_adjustment + end + end end end end diff --git a/lib/fog/aws/parsers/auto_scaling/describe_policies.rb b/lib/fog/aws/parsers/auto_scaling/describe_policies.rb index a8357c5e7a..b9bbde1aca 100644 --- a/lib/fog/aws/parsers/auto_scaling/describe_policies.rb +++ b/lib/fog/aws/parsers/auto_scaling/describe_policies.rb @@ -3,20 +3,28 @@ module Parsers module AWS module AutoScaling class DescribePolicies < Fog::Parsers::Base + + NESTING_MEMBERS = %w(scaling_policy alarm dimension target_tracking step_adjustment custom_spec predefined_spec).freeze + def reset - reset_scaling_policy - reset_alarm + NESTING_MEMBERS.each do |obj| + public_send(:"reset_#{obj}") + end @results = { 'ScalingPolicies' => [] } @response = { 'DescribePoliciesResult' => {}, 'ResponseMetadata' => {} } - @in_alarms = false + (NESTING_MEMBERS - ['scaling_policy']).each do |member| + instance_variable_set(:"@in_#{member}", false) + end end - def reset_scaling_policy - @scaling_policy = { 'Alarms' => [] } + (NESTING_MEMBERS - ['scaling_policy']).each do |obj| + define_method(:"reset_#{obj}") do + instance_variable_set(:"@#{obj}", {}) + end end - def reset_alarm - @alarm = {} + def reset_scaling_policy + @scaling_policy = { 'Alarms' => [], 'StepAdjustments' => [] } end def start_element(name, attrs = []) @@ -24,6 +32,16 @@ def start_element(name, attrs = []) case name when 'Alarms' @in_alarms = true + when 'TargetTrackingConfiguration' + @in_target_tracking = true + when 'StepAdjustments' + @in_step_adjustments = true + when 'Dimensions' + @in_dimensions = true + when 'CustomizedMetricSpecification' + @in_custom_spec = true + when 'PredefinedMetricSpecification' + @in_predefined_spec = true end end @@ -31,27 +49,63 @@ def end_element(name) case name when 'AlarmARN', 'AlarmName' @alarm[name] = value - - when 'AdjustmentType', 'AutoScalingGroupName', 'PolicyARN', 'PolicyName' + when 'MetricName', 'Statistics', 'Unit', 'Namespace' + @custom_spec[name] = value + when 'Name', 'Value' + @dimension[name] = value + when 'AdjustmentType', 'AutoScalingGroupName', 'MetricAggregationType', 'PolicyARN', 'PolicyName', 'PolicyType' @scaling_policy[name] = value - when 'Cooldown', 'MinAdjustmentStep', 'ScalingAdjustment' + when 'PredefinedMetricType', 'ResourceLabel' + @predefined_spec[name] = value + when 'Cooldown', 'MinAdjustmentStep', 'MinAdjustmentMagnitude', 'EstimatedInstanceWarmup' @scaling_policy[name] = value.to_i - + when 'DisableScaleIn' + @target_tracking[name] = (value == 'true') + when 'TargetValue' + @target_tracking[name] = value.to_f + when 'MetricIntervalLowerBound', 'MetricIntervalUpperBound' + @step_adjustment[name] = value.nil? ? value : value.to_f + when 'ScalingAdjustment' + if @in_step_adjustments + @step_adjustment[name] = value.to_i + else + @scaling_policy[name] = value.to_i + end when 'NextToken' @results[name] = value - when 'RequestId' @response['ResponseMetadata'][name] = value - when 'DescribePoliciesResponse' @response['DescribePoliciesResult'] = @results - when 'Alarms' @in_alarms = false + when 'TargetTrackingConfiguration' + @in_target_tracking = false + @scaling_policy['TargetTrackingConfiguration'] = @target_tracking + reset_target_tracking + when 'StepAdjustments' + @in_step_adjustments = false + when 'Dimensions' + @in_dimensions = false + when 'CustomizedMetricSpecification' + @in_custom_spec = false + @target_tracking['CustomizedMetricSpecification'] = @custom_spec + reset_custom_spec + when 'PredefinedMetricSpecification' + @in_predefined_spec = false + @target_tracking['PredefinedMetricSpecification'] = @predefined_spec + reset_predefined_spec when 'member' if @in_alarms @scaling_policy['Alarms'] << @alarm reset_alarm + elsif @in_step_adjustments + @scaling_policy['StepAdjustments'] << @step_adjustment + reset_step_adjustment + elsif @in_dimensions + @custom_spec['Dimensions'] ||= [] + @custom_spec['Dimensions'] << @dimension + reset_dimension else @results['ScalingPolicies'] << @scaling_policy reset_scaling_policy diff --git a/lib/fog/aws/requests/auto_scaling/describe_policies.rb b/lib/fog/aws/requests/auto_scaling/describe_policies.rb index c8fa2822cb..e7726d3e28 100644 --- a/lib/fog/aws/requests/auto_scaling/describe_policies.rb +++ b/lib/fog/aws/requests/auto_scaling/describe_policies.rb @@ -17,6 +17,10 @@ class Real # be described with each call. # * 'NextToken'<~String> - The token returned by a previous call to # indicate that there is more data available. + # * PolicyTypes<~Array> - A list of policy types to be + # described. If this list is omitted, policies of all types are + # described. If an auto scaling group name is provided, the results + # are limited to that group. Valid values are SimpleScaling and StepScaling # * PolicyNames<~Array> - A list of policy names or policy ARNs to be # described. If this list is omitted, all policy names are # described. If an auto scaling group name is provided, the results @@ -31,25 +35,78 @@ class Real # * 'RequestId'<~String> - Id of request # * 'DescribePoliciesResult'<~Hash>: # * 'ScalingPolicies'<~Array>: - # * 'AdjustmentType'<~String> - Specifies whether the - # adjustment is an absolute number or a percentage of the - # current capacity. # * 'Alarms'<~Array>: # * 'AlarmARN'<~String> - The Amazon Resource Name (ARN) of # the alarm. # * 'AlarmName'<~String> - The name of the alarm. # * 'AutoScalingGroupName'<~String> - The name of the Auto # Scaling group associated with this scaling policy. - # * 'Cooldown'<~Integer> - The amount of time, in seconds, - # after a scaling activity completes before any further - # trigger-related scaling activities can start. # * 'PolicyARN'<~String> - The Amazon Resource Name (ARN) of # the policy. # * 'PolicyName'<~String> - The name of the scaling policy. - # * 'ScalingAdjustment'<~Integer> - The number associated with - # the specified AdjustmentType. A positive value adds to the - # current capacity and a negative value removes from the - # current capacity. + # * 'Cooldown'<~Integer> - The amount of time, in seconds, after a + # scaling activity completes before any further trigger-related + # scaling activities can start + # * 'AdjustmentType'<~String> - The valid values are ChangeInCapacity, ExactCapacity, and PercentChangeInCapacity + # The param is supported by SimpleScaling and StepScaling policy types + # * 'EstimatedInstanceWarmup'<~Integer> - The estimated time, in seconds, + # until a newly launched instance can contribute to the CloudWatch metrics. + # The default is to use the value specified for the default cooldown period for the group. + # This parameter is supported if the policy type is StepScaling or TargetTrackingScaling. + # * 'MetricAggregationType'<~String> - The aggregation type for the CloudWatch metrics. + # The valid values are Minimum, Maximum, and Average. If the aggregation type is null, the value is treated as Average. + # This parameter is supported if the policy type is StepScaling. + # * 'MinAdjustmentMagnitude'<~String> - The minimum number of instances to scale. + # If the value of AdjustmentType is PercentChangeInCapacity, + # the scaling policy changes the DesiredCapacity of the Auto Scaling group by at least this many instances. + # Otherwise, the error is ValidationError. + # This parameter is supported if the policy type is SimpleScaling or StepScaling. + # * 'MinAdjustmentStep'<~String> - This parameter has been deprecated. + # Available for backward compatibility. Use MinAdjustmentMagnitude instead. + # * 'PolicyType'<~String> - The policy type. The valid values are SimpleScaling, StepScaling, and TargetTrackingScaling. + # If the policy type is null, the value is treated as SimpleScaling. + # * 'ScalingAdjustment'<~Integer> - The amount by which to scale, based on the specified adjustment type. + # A positive value adds to the current capacity while a negative number removes from the current capacity. + # This parameter is required if the policy type is SimpleScaling and not supported otherwise. + # * 'StepAdjustments'<~Array> - A set of adjustments that enable you to scale based on the size of the alarm breach. + # This parameter is required if the policy type is StepScaling and not supported otherwise. + # Each element of the array should be of type StepAdjustment: + # * 'stepAdjustment'<~Hash>: + # * 'MetricIntervalLowerBound'<~Float> - The lower bound for the difference between the alarm threshold + # and the CloudWatch metric. + # If the metric value is above the breach threshold, the lower bound is inclusive + # (the metric must be greater than or equal to the threshold plus the lower bound). + # Otherwise, it is exclusive (the metric must be greater than the threshold plus the lower bound). + # A null value indicates negative infinity. + # * 'MetricIntervalUpperBound'<~Float> - The upper bound for the difference between the alarm threshold + # and the CloudWatch metric. If the metric value is above the breach threshold, + # the upper bound is exclusive (the metric must be less than the threshold plus the upper bound). + # Otherwise, it is inclusive (the metric must be less than or equal to the threshold plus the upper bound). + # A null value indicates positive infinity. + # The upper bound must be greater than the lower bound. + # * 'ScalingAdjustment'<~Integer> - The amount by which to scale, based on the specified adjustment type. + # A positive value adds to the current capacity while a negative number removes from the current capacity. + # * 'TargetTrackingConfiguration'<~Hash> - A target tracking policy configuration + # This parameter is required if the policy type is TargetTrackingScaling and not supported otherwise. + # * 'CustomizedMetricSpecification'<~Hash> - A customized metric. + # * 'MetricName'<~String> - The name of the metric + # * 'Namespace'<~String> - The namespace of the metric + # * 'Statistic'<~String> - The statistic of the metric. + # * 'Unit'<~String> - The unit of the metric + # * 'Dimensions'<~Array> - Array of metric dimension objects + # * 'metricDimension'<~Hash>: + # * 'Value'<~String> - The value of the dimension + # * 'Name'<~String> - The name of the dimension + # * 'DisableScaleIn'<~Boolean> - Indicates whether scale in by the target tracking policy is disabled. + # If scale in is disabled, the target tracking policy won't remove instances from the Auto Scaling group. + # Otherwise, the target tracking policy can remove instances from the Auto Scaling group. + # The default is disabled. + # * 'TargetValue'<~Float> - The target value for the metric + # * 'PredefinedMetricSpecification'<~Hash> - A predefined metric. You can specify either a predefined metric + # or a customized metric. + # * 'PredefinedMetricType'<~String> - The metric type. + # * 'ResourceLabel'<~String> - Identifies the resource associated with the metric type. + # Should be defined if predefined metric type is ALBRequestCountPerTarget # * 'NextToken'<~String> - Acts as a paging mechanism for large # result sets. Set to a non-empty string if there are # additional results waiting to be returned. Pass this in to @@ -62,6 +119,9 @@ def describe_policies(options = {}) if policy_names = options.delete('PolicyNames') options.merge!(AWS.indexed_param('PolicyNames.member.%d', [*policy_names])) end + if policy_types = options.delete('PolicyTypes') + options.merge!(AWS.indexed_param('PolicyTypes.member.%d', [*policy_types])) + end request({ 'Action' => 'DescribePolicies', :parser => Fog::Parsers::AWS::AutoScaling::DescribePolicies.new @@ -75,20 +135,25 @@ def describe_policies(options = {}) policy_set = self.data[:scaling_policies] for opt_key, opt_value in options - if opt_key == "PolicyNames" && opt_value != nil && opt_value != "" - policy_set = policy_set.reject do |asp_name, asp_data| - ![*options["PolicyNames"]].include?(asp_name) + if opt_key == 'PolicyNames' && opt_value != nil && opt_value != '' + policy_set = policy_set.reject do |asp_name, _asp_data| + ![*options['PolicyNames']].include?(asp_name) + end + elsif opt_key == 'PolicyTypes' && opt_value != nil && opt_value != '' + policy_set = policy_set.reject do |asp_name, _asp_data| + !([*options['PolicyTypes']] & %w(SimpleScaling StepScaling)).include?(asp_name) end - elsif opt_key == "AutoScalingGroupName" && opt_value != nil && opt_value != "" - policy_set = policy_set.reject do |asp_name, asp_data| - options["AutoScalingGroupName"] != asp_data["AutoScalingGroupName"] + elsif opt_key == 'AutoScalingGroupName' && opt_value != nil && opt_value != '' + policy_set = policy_set.reject do |_asp_name, asp_data| + options['AutoScalingGroupName'] != asp_data['AutoScalingGroupName'] end end end policy_set.each do |asp_name, asp_data| results['ScalingPolicies'] << { - 'PolicyName' => asp_name + 'PolicyName' => asp_name, + 'Alarms' => [] }.merge!(asp_data) end response = Excon::Response.new diff --git a/lib/fog/aws/requests/auto_scaling/put_scaling_policy.rb b/lib/fog/aws/requests/auto_scaling/put_scaling_policy.rb index 22c5663bbd..b7b210a84f 100644 --- a/lib/fog/aws/requests/auto_scaling/put_scaling_policy.rb +++ b/lib/fog/aws/requests/auto_scaling/put_scaling_policy.rb @@ -11,21 +11,75 @@ class Real # request. # # ==== Parameters - # * adjustment_type<~String> - Specifies whether the scaling_adjustment - # is an absolute number or a percentage of the current capacity. # * auto_scaling_group_name<~String> - The name or ARN of the Auto # Scaling group. # * policy_name<~String> - The name of the policy you want to create or # update. - # * scaling_adjustment<~Integer> - The number of instances by which to - # scale. AdjustmentType determines the interpretation of this number - # (e.g., as an absolute number or as a percentage of the existing - # Auto Scaling group size). A positive increment adds to the current - # capacity and a negative value removes from the current capacity. # * options<~Hash>: # * 'Cooldown'<~Integer> - The amount of time, in seconds, after a # scaling activity completes before any further trigger-related # scaling activities can start + # * 'AdjustmentType'<~String> - The valid values are ChangeInCapacity, ExactCapacity, and PercentChangeInCapacity + # The param is supported by SimpleScaling and StepScaling policy types + # * 'EstimatedInstanceWarmup'<~Integer> - The estimated time, in seconds, + # until a newly launched instance can contribute to the CloudWatch metrics. + # The default is to use the value specified for the default cooldown period for the group. + # This parameter is supported if the policy type is StepScaling or TargetTrackingScaling. + # * 'MetricAggregationType'<~String> - The aggregation type for the CloudWatch metrics. + # The valid values are Minimum, Maximum, and Average. If the aggregation type is null, the value is treated as Average. + # This parameter is supported if the policy type is StepScaling. + # * 'MinAdjustmentMagnitude'<~String> - The minimum number of instances to scale. + # If the value of AdjustmentType is PercentChangeInCapacity, + # the scaling policy changes the DesiredCapacity of the Auto Scaling group by at least this many instances. + # Otherwise, the error is ValidationError. + # This parameter is supported if the policy type is SimpleScaling or StepScaling. + # * 'MinAdjustmentStep'<~String> - This parameter has been deprecated. + # Available for backward compatibility. Use MinAdjustmentMagnitude instead. + # * 'PolicyType'<~String> - The policy type. The valid values are SimpleScaling, StepScaling, and TargetTrackingScaling. + # If the policy type is null, the value is treated as SimpleScaling. + # * 'ScalingAdjustment'<~Integer> - The amount by which to scale, based on the specified adjustment type. + # A positive value adds to the current capacity while a negative number removes from the current capacity. + # This parameter is required if the policy type is SimpleScaling and not supported otherwise. + # * 'StepAdjustments'<~Array> - A set of adjustments that enable you to scale based on the size of the alarm breach. + # This parameter is required if the policy type is StepScaling and not supported otherwise. + # Each element of the array should be of type StepAdjustment: + # * 'stepAdjustment'<~Hash>: + # * 'MetricIntervalLowerBound'<~Float> - The lower bound for the difference between the alarm threshold + # and the CloudWatch metric. + # If the metric value is above the breach threshold, the lower bound is inclusive + # (the metric must be greater than or equal to the threshold plus the lower bound). + # Otherwise, it is exclusive (the metric must be greater than the threshold plus the lower bound). + # A null value indicates negative infinity. + # * 'MetricIntervalUpperBound'<~Float> - The upper bound for the difference between the alarm threshold + # and the CloudWatch metric. If the metric value is above the breach threshold, + # the upper bound is exclusive (the metric must be less than the threshold plus the upper bound). + # Otherwise, it is inclusive (the metric must be less than or equal to the threshold plus the upper bound). + # A null value indicates positive infinity. + # The upper bound must be greater than the lower bound. + # * 'ScalingAdjustment'<~Integer> - The amount by which to scale, based on the specified adjustment type. + # A positive value adds to the current capacity while a negative number removes from the current capacity. + # * 'TargetTrackingConfiguration'<~Hash> - A target tracking policy configuration + # This parameter is required if the policy type is TargetTrackingScaling and not supported otherwise. + # * 'CustomizedMetricSpecification'<~Hash> - A customized metric. + # * 'MetricName'<~String> - The name of the metric + # * 'Namespace'<~String> - The namespace of the metric + # * 'Statistic'<~String> - The statistic of the metric. + # * 'Unit'<~String> - The unit of the metric + # * 'Dimensions'<~Array> - Array of metric dimension objects + # * 'metricDimension'<~Hash>: + # * 'Value'<~String> - The value of the dimension + # * 'Name'<~String> - The name of the dimension + # * 'DisableScaleIn'<~Boolean> - Indicates whether scale in by the target tracking policy is disabled. + # If scale in is disabled, the target tracking policy won't remove instances from the Auto Scaling group. + # Otherwise, the target tracking policy can remove instances from the Auto Scaling group. + # The default is disabled. + # * 'TargetValue'<~Float> - The target value for the metric + # * 'PredefinedMetricSpecification'<~Hash> - A predefined metric. You can specify either a predefined metric + # or a customized metric. + # * 'PredefinedMetricType'<~String> - The metric type. + # * 'ResourceLabel'<~String> - Identifies the resource associated with the metric type. + # Should be defined if predefined metric type is ALBRequestCountPerTarget + # # # ==== Returns # * response<~Excon::Response>: @@ -38,33 +92,37 @@ class Real # ==== See Also # http://docs.amazonwebservices.com/AutoScaling/latest/APIReference/API_PutScalingPolicy.html # - def put_scaling_policy(adjustment_type, auto_scaling_group_name, policy_name, scaling_adjustment, options = {}) + def put_scaling_policy(auto_scaling_group_name, policy_name, options = {}) + if steps = options.delete('StepAdjustments') + options.merge!(AWS.indexed_param('StepAdjustments.member.%d.MetricIntervalLowerBound', steps.map { |step| step['MetricIntervalLowerBound']})) + options.merge!(AWS.indexed_param('StepAdjustments.member.%d.MetricIntervalUpperBound', steps.map { |step| step['MetricIntervalUpperBound']})) + options.merge!(AWS.indexed_param('StepAdjustments.member.%d.ScalingAdjustment', steps.map { |step| step['ScalingAdjustment']})) + end request({ 'Action' => 'PutScalingPolicy', - 'AdjustmentType' => adjustment_type, 'AutoScalingGroupName' => auto_scaling_group_name, 'PolicyName' => policy_name, - 'ScalingAdjustment' => scaling_adjustment, :parser => Fog::Parsers::AWS::AutoScaling::PutScalingPolicy.new }.merge!(options)) end end class Mock - def put_scaling_policy(adjustment_type, auto_scaling_group_name, policy_name, scaling_adjustment, options = {}) + def put_scaling_policy(auto_scaling_group_name, policy_name, options = {}) unless self.data[:auto_scaling_groups].key?(auto_scaling_group_name) raise Fog::AWS::AutoScaling::ValidationError.new('Auto Scaling Group name not found - null') end - self.data[:scaling_policies][policy_name] = { - 'AdjustmentType' => adjustment_type, - 'Alarms' => [], - 'AutoScalingGroupName' => auto_scaling_group_name, - 'Cooldown' => 0, - 'MinAdjustmentStep' => 0, - 'PolicyARN' => Fog::AWS::Mock.arn('autoscaling', self.data[:owner_id], "scalingPolicy:00000000-0000-0000-0000-000000000000:autoScalingGroupName/#{auto_scaling_group_name}:policyName/#{policy_name}", self.region), - 'PolicyName' => policy_name, - 'ScalingAdjustment' => scaling_adjustment - }.merge!(options) + + data = case options[:type] + when 'StepScaling' + data_for_step_policy + when 'TargetTrackingScaling' + data_for_target_policy + else + data_for_simple_policy + end + data.merge!(common_data(auto_scaling_group_name, policy_name)) + self.data[:scaling_policies][policy_name] = data.merge!(options) response = Excon::Response.new response.status = 200 @@ -73,6 +131,54 @@ def put_scaling_policy(adjustment_type, auto_scaling_group_name, policy_name, sc } response end + + private + + def common_data(group_name, policy_name) + { + 'PolicyName' => policy_name, + 'AutoScalingGroupName' => group_name, + 'PolicyARN' => Fog::AWS::Mock.arn('autoscaling', self.data[:owner_id], "scalingPolicy:00000000-0000-0000-0000-000000000000:autoScalingGroupName/#{group_name}:policyName/#{policy_name}", self.region), + } + end + + def data_for_simple_policy + { + 'AdjustmentType' => 'ChangeInCapacity', + 'Cooldown' => 0, + 'MinAdjustmentMagnitude' => 1, + 'ScalingAdjustment' => 1 + } + end + + def data_for_step_policy + { + 'AdjustmentType' => 'ChangeInCapacity', + 'EstimatedInstanceWarmup' => 0, + 'MinAdjustmentMagnitude' => 1, + 'MetricAggregationType' => 'Average', + 'StepAdjustments' => [ + { + 'MetricIntervalLowerBound' => 0, + 'MetricIntervalUpperBound' => nil, + 'ScalingAdjustment' => 1 + } + ] + } + end + + def data_for_target_policy + { + 'EstimatedInstanceWarmup' => 0, + 'TargetTrackingConfiguration' => { + 'DisableScaleIn' => false, + 'TargetValue' => 50, + 'PredefinedMetricSpecification' => { + 'PredefinedMetricType' => 'ASGAverageCPUUtilization' + } + } + } + end end end end diff --git a/tests/aws_test.rb b/tests/aws_test.rb new file mode 100644 index 0000000000..caf63bbb11 --- /dev/null +++ b/tests/aws_test.rb @@ -0,0 +1,179 @@ +Shindo.tests('AWS', ['aws']) do + tests('map keys') do + simple_hash = { + :a => 10, + :b => 'str', + 'C' => 1.5 + } + complex_hash = { + :x => 100, + :y => 200, + :z => [{ + :a => 100, + :b => 200 + }, { + :a => 300, + :b => 400 + }], + :w => { + :x => 'str', + :y => 'str2' + } + } + tests('without mapping and block') { + returns(complex_hash) { Fog::AWS.map_keys(complex_hash) } + } + + tests('with block performing changes') { + result_hash = { + 'A' => 10, + 'B' => 'str', + 'C' => 1.5 + } + returns(result_hash) { Fog::AWS.map_keys(simple_hash) { |key| key.to_s.upcase } } + } + + tests('with mapping') { + result_hash = { + 'AAA' => 10, + 'BB' => 'str', + :c => 1.5 + } + mapping = { + :a => 'AAA', + :b => 'BB', + 'C' => :c + } + returns(result_hash) { Fog::AWS.map_keys(simple_hash, mapping) } + } + + tests('with mapping and block') { + result_hash = { + 'AAA' => 10, + 'B' => 'str', + 'C' => 1.5 + } + mapping = { + :a => 'AAA' + } + returns(result_hash) { Fog::AWS.map_keys(simple_hash, mapping) { |key| key.to_s.upcase } } + } + + tests('performs nested mapping for hashes and arrays') { + result_hash = { + 'X' => 100, + 'Y' => 200, + 'Z' => [{ + 'AAA' => 100, + :bbb => 200 + }, { + 'AAA' => 300, + :bbb => 400 + }], + 'W' => { + :X => 'str', + :Y => 'str2' + } + } + + mapping = { + :z => { + 'Z' => { + :a => 'AAA', + :b => :bbb + } + }, + :w => { + 'W' => { + :x => :X, + :y => :Y + } + } + } + + returns(result_hash) { Fog::AWS.map_keys(complex_hash, mapping) { |key| key.to_s.upcase } } + } + end + + tests('invert mapping') { + mapping = { + 'A' => { + :a => { + :b => 'B', + 'c' => 'C' + } + } + } + + result = { + :a => { + 'A' => { + 'B' => :b, + 'C' => 'c' + } + } + } + + returns(result) { Fog::AWS.invert_mappings(mapping) } + } + + tests('mapping from aws') { + aws_data = { + 'A' => { + 'B' => { + 'C' => 100, + 'D' => 200 + }, + 'E' => [{ + 'FgH' => 300, + 'IJkl' => 400 + }] + } + } + + result = { + :a => { + :b => { + :c => 100, + :d => 200 + }, + :e => [{ + :fg_h => 300, + :i_jkl => 400 + }] + } + } + + returns(result) { Fog::AWS.map_from_aws(aws_data) } + } + + tests('mapping to aws') { + data = { + :a => { + :b => { + :c => 100, + 'd' => 200 + }, + :e => [{ + 'fg_h' => 300, + :i_jkl => 400 + }] + } + } + + result = { + 'A' => { + 'B' => { + 'C' => 100, + 'D' => 200 + }, + 'E' => [{ + 'FgH' => 300, + 'IJkl' => 400 + }] + } + } + + returns(result) { Fog::AWS.map_to_aws(data) } + } +end \ No newline at end of file diff --git a/tests/models/auto_scaling/policies_test.rb b/tests/models/auto_scaling/policies_test.rb new file mode 100644 index 0000000000..b4b285be60 --- /dev/null +++ b/tests/models/auto_scaling/policies_test.rb @@ -0,0 +1,26 @@ +Shindo.tests('AWS::AutoScaling | policies', ['aws', 'auto_scaling_m', 'models']) do + params = { + auto_scaling_group_name: 'test', + id: 'test_policy', + type: 'SimpleScaling', + scaling_adjustment: 1 + } + + lc_params = { + :id => 'lc', + :image_id => 'image-id', + :instance_type => 'instance-type', + } + + group_params = { + :id => 'test', + :availability_zones => [], + :launch_configuration_name => 'lc' + } + + Fog::AWS[:auto_scaling].configurations.new(lc_params).save + + Fog::AWS[:auto_scaling].groups.new(group_params).save + + collection_tests(Fog::AWS[:auto_scaling].policies, params, true) +end diff --git a/tests/models/auto_scaling/policy_test.rb b/tests/models/auto_scaling/policy_test.rb new file mode 100644 index 0000000000..25a3850f03 --- /dev/null +++ b/tests/models/auto_scaling/policy_test.rb @@ -0,0 +1,28 @@ +Shindo.tests('AWS::AutoScaling | policy', ['aws', 'auto_scaling_m', 'models']) do + + params = { + auto_scaling_group_name: 'test', + id: 'test_policy', + type: 'SimpleScaling', + scaling_adjustment: 1 + } + + lc_params = { + :id => 'lc', + :image_id => 'image-id', + :instance_type => 'instance-type', + } + + group_params = { + :id => 'test', + :availability_zones => [], + :launch_configuration_name => 'lc' + } + + Fog::AWS[:auto_scaling].configurations.new(lc_params).save + + Fog::AWS[:auto_scaling].groups.new(group_params).save + + model_tests(Fog::AWS[:auto_scaling].policies, params, true) + +end \ No newline at end of file diff --git a/tests/parsers/auto_scaling/describe_policies.rb b/tests/parsers/auto_scaling/describe_policies.rb new file mode 100644 index 0000000000..3fa52ba458 --- /dev/null +++ b/tests/parsers/auto_scaling/describe_policies.rb @@ -0,0 +1,72 @@ +require 'fog/xml' +require 'fog/aws/parsers/auto_scaling/describe_policies' + +DESCRIBE_POLICIES_RESULT = <<-EOF + + + + + arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:c322761b-3172-4d56-9a21-0ed9dEXAMPLE:autoScalingGroupName/my-asg:policyName/MyScaleDownPolicy + ChangeInCapacity + -1 + MyScaleDownPolicy + SimpleScaling + my-asg + 60 + + + TestQueue + arn:aws:cloudwatch:us-east-1:123456789012:alarm:TestQueue + + + + + arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:c55a5cdd-9be0-435b-b60b-a8dd3EXAMPLE:autoScalingGroupName/my-asg:policyName/MyScaleUpPolicy + ChangeInCapacity + + + 0 + 1 + + + MyScaleUpPolicy + StepScaling + my-asg + 120 + Average + + + TestQueue + arn:aws:cloudwatch:us-east-1:123456789012:alarm:TestQueue + + + + + arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:c55a5cdd-9be0-435b-b23b-a8dd3EXAMPLE:autoScalingGroupName/my-asg:policyName/MyScaleUpTargetPolicy + MyScaleUpTargetPolicy + TargetTrackingScaling + my-asg + 120 + + + ASGAverageCPUUtilization + + 50 + true + + + + + + a6ea2117-fac1-11e2-abd3-1740ab4ef14e + + +EOF + +Shindo.tests('AWS::Autoscaling | parsers | describe_policies', ['aws', 'auto_scaling', 'parser']) do + tests('parses the xml').formats(AWS::AutoScaling::Formats::DESCRIBE_POLICIES) do + parser = Nokogiri::XML::SAX::Parser.new(Fog::Parsers::AWS::AutoScaling::DescribePolicies.new) + parser.parse(DESCRIBE_POLICIES_RESULT) + parser.document.response + end +end diff --git a/tests/requests/auto_scaling/describe_types_tests.rb b/tests/requests/auto_scaling/describe_types_tests.rb index 216915cf51..b3cbdeebb3 100644 --- a/tests/requests/auto_scaling/describe_types_tests.rb +++ b/tests/requests/auto_scaling/describe_types_tests.rb @@ -97,6 +97,33 @@ body end + tests ("#describe_policies").formats(AWS::AutoScaling::Formats::DESCRIBE_POLICIES) do + params = { + auto_scaling_group_name: 'test', + id: 'test_policy', + type: 'SimpleScaling', + scaling_adjustment: 1 + } + + lc_params = { + :id => 'lc', + :image_id => 'image-id', + :instance_type => 'instance-type', + } + + group_params = { + :id => 'test', + :availability_zones => [], + :launch_configuration_name => 'lc' + } + + Fog::AWS[:auto_scaling].configurations.new(lc_params).save + + Fog::AWS[:auto_scaling].groups.new(group_params).save + + body = Fog::AWS[:auto_scaling].describe_termination_policy_types.body + end + end end diff --git a/tests/requests/auto_scaling/helper.rb b/tests/requests/auto_scaling/helper.rb index 9cd5f245ec..1c30b83ee3 100644 --- a/tests/requests/auto_scaling/helper.rb +++ b/tests/requests/auto_scaling/helper.rb @@ -123,15 +123,27 @@ module Formats 'UserData' => Fog::Nullable::String } + STEP_ADJUSTMENT = { + 'MetricIntervalLowerBound' => Fog::Nullable::Float, + 'MetricIntervalUpperBound' => Fog::Nullable::Float, + 'ScalingAdjustment' => Integer + } + SCALING_POLICY = { - 'AdjustmentType' => String, + 'AdjustmentType' => Fog::Nullable::String, 'Alarms' => [ALARM], 'AutoScalingGroupName' => String, - 'Cooldown' => Integer, - 'MinAdjustmentStep' => Integer, + 'Cooldown' => Fog::Nullable::Integer, + 'EstimatedInstanceWarmup' => Fog::Nullable::Integer, + 'MetricAggregationType' => Fog::Nullable::String, + 'MinAdjustmentMagnitude' => Fog::Nullable::String, + 'MinAdjustmentStep' => Fog::Nullable::Integer, 'PolicyARN' => String, 'PolicyName' => String, - 'ScalingAdjustment' => Integer + 'PolicyType' => String, + 'ScalingAdjustment' => Fog::Nullable::Integer, + 'TargetTrackingConfiguration' => Fog::Nullable::Hash, + 'StepAdjustments' => [STEP_ADJUSTMENT] } DESCRIBE_ADJUSTMENT_TYPES = BASIC.merge({ diff --git a/tests/requests/auto_scaling/model_tests.rb b/tests/requests/auto_scaling/model_tests.rb index 72a1fa74c1..60792561a0 100644 --- a/tests/requests/auto_scaling/model_tests.rb +++ b/tests/requests/auto_scaling/model_tests.rb @@ -2,7 +2,9 @@ tests('success') do lc = nil + asg = nil lc_id = 'fog-model-lc' + asg_id = 'fog-model-asg' tests('configurations') do tests('getting a missing configuration') do @@ -38,9 +40,6 @@ returns(nil) { Fog::AWS[:auto_scaling].groups.get('fog-no-such-asg') } end - asg = nil - asg_id = 'fog-model-asg' - tests('create') do asg = Fog::AWS[:auto_scaling].groups.create(:id => asg_id, :availability_zones => ['us-east-1d'], :launch_configuration_name => lc_id) #tests("dns names is set").returns(true) { asg.dns_name.is_a?(String) } @@ -76,11 +75,6 @@ end end - tests('destroy group') do - asg.destroy - asg = nil - end - #tests('registering an invalid instance') do # raises(Fog::AWS::AutoScaling::InvalidInstance) { asg.register_instances('i-00000000') } #end @@ -90,13 +84,6 @@ #end end - tests('configurations') do - tests('destroy configuration') do - lc.destroy - lc = nil - end - end - #server = Fog::AWS[:compute].servers.create #tests('register instance') do # begin @@ -186,50 +173,66 @@ # end #end - #tests('policies') do - # app_policy_id = 'my-app-policy' - # - # tests 'are empty' do - # returns([]) { elb.policies.to_a } - # end - # - # tests('#all') do - # returns([]) { elb.policies.all.to_a } - # end - # - # tests('create app policy') do - # elb.policies.create(:id => app_policy_id, :cookie => 'my-app-cookie', :cookie_stickiness => :app) - # returns(app_policy_id) { elb.policies.first.id } - # end - # - # tests('get policy') do - # returns(app_policy_id) { elb.policies.get(app_policy_id).id } - # end - # - # tests('destroy app policy') do - # elb.policies.first.destroy - # returns([]) { elb.policies.to_a } - # end - # - # lb_policy_id = 'my-lb-policy' - # tests('create lb policy') do - # elb.policies.create(:id => lb_policy_id, :expiration => 600, :cookie_stickiness => :lb) - # returns(lb_policy_id) { elb.policies.first.id } - # end - # - # tests('setting a listener policy') do - # elb.set_listener_policy(80, lb_policy_id) - # returns([lb_policy_id]) { elb.listeners.get(80).policy_names } - # end - # - # tests('unsetting a listener policy') do - # elb.unset_listener_policy(80) - # returns([]) { elb.listeners.get(80).policy_names } - # end - # - # tests('a malformed policy') do - # raises(ArgumentError) { elb.policies.create(:id => 'foo', :cookie_stickiness => 'invalid stickiness') } - # end - #end + tests('policies') do + simple_policy_id = 'fog-model-asg-policy-simple' + step_policy_id = 'fog-model-asg-policy-step' + target_policy_id = 'fog-model-asg-policy-target' + + tests 'create simple' do + asg_policy = Fog::AWS[:auto_scaling].policies.create({ id: simple_policy_id, scaling_adjustment: 1, auto_scaling_group_name: asg_id }) + tests('arn not nil').returns(false) { asg_policy.arn.nil? } + tests('adjustment is set').returns(true) { asg_policy.scaling_adjustment == 1 } + tests('alarms are not set').returns([]) { asg_policy.alarms } + end + + tests('create step') do + asg_policy = Fog::AWS[:auto_scaling].policies.create({ + id: step_policy_id, + type: 'StepScaling', + step_adjustments: [{ + 'MetricIntervalLowerBound' => 0, + 'MetricIntervalUpperBound' => nil, + 'ScalingAdjustment' => 1 + }], + auto_scaling_group_name: asg_id }) + tests('arn not nil').returns(false) { asg_policy.arn.nil? } + tests('nested attributes are available with snake case').returns(true) { asg_policy.step_adjustments[0][:metric_interval_lower_bound] == 0 } + end + + tests('create target') do + asg_policy = Fog::AWS[:auto_scaling].policies.create({ + id: target_policy_id, + type: 'TargetTrackingScaling', + target_tracking_configuration: { + predefined_metric_specification: { + predefined_metric_type: 'ASGAverageCPUUtilization' + }, + target_value: 50 + }, + auto_scaling_group_name: asg_id }) + tests('arn not nil').returns(false) { asg_policy.arn.nil? } + tests('nested attributes are available with snake case').returns(true) { asg_policy.target_tracking_configuration[:target_value] == 50 } + end + + tests('destroy') do + policy = Fog::AWS[:auto_scaling].policies.get(simple_policy_id, asg_id) + policy.destroy + returns(nil) { Fog::AWS[:auto_scaling].policies.get(simple_policy_id, asg_id) } + end + end + + tests('groups') do + tests('destroy group') do + asg.destroy + asg = nil + end + end + + tests('configurations') do + tests('destroy configuration') do + lc.destroy + lc = nil + end + end end end