Merge pull request #12533 from lyft/boto_autoscale

Initial commit of boto_asg module and state
This commit is contained in:
Thomas S Hatch 2014-05-05 09:54:33 -06:00
commit ef463f929f
2 changed files with 645 additions and 0 deletions

315
salt/modules/boto_asg.py Normal file
View File

@ -0,0 +1,315 @@
# -*- coding: utf-8 -*-
'''
Connection module for Amazon Autoscale Groups
.. versionadded:: Helium
:configuration: This module accepts explicit autoscale credentials but can also
utilize IAM roles assigned to the instance trough Instance Profiles.
Dynamic credentials are then automatically obtained from AWS API and no
further configuration is necessary. More Information available at::
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
If IAM roles are not used you need to specify them either in a pillar or
in the minion's config file::
asg.keyid: GKTADJGHEIQSXMKKRBJ08H
asg.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
A region may also be specified in the configuration::
asg.region: us-east-1
If a region is not specified, the default is us-east-1.
It's also possible to specify key, keyid and region via a profile, either
as a passed in dict, or as a string to pull from pillars or minion config:
myprofile:
keyid: GKTADJGHEIQSXMKKRBJ08H
key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
region: us-east-1
:depends: boto
'''
# Import Python libs
import logging
import json
log = logging.getLogger(__name__)
# Import third party libs
try:
import boto
import boto.ec2
import boto.ec2.autoscale as autoscale
logging.getLogger('boto').setLevel(logging.CRITICAL)
HAS_BOTO = True
except ImportError:
HAS_BOTO = False
from salt._compat import string_types
import salt.utils.odict as odict
def __virtual__():
'''
Only load if boto libraries exist.
'''
if not HAS_BOTO:
return False
return True
def exists(name, region=None, key=None, keyid=None, profile=None):
'''
Check to see if an autoscale group exists.
CLI example::
salt myminion boto_asg.exists myasg region=us-east-1
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return False
try:
conn.conn.get_all_groups(names=[name])
return True
except boto.exception.BotoServerError as e:
log.debug(e)
return False
def get_config(name, region=None, key=None, keyid=None, profile=None):
'''
Get the configuration for an autoscale group
CLI example::
salt myminion boto_asg.get_config myasg region=us-east-1
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return None
try:
asg = conn.get_all_groups(names=[name])
if asg:
asg = asg[0]
else:
return {}
ret = odict.OrderedDict()
attrs = ['name', 'availability_zones', 'default_cooldown',
'desired_capacity', 'health_check_period',
'health_check_type', 'launch_config_name', 'load_balancers',
'max_size', 'min_size', 'placement_group',
'vpc_zone_identifier', 'tags', 'termination_policies']
for attr in attrs:
# Tags are objects, so we need to turn them into dicts.
if attr == 'tags':
_tags = []
for tag in asg.tags:
_tag = odict.OrderedDict()
_tag['key'] = tag.key
_tag['value'] = tag.value
_tag['propagate_at_launch'] = tag.propagate_at_launch
_tags.append(_tag)
ret['tags'] = _tags
else:
ret[attr] = getattr(asg, attr)
return ret
except boto.exception.BotoServerError as e:
log.debug(e)
return {}
def create(name, launch_config_name, availability_zones, min_size, max_size,
desired_capacity=None, load_balancers=None, default_cooldown=None,
health_check_type=None, health_check_period=None,
placement_group=None, vpc_zone_identifier=None, tags=None,
termination_policies=None, region=None, key=None, keyid=None,
profile=None):
'''
Create an ELB
CLI example to create an ELB::
salt myminion boto_asg.create myasg mylc '["us-east-1a", "us-east-1e"]' 1 10 load_balancers='["myelb", "myelb2"]' tags='[{"key": "Name", value="myasg", "propagate_at_launch": True}]'
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return False
if isinstance(availability_zones, string_types):
availability_zones = json.loads(availability_zones)
if isinstance(load_balancers, string_types):
load_balancers = json.loads(load_balancers)
if isinstance(vpc_zone_identifier, string_types):
vpc_zone_identifier = json.loads(vpc_zone_identifier)
if isinstance(tags, string_types):
tags = json.loads(tags)
# Make a list of tag objects from the dict.
_tags = []
for tag in tags:
try:
key = tag.get('key')
except KeyError:
log.error('Tag missing key.')
return False
try:
value = tag.get('value')
except KeyError:
log.error('Tag missing value.')
return False
propagate_at_launch = tag.get('propagate_at_launch', False)
_tag = autoscale.Tag(key=key, value=value, resource_id=name,
propagate_at_launch=propagate_at_launch)
_tags.append(_tag)
if isinstance(termination_policies, string_types):
termination_policies = json.loads(termination_policies)
try:
_asg = autoscale.AutoScalingGroup(
name=name, launch_config=launch_config_name,
availability_zones=availability_zones,
min_size=min_size, max_size=max_size,
desired_capacity=desired_capacity, load_balancers=load_balancers,
default_cooldown=default_cooldown,
health_check_type=health_check_type,
health_check_period=health_check_period,
placement_group=placement_group, tags=_tags,
vpc_zone_identifier=vpc_zone_identifier,
termination_policies=termination_policies)
conn.create_auto_scaling_group(_asg)
log.info('Created ASG {0}'.format(name))
return True
except boto.exception.BotoServerError as e:
log.debug(e)
msg = 'Failed to create ELB {0}'.format(name)
log.error(msg)
return False
def update(name, launch_config_name, availability_zones, min_size, max_size,
desired_capacity=None, load_balancers=None, default_cooldown=None,
health_check_type=None, health_check_period=None,
placement_group=None, vpc_zone_identifier=None, tags=None,
termination_policies=None, region=None, key=None, keyid=None,
profile=None):
'''
Update an ELB
CLI example::
salt myminion boto_asg.update myasg mylc '["us-east-1a", "us-east-1e"]' 1 10 load_balancers='["myelb", "myelb2"]' tags='[{"key": "Name", value="myasg", "propagate_at_launch": True}]'
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return False
if isinstance(availability_zones, string_types):
availability_zones = json.loads(availability_zones)
if isinstance(load_balancers, string_types):
load_balancers = json.loads(load_balancers)
if isinstance(vpc_zone_identifier, string_types):
vpc_zone_identifier = json.loads(vpc_zone_identifier)
if isinstance(tags, string_types):
tags = json.loads(tags)
# Make a list of tag objects from the dict.
_tags = []
for tag in tags:
try:
key = tag.get('key')
except KeyError:
log.error('Tag missing key.')
return False
try:
value = tag.get('value')
except KeyError:
log.error('Tag missing value.')
return False
propagate_at_launch = tag.get('propagate_at_launch', False)
_tag = autoscale.Tag(key=key, value=value, resource_id=name,
propagate_at_launch=propagate_at_launch)
_tags.append(_tag)
if isinstance(termination_policies, string_types):
termination_policies = json.loads(termination_policies)
try:
_asg = autoscale.AutoScalingGroup(
connection=conn,
name=name, launch_config=launch_config_name,
availability_zones=availability_zones,
min_size=min_size, max_size=max_size,
desired_capacity=desired_capacity, load_balancers=load_balancers,
default_cooldown=default_cooldown,
health_check_type=health_check_type,
health_check_period=health_check_period,
placement_group=placement_group, tags=_tags,
vpc_zone_identifier=vpc_zone_identifier,
termination_policies=termination_policies)
_asg.update()
# Seems the update call doesn't handle tags, so we'll need to update
# that separately.
conn.create_or_update_tags(_tags)
log.info('Updated ASG {0}'.format(name))
return True
except boto.exception.BotoServerError as e:
log.debug(e)
msg = 'Failed to update ELB {0}'.format(name)
log.error(msg)
return False
def delete(name, force=False, region=None, key=None, keyid=None, profile=None):
'''
Delete an autoscale group.
CLI example::
salt myminion boto_asg.delete myasg region=us-east-1
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return False
try:
conn.delete_auto_scaling_group(name, force)
msg = 'Deleted autoscale group {0}.'.format(name)
log.info(msg)
return True
except boto.exception.BotoServerError as e:
log.debug(e)
msg = 'Failed to delete autoscale group {0}'.format(name)
log.error(msg)
return False
def _get_conn(region, key, keyid, profile):
'''
Get a boto connection to autoscale.
'''
if profile:
if isinstance(profile, string_types):
_profile = __salt__['config.option'](profile)
elif isinstance(profile, dict):
_profile = profile
key = _profile.get('key', None)
keyid = _profile.get('keyid', None)
region = _profile.get('region', None)
if not region and __salt__['config.option']('asg.region'):
region = __salt__['config.option']('asg.region')
if not region:
region = 'us-east-1'
if not key and __salt__['config.option']('asg.key'):
key = __salt__['config.option']('asg.key')
if not keyid and __salt__['config.option']('asg.keyid'):
keyid = __salt__['config.option']('asg.keyid')
try:
conn = autoscale.connect_to_region(region, aws_access_key_id=keyid,
aws_secret_access_key=key)
except boto.exception.NoAuthHandlerFound:
log.error('No authentication credentials found when attempting to'
' make boto autoscale connection.')
return None
return conn

330
salt/states/boto_asg.py Normal file
View File

@ -0,0 +1,330 @@
# -*- coding: utf-8 -*-
'''
Manage Autoscale Groups
=======================
.. versionadded:: Helium
Create and destroy autoscale groups. Be aware that this interacts with Amazon's
services, and so may incur charges.
This module uses boto, which can be installed via package, or pip.
This module accepts explicit autoscale credentials but can also utilize
IAM roles assigned to the instance trough Instance Profiles. Dynamic
credentials are then automatically obtained from AWS API and no further
configuration is necessary. More Information available at::
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
If IAM roles are not used you need to specify them either in a pillar or
in the minion's config file::
asg.keyid: GKTADJGHEIQSXMKKRBJ08H
asg.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
It's also possible to specify key, keyid and region via a profile, either
as a passed in dict, or as a string to pull from pillars or minion config:
myprofile:
keyid: GKTADJGHEIQSXMKKRBJ08H
key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
region: us-east-1
.. code-block:: yaml
Ensure myasg exists:
boto_asg.present:
- name: myasg
- launch_config_name: mylc
- availability_zones:
- us-east-1a
- us-east-1b
- min_size: 1
- max_size: 1
- desired_capacity: 1
- load_balancers:
- myelb
- region: us-east-1
- keyid: GKTADJGHEIQSXMKKRBJ08H
- key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
# Using a profile from pillars.
Ensure myasg exists:
boto_asg.present:
- name: myasg
- launch_config_name: mylc
- availability_zones:
- us-east-1a
- us-east-1b
- min_size: 1
- max_size: 1
- desired_capacity: 1
- load_balancers:
- myelb
- profile: myprofile
# Passing in a profile.
Ensure myasg exists:
boto_asg.present:
- name: myasg
- launch_config_name: mylc
- availability_zones:
- us-east-1a
- us-east-1b
- min_size: 1
- max_size: 1
- desired_capacity: 1
- load_balancers:
- myelb
- profile:
keyid: GKTADJGHEIQSXMKKRBJ08H
key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
region: us-east-1
# Deleting an autoscale group with running instances.
Ensure myasg is deleted:
boto_asg.absent:
- name: myasg
# If instances exist, we must force the deletion of the asg.
- force: True
'''
def __virtual__():
'''
Only load if boto is available.
'''
return 'boto_asg' if 'boto_asg.exists' in __salt__ else False
def present(
name,
launch_config_name,
availability_zones,
min_size,
max_size,
desired_capacity=None,
load_balancers=None,
default_cooldown=None,
health_check_type=None,
health_check_period=None,
placement_group=None,
vpc_zone_identifier=None,
tags=None,
termination_policies=None,
region=None,
key=None,
keyid=None,
profile=None):
'''
Ensure the autoscale group exists.
name
Name of the autoscale group.
launch_config_name
Name of the launch config to use for the group.
availability_zones
List of availability zones for the group.
min_size
Minimum size of the group.
max_size
Maximum size of the group.
desired_capacity
The desired capacity of the group.
load_balancers
List of load balancers for the group. Once set this can not be
updated (Amazon restriction).
default_cooldown
Number of seconds after a Scaling Activity completes before any further
scaling activities can start.
health_check_type
The service you want the health status from, Amazon EC2 or Elastic Load
Balancer (EC2 or ELB).
health_check_period
Length of time in seconds after a new EC2 instance comes into service
that Auto Scaling starts checking its health.
placement_group
Physical location of your cluster placement group created in Amazon
EC2. Once set this can not be updated (Amazon restriction).
vpc_zone_identifier
A list of the subnet identifiers of the Virtual Private Cloud.
tags
A list of tags. Example:
- key: 'key'
value: 'value'
propagate_at_launch: true
termination_policies
A list of termination policies. Valid values are: OldestInstance,
NewestInstance, OldestLaunchConfiguration,
ClosestToNextInstanceHour, Default. If no value is specified, the
Default value is used.
region
The region to connect to.
key
Secret key to be used.
keyid
Access key to be used.
profile
A dict with region, key and keyid, or a pillar key (string)
that contains a dict with region, key and keyid.
'''
ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}
asg = __salt__['boto_asg.get_config'](name, region, key, keyid, profile)
if asg is None:
ret['result'] = False
ret['comment'] = 'Failed to check autoscale group existence.'
elif not asg:
if __opts__['test']:
msg = 'Autoscale group set to be created.'
ret['comment'] = msg
return ret
created = __salt__['boto_asg.create'](name, launch_config_name,
availability_zones, min_size,
max_size, desired_capacity,
load_balancers, default_cooldown,
health_check_type,
health_check_period,
placement_group,
vpc_zone_identifier, tags,
termination_policies, region,
key, keyid, profile)
if created:
ret['result'] = True
ret['changes']['old'] = None
asg = __salt__['boto_asg.get_config'](name, region, key, keyid,
profile)
ret['changes']['new'] = asg
else:
ret['result'] = False
ret['comment'] = 'Failed to create autoscale group'
else:
need_update = False
# If any of these attributes can't be modified after creation
# time, we should remove them from the dict.
config = {
'launch_config_name': launch_config_name,
'availability_zones': availability_zones,
'min_size': min_size,
'max_size': max_size,
'desired_capacity': desired_capacity,
'default_cooldown': default_cooldown,
'health_check_type': health_check_type,
'health_check_period': health_check_period,
'vpc_zone_identifier': vpc_zone_identifier,
'tags': tags,
'termination_policies': termination_policies
}
for key, value in config.iteritems():
# Only modify values being specified; introspection is difficult
# otherwise since it's hard to track default values, which will
# always be returned from AWS.
if not value:
continue
if key in asg:
_value = asg[key]
if isinstance(value, list):
_value.sort()
value.sort()
if _value != value:
need_update = True
if need_update:
if __opts__['test']:
msg = 'Autoscale group set to be updated.'
ret['comment'] = msg
return ret
updated = __salt__['boto_asg.update'](name, launch_config_name,
availability_zones, min_size,
max_size, desired_capacity,
load_balancers,
default_cooldown,
health_check_type,
health_check_period,
placement_group,
vpc_zone_identifier, tags,
termination_policies, region,
key, keyid, profile)
if updated:
ret['result'] = True
ret['changes']['old'] = asg
asg = __salt__['boto_asg.get_config'](name, region, key, keyid,
profile)
ret['changes']['new'] = asg
ret['comment'] = 'Updated autoscale group.'
else:
ret['result'] = False
ret['comment'] = 'Failed to update autoscale group.'
else:
ret['comment'] = 'Autoscale group present.'
return ret
def absent(
name,
force=False,
region=None,
key=None,
keyid=None,
profile=None):
'''
Ensure the named autoscale group is deleted.
name
Name of the autoscale group.
force
Force deletion of autoscale group.
region
The region to connect to.
key
Secret key to be used.
keyid
Access key to be used.
profile
A dict with region, key and keyid, or a pillar key (string)
that contains a dict with region, key and keyid.
'''
ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}
asg = __salt__['boto_asg.get_config'](name, region, key, keyid, profile)
if asg is None:
ret['result'] = False
ret['comment'] = 'Failed to check autoscale group existence.'
elif asg:
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'Autoscale group set to be deleted.'
return ret
deleted = __salt__['boto_asg.delete'](name, force, region, key, keyid,
profile)
if deleted:
ret['result'] = True
ret['changes']['old'] = asg
ret['changes']['new'] = None
ret['comment'] = 'Deleted autoscale group.'
else:
ret['result'] = False
ret['comment'] = 'Failed to delete autoscale group.'
else:
ret['comment'] = 'Autoscale group does not exist.'
return ret