From 2afa7c691d5b6cb06d53007d62ae91b6e42b4cdd Mon Sep 17 00:00:00 2001 From: Zeekin Date: Tue, 18 Mar 2014 10:32:55 +1000 Subject: [PATCH] Added AWS modules ec2_scaling_policy and ec2_metricalarm for configuring scaling policies for autoscaling groups, and metric alarms. --- cloud/ec2_metricalarm | 268 +++++++++++++++++++++++++++++++++++++++ cloud/ec2_scaling_policy | 181 ++++++++++++++++++++++++++ 2 files changed, 449 insertions(+) create mode 100644 cloud/ec2_metricalarm create mode 100755 cloud/ec2_scaling_policy diff --git a/cloud/ec2_metricalarm b/cloud/ec2_metricalarm new file mode 100644 index 00000000000..d1f3f8151fa --- /dev/null +++ b/cloud/ec2_metricalarm @@ -0,0 +1,268 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = """ + +--- +module: ec2_metricalarm +short_description: Create/update or delete AWS Cloudwatch 'metric alarms' +description: + - Can create or delete AWS metric alarms + - Metrics you wish to alarm on must already exist +version_added: "1.6" +requirements: [ "boto" ] +author: Zacharie Eakin +options: + state: + description: + - register or deregister the alarm + required: true + choices: ['present', 'absent'] + name: + desciption: + - Unique name for the alarm + required: true + metric: + description: + - Name of the monitored metric (e.g. CPUUtilization) + - Metric must already exist + required: false + namespace: + description: + - Name of the appropriate namespace, which determines the category it will appear under in cloudwatch + required: false + options: ['AWS/AutoScaling','AWS/Billing','AWS/DynamoDB','AWS/ElastiCache','AWS/EBS','AWS/EC2','AWS/ELB','AWS/ElasticMapReduce','AWS/OpsWorks','AWS/Redshift','AWS/RDS','AWS/Route53','AWS/SNS','AWS/SQS','AWS/StorageGateway'] + statistic: + description: + - Operation applied to the metric + - Works in conjunction with period and evaluation_periods to determine the comparison value + required: false + options: ['SampleCount','Average','Sum','Minimum','Maximum'] + comparison: + description: + - Determines how the threshold value is compared + required: false + options: ['<=','<','>','>='] + threshold: + description: + - Sets the min/max bound for triggering the alarm + required: false + period: + description: + - The time (in seconds) between metric evaluations + required: false + evaluation_periods: + description: + - The number of times in which the metric is evaluated before final calculation + required: false + unit: + description: + - The threshold's unit of measurement + required: false + options: ['Seconds','Microseconds','Milliseconds','Bytes','Kilobytes','Megabytes','Gigabytes','Terabytes','Bits','Kilobits','Megabits','Gigabits','Terabits','Percent','Count','Bytes/Second','Kilobytes/Second','Megabytes/Second','Gigabytes/Second','Terabytes/Second','Bits/Second','Kilobits/Second','Megabits/Second','Gigabits/Second','Terabits/Second','Count/Second','None'] + description: + description: + - A longer desciption of the alarm + required: false + dimensions: + description: + - Describes to what the alarm is applied + required: false + alarm_actions: + description: + - A list of the names action(s) taken when the alarm is in the 'alarm' status + required: false + insufficient_data_actions: + description: + - A list of the names of action(s) to take when the alarm is in the 'insufficient_data' status + required: false + ok_actions: + description: + - A list of the names of action(s) to take when the alarm is in the 'ok' status + required: false + +--- +""" + +EXAMPLES = ''' + - name: create alarm + ec2_metricalarm: + state: present + region: ap-southeast-2 + name: "cpu-low" + metric: "CPUUtilization" + namespace: "AWS/EC2" + statistic: Average + comparison: "<=" + threshold: 5.0 + period: 300 + evaluation_periods: 3 + unit: "Percent" + description: "This will alarm when a bamboo slave's cpu usage average is lower than 5% for 15 minutes " + dimensions: {'InstanceId':'i-XXX'} + alarm_actions: ["action1","action2"] + + +''' + +import sys + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +try: + import boto.ec2.cloudwatch + from boto.ec2.cloudwatch import CloudWatchConnection, MetricAlarm + from boto.exception import BotoServerError +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + + +def create_metric_alarm(connection, module): + + name = module.params.get('name') + metric = module.params.get('metric') + namespace = module.params.get('namespace') + statistic = module.params.get('statistic') + comparison = module.params.get('comparison') + threshold = module.params.get('threshold') + period = module.params.get('period') + evaluation_periods = module.params.get('evaluation_periods') + unit = module.params.get('unit') + description = module.params.get('description') + dimensions = module.params.get('dimensions') + alarm_actions = module.params.get('alarm_actions') + insufficient_data_actions = module.params.get('insufficient_data_actions') + ok_actions = module.params.get('ok_actions') + + alarms = connection.describe_alarms(alarm_names=[name]) + + if not alarms: + + alm = MetricAlarm( + name=name, + metric=metric, + namespace=namespace, + statistic=statistic, + comparison=comparison, + threshold=threshold, + period=period, + evaluation_periods=evaluation_periods, + unit=unit, + description=description, + dimensions=dimensions, + alarm_actions=alarm_actions, + insufficient_data_actions=insufficient_data_actions, + ok_actions=ok_actions + ) + try: + connection.create_alarm(alm) + module.exit_json(changed=True) + except BotoServerError, e: + module.fail_json(msg=str(e)) + + else: + alarm = alarms[0] + changed = False + + for attr in ('comparison','metric','namespace','statistic','threshold','period','evaluation_periods','unit','description'): + if getattr(alarm, attr) != module.params.get(attr): + changed = True + setattr(alarm, attr, module.params.get(attr)) + #this is to deal with a current bug where you cannot assign '<=>' to the comparator when modifying an existing alarm + comparison = alarm.comparison + comparisons = {'<=' : 'LessThanOrEqualToThreshold', '<' : 'LessThanThreshold', '>=' : 'GreaterThanOrEqualToThreshold', '>' : 'GreaterThanThreshold'} + alarm.comparison = comparisons[comparison] + + dim1 = module.params.get('dimensions') + dim2 = alarm.dimensions + + for keys in dim1: + if not isinstance(dim1[keys], list): + dim1[keys] = [dim1[keys]] + if dim1[keys] != dim2[keys]: + changed=True + setattr(alarm, 'dimensions', dim1) + + for attr in ('alarm_actions','insufficient_data_actions','ok_actions'): + action = module.params.get(attr) or [] + if getattr(alarm, attr) != action: + changed = True + setattr(alarm, attr, module.params.get(attr)) + + try: + if changed: + connection.create_alarm(alarm) + module.exit_json(changed=changed) + except BotoServerError, e: + module.fail_json(msg=str(e)) + + +def delete_metric_alarm(connection, module): + name = module.params.get('name') + + alarms = connection.describe_alarms(alarm_names=[name]) + + if alarms: + try: + connection.delete_alarms([name]) + module.exit_json(changed=True) + except BotoServerError, e: + module.fail_json(msg=str(e)) + else: + module.exit_json(changed=False) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name=dict(required=True, type='str'), + metric=dict(type='str'), + namespace=dict(type='str', choices=['AWS/AutoScaling', 'AWS/Billing', 'AWS/DynamoDB', 'AWS/ElastiCache', 'AWS/EBS', 'AWS/EC2', + 'AWS/ELB', 'AWS/ElasticMapReduce', 'AWS/OpsWorks', 'AWS/Redshift', 'AWS/RDS', 'AWS/Route53', 'AWS/SNS', 'AWS/SQS', 'AWS/StorageGateway']), statistic=dict(type='str', choices=['SampleCount', 'Average', 'Sum', 'Minimum', 'Maximum']), + comparison=dict(type='str', choices=['<=', '<', '>', '>=']), + threshold=dict(type='float'), + period=dict(type='int'), + unit=dict(type='str', choices=['Seconds', 'Microseconds', 'Milliseconds', 'Bytes', 'Kilobytes', 'Megabytes', 'Gigabytes', 'Terabytes', 'Bits', 'Kilobits', 'Megabits', 'Gigabits', 'Terabits', 'Percent', 'Count', 'Bytes/Second', 'Kilobytes/Second', 'Megabytes/Second', 'Gigabytes/Second', 'Terabytes/Second', 'Bits/Second', 'Kilobits/Second', 'Megabits/Second', 'Gigabits/Second', 'Terabits/Second', 'Count/Second', 'None']), + evaluation_periods=dict(type='int'), + description=dict(type='str'), + dimensions=dict(type='dict'), + alarm_actions=dict(type='list'), + insufficient_data_actions=dict(type='list'), + ok_actions=dict(type='list'), + state=dict(default='present', choices=['present', 'absent']), + region=dict(aliases=['aws_region', 'ec2_region'], choices=AWS_REGIONS), + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + state = module.params.get('state') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + try: + connection = connect_to_aws(boto.ec2.cloudwatch, region, **aws_connect_params) + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg=str(e)) + + if state == 'present': + create_metric_alarm(connection, module) + elif state == 'absent': + delete_metric_alarm(connection, module) + +main() diff --git a/cloud/ec2_scaling_policy b/cloud/ec2_scaling_policy new file mode 100755 index 00000000000..b2395cd0a3c --- /dev/null +++ b/cloud/ec2_scaling_policy @@ -0,0 +1,181 @@ +#!/usr/bin/python + +DOCUMENTATION = """ +--- +module:ec2_scaling_policy +short_description: Create or delete AWS scaling policies for Autoscaling groups +description: + - Can create or delete scaling policies for autoscaling groups + - Referenced autoscaling groups must already exist +version_added: "1.6" +requirements: [ "boto" ] +author: Zacharie Eakin +options: + state: + description: + - register or deregister the policy + required: true + choices: ['present', 'absent'] + name: + description: + - Unique name for the scaling policy + required: true + asg_name: + description: + - Name of the associated autoscaling group + required: true + adjustment_type: + desciption: + - The type of change in capacity of the autoscaling group + required: false + choices: ['ChangeInCapacity','ExactCapacity','PercentChangeInCapacity'] + scaling_adjustment: + description: + - The amount by which the autoscaling group is adjusted by the policy + required: false + min_adjustment_step: + description: + - Minimum amount of adjustment when policy is triggered + required: false + cooldown: + description: + - The minimum period of time between which autoscaling actions can take place + required: false +""" + +EXAMPLES = ''' +- ec2_scaling_policy: + state: present + region: US-XXX + name: "scaledown-policy" + adjustment_type: "ChangeInCapacity" + asg_name: "slave-pool" + scaling_adjustment: -1 + min_adjustment_step: 1 + cooldown: 300 +''' + + +import sys + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +try: + import boto.ec2.autoscale + from boto.ec2.autoscale import ScalingPolicy + from boto.exception import BotoServerError + +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + + +def create_scaling_policy(connection, module): + sp_name = module.params.get('name') + adjustment_type = module.params.get('adjustment_type') + asg_name = module.params.get('asg_name') + scaling_adjustment = module.params.get('scaling_adjustment') + min_adjustment_step = module.params.get('min_adjustment_step') + cooldown = module.params.get('cooldown') + + scalingPolicies = connection.get_all_policies(as_group=asg_name,policy_names=[sp_name]) + + if not scalingPolicies: + sp = ScalingPolicy( + name=sp_name, + adjustment_type=adjustment_type, + as_name=asg_name, + scaling_adjustment=scaling_adjustment, + min_adjustment_step=min_adjustment_step, + cooldown=cooldown) + + try: + connection.create_scaling_policy(sp) + module.exit_json(changed=True) + except BotoServerError, e: + module.fail_json(msg=str(e)) + else: + policy = scalingPolicies[0] + changed = False + + #min_adjustment_step attribute is only relevant if the adjustment_type + #is set to percentage change in capacity, so it is a special case + if getattr(policy, 'adjustment_type') == 'PercentChangeInCapacity': + if getattr(policy, 'min_adjustment_step') != module.params.get('min_adjustment_step'): + changed = True + + #set the min adjustment step incase the user decided to change their adjustment type to percentage + setattr(policy, 'min_adjustment_step', module.params.get('min_adjustment_step')) + + #check the remaining attributes + for attr in ('adjustment_type','scaling_adjustment','cooldown'): + if getattr(policy, attr) != module.params.get(attr): + changed = True + setattr(policy, attr, module.params.get(attr)) + + try: + if changed: + connection.create_scaling_policy(policy) + policy = connection.get_all_policies(policy_names=[sp_name])[0] + module.exit_json(changed=changed, name=policy.name, arn=policy.policy_arn, as_name=policy.as_name, scaling_adjustment=policy.scaling_adjustment, cooldown=policy.cooldown, adjustment_type=policy.adjustment_type, min_adjustment_step=policy.min_adjustment_step) + module.exit_json(changed=changed) + except BotoServerError, e: + module.fail_json(msg=str(e)) + + +def delete_scaling_policy(connection, module): + sp_name = module.params.get('name') + asg_name = module.params.get('asg_name') + + scalingPolicies = connection.get_all_policies(as_group=asg_name,policy_names=[sp_name]) + + if scalingPolicies: + try: + connection.delete_policy(sp_name, asg_name) + module.exit_json(changed=True) + except BotoServerError, e: + module.exit_json(changed=False, msg=str(e)) + else: + module.exit_json(changed=False) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name = dict(required=True, type='str'), + adjustment_type = dict(type='str', choices=['ChangeInCapacity','ExactCapacity','PercentChangeInCapacity']), + asg_name = dict(required=True, type='str'), + scaling_adjustment = dict(type='int'), + min_adjustment_step = dict(type='int'), + cooldown = dict(type='int'), + region = dict(aliases=['aws_region', 'ec2_region'], choices=AWS_REGIONS), + state=dict(default='present', choices=['present', 'absent']), + ) + ) + + module = AnsibleModule(argument_spec=argument_spec) + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + state = module.params.get('state') + + try: + connection = connect_to_aws(boto.ec2.autoscale, region, **aws_connect_params) + except boto.exception.NoAuthHandlerFound, e: + module.fail_json(msg = str(e)) + + if state == 'present': + create_scaling_policy(connection, module) + elif state == 'absent': + delete_scaling_policy(connection, module) + + +main() + + + + + +