Merge pull request #12741 from lyft/boto-secgroup

Initial commit of boto_secgroup execution module and state.
This commit is contained in:
Thomas S Hatch 2014-05-13 14:07:02 -06:00
commit 92aca12854
2 changed files with 655 additions and 0 deletions

View File

@ -0,0 +1,320 @@
# -*- coding: utf-8 -*-
'''
Connection module for Amazon Security Groups
.. versionadded:: Helium
:configuration: This module accepts explicit ec2 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::
secgroup.keyid: GKTADJGHEIQSXMKKRBJ08H
secgroup.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
A region may also be specified in the configuration::
secgroup.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
log = logging.getLogger(__name__)
# Import third party libs
try:
import boto
import boto.ec2
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 security group exists.
CLI example::
salt myminion boto_secgroup.exists mysecgroup
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return False
try:
conn.get_all_security_groups([name])
return True
except boto.exception.BotoServerError as e:
log.debug(e)
return False
def _split_rules(rules):
'''
Split rules with combined grants into individual rules.
Amazon returns a set of rules with the same protocol, from and to ports
together as a single rule with a set of grants. Authorizing and revoking
rules, however, is done as a split set of rules. This function splits the
rules up.
'''
split = []
for rule in rules:
ip_protocol = rule.get('ip_protocol')
to_port = rule.get('to_port')
from_port = rule.get('from_port')
grants = rule.get('grants')
for grant in grants:
_rule = {'ip_protocol': ip_protocol,
'to_port': to_port,
'from_port': from_port}
for key, val in grant.iteritems():
_rule[key] = val
split.append(_rule)
return split
def get_config(name=None, group_id=None, region=None, key=None, keyid=None,
profile=None):
'''
Get the configuration for a security group.
CLI example::
salt myminion boto_secgroup.get_config mysecgroup
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return None
if not (name or group_id):
return None
try:
if name:
sg = conn.get_all_security_groups([name])
else:
sg = conn.get_all_security_groups(group_ids=[group_id])
except boto.exception.BotoServerError as e:
msg = 'Failed to get config for security group {0}.'.format(name)
log.error(msg)
log.debug(e)
return {}
sg = sg[0]
ret = odict.OrderedDict()
ret['name'] = name
ret['group_id'] = sg.id
ret['owner_id'] = sg.owner_id
ret['description'] = sg.description
# TODO: add support for tags
_rules = []
for rule in sg.rules:
attrs = ['ip_protocol', 'from_port', 'to_port', 'grants']
_rule = odict.OrderedDict()
for attr in attrs:
val = getattr(rule, attr)
if not val:
continue
if attr == 'grants':
_grants = []
for grant in val:
g_attrs = {'name': 'source_group_name',
'owner_id': 'source_group_owner_id',
'group_id': 'source_group_group_id',
'cidr_ip': 'cidr_ip'}
_grant = odict.OrderedDict()
for g_attr, g_attr_map in g_attrs.iteritems():
g_val = getattr(grant, g_attr)
if not g_val:
continue
_grant[g_attr_map] = g_val
_grants.append(_grant)
_rule['grants'] = _grants
elif attr == 'from_port':
_rule[attr] = int(val)
elif attr == 'to_port':
_rule[attr] = int(val)
else:
_rule[attr] = val
_rules.append(_rule)
ret['rules'] = _split_rules(_rules)
return ret
def create(name, description, vpc_id=None, region=None, key=None, keyid=None,
profile=None):
'''
Create an autoscale group.
CLI example::
salt myminion boto_secgroup.create mysecgroup 'My Security Group'
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return False
created = conn.create_security_group(name, description, vpc_id)
if created:
log.info('Created security group {0}.'.format(name))
return True
else:
msg = 'Failed to create security group {0}.'.format(name)
log.error(msg)
return False
def delete(name, group_id=None, region=None, key=None, keyid=None,
profile=None):
'''
Delete an autoscale group.
CLI example::
salt myminion boto_secgroup.delete mysecgroup
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return False
deleted = conn.delete_security_group(name, group_id)
if deleted:
log.info('Deleted security group {0}.'.format(name))
return True
else:
msg = 'Failed to delete security group {0}.'.format(name)
log.error(msg)
return False
def authorize(name, source_group_name=None,
source_group_owner_id=None, ip_protocol=None,
from_port=None, to_port=None, cidr_ip=None, group_id=None,
source_group_group_id=None, region=None, key=None,
keyid=None, profile=None):
'''
Add a new rule to an existing security group.
CLI example::
salt myminion boto_secgroup.authorize mysecgroup ip_protocol=tcp from_port=80 to_port=80 cidr_ip='["10.0.0.0/0", "192.168.0.0/0"]'
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return False
try:
added = conn.authorize_security_group(
group_name=name, src_security_group_name=source_group_name,
src_security_group_owner_id=source_group_owner_id,
ip_protocol=ip_protocol, from_port=from_port, to_port=to_port,
cidr_ip=cidr_ip, group_id=group_id,
src_security_group_group_id=source_group_group_id)
if added:
log.info('Added rule to security group {0}.'.format(name))
return True
else:
msg = 'Failed to add rule to security group {0}.'.format(name)
log.error(msg)
return False
except boto.exception.EC2ResponseError as e:
log.debug(e)
msg = 'Failed to add rule to security group {0}.'.format(name)
log.error(msg)
return False
def revoke(name, source_group_name=None,
source_group_owner_id=None, ip_protocol=None,
from_port=None, to_port=None, cidr_ip=None, group_id=None,
source_group_group_id=None, region=None, key=None,
keyid=None, profile=None):
'''
Remove a rule from an existing security group.
CLI example::
salt myminion boto_secgroup.revoke mysecgroup ip_protocol=tcp from_port=80 to_port=80 cidr_ip='["10.0.0.0/0", "192.168.0.0/0"]'
'''
conn = _get_conn(region, key, keyid, profile)
if not conn:
return False
try:
revoked = conn.revoke_security_group(
group_name=name, src_security_group_name=source_group_name,
src_security_group_owner_id=source_group_owner_id,
ip_protocol=ip_protocol, from_port=from_port, to_port=to_port,
cidr_ip=cidr_ip, group_id=group_id,
src_security_group_group_id=source_group_group_id)
if revoked:
log.info('Removed rule from security group {0}.'.format(name))
return True
else:
msg = 'Failed to remove rule from security group {0}.'.format(name)
log.error(msg)
return False
except boto.exception.EC2ResponseError as e:
log.debug(e)
msg = 'Failed to remove rule from security group {0}.'.format(name)
log.error(msg)
return False
def _get_conn(region, key, keyid, profile):
'''
Get a boto connection to ec2.
'''
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']('secgroup.region'):
region = __salt__['config.option']('secgroup.region')
if not region:
region = 'us-east-1'
if not key and __salt__['config.option']('secgroup.key'):
key = __salt__['config.option']('secgroup.key')
if not keyid and __salt__['config.option']('secgroup.keyid'):
keyid = __salt__['config.option']('secgroup.keyid')
try:
conn = boto.ec2.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 ec2 connection for security groups.')
return None
return conn

View File

@ -0,0 +1,335 @@
# -*- coding: utf-8 -*-
'''
Manage Security Groups
======================
.. versionadded:: Helium
Create and destroy Security 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 ec2 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::
secgroup.keyid: GKTADJGHEIQSXMKKRBJ08H
secgroup.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 mysecgroup exists:
boto_secgroup.present:
- name: mysecgroup
- description: My security group
- rules:
- ip_protocol: tcp
from_port: 80
to_port: 80
cidr_ip:
- 10.0.0.0/0
- 192.168.0.0/0
- region: us-east-1
- keyid: GKTADJGHEIQSXMKKRBJ08H
- key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
# Using a profile from pillars
Ensure mysecgroup exists:
boto_secgroup.present:
- name: mysecgroup
- description: My security group
- region: us-east-1
- profile: myprofile
# Passing in a profile
Ensure mysecgroup exists:
boto_secgroup.present:
- name: mysecgroup
- description: My security group
- region: us-east-1
- profile:
keyid: GKTADJGHEIQSXMKKRBJ08H
key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
'''
import salt.utils.dictupdate as dictupdate
from salt.exceptions import SaltInvocationError
def __virtual__():
'''
Only load if boto is available.
'''
return 'boto_secgroup' if 'boto_secgroup.exists' in __salt__ else False
def present(
name,
description,
vpc_id=None,
rules=None,
region=None,
key=None,
keyid=None,
profile=None):
'''
Ensure the security group exists with the specified rules.
name
Name of the security group.
description
A description of this security group.
vpc_id
The ID of the VPC to create the security group in, if any.
rules
A list of ingress rule dicts.
region
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': {}}
_ret = _security_group_present(name, description, vpc_id, region, key,
keyid, profile)
ret['changes'] = _ret['changes']
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
if _ret['result'] is not None:
ret['result'] = _ret['result']
if ret['result'] is False:
return ret
if not rules:
rules = []
_ret = _rules_present(name, rules, region, key, keyid, profile)
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
if _ret['result'] is not None:
ret['result'] = _ret['result']
return ret
def _security_group_present(
name,
description,
vpc_id,
region,
key,
keyid,
profile):
ret = {'result': None, 'comment': '', 'changes': {}}
exists = __salt__['boto_secgroup.exists'](name, region, key, keyid,
profile)
if not exists:
if __opts__['test']:
msg = 'Security group {0} is set to be created.'.format(name)
ret['comment'] = msg
return ret
created = __salt__['boto_secgroup.create'](name, description, vpc_id,
region, key, keyid, profile)
if created:
ret['result'] = True
ret['changes']['old'] = {'secgroup': None}
sg = __salt__['boto_secgroup.get_config'](name, None, region, key,
keyid, profile)
ret['changes']['new'] = {'secgroup': sg}
ret['comment'] = 'Security group {0} created.'.format(name)
else:
ret['result'] = False
msg = 'Failed to create {0} security group.'.format(name)
ret['comment'] = msg
else:
ret['comment'] = 'Security group {0} present.'.format(name)
return ret
def _get_rule_changes(rules, _rules):
to_delete = []
to_create = []
for rule in rules:
try:
ip_protocol = rule.get('ip_protocol')
to_port = rule.get('to_port')
from_port = rule.get('from_port')
except KeyError:
raise SaltInvocationError('ip_protocol, to_port, and from_port are'
' required arguments for security group'
' rules.')
supported_protocols = ['tcp', 'udp', 'icmp', 'all']
if ip_protocol not in supported_protocols:
msg = ('Invalid ip_protocol {0} specified in security group rule.')
raise SaltInvocationError(msg.format(ip_protocol))
cidr_ip = rule.get('cidr_ip', None)
group_name = rule.get('source_group_name', None)
group_id = rule.get('source_group_group_id', None)
owner_id = rule.get('source_group_owner_id', None)
if cidr_ip and (group_id or group_name):
raise SaltInvocationError('cidr_ip and source groups can not both'
' be specified in security group rules.')
if group_id and group_name:
raise SaltInvocationError('Either source_group_group_id or'
' source_group_name can be specified in'
' security group rules, but not both.')
if not (cidr_ip or group_id or group_name):
raise SaltInvocationError('cidr_ip, source_group_group_id, or'
' source_group_name must be provided for'
' security group rules.')
rule_found = False
for _rule in _rules:
if (ip_protocol == _rule['ip_protocol'] and
from_port == _rule['from_port'] and
to_port == _rule['to_port']):
_cidr_ip = _rule.get('cidr_ip', None)
_owner_id = _rule.get('source_group_owner_id', None)
_group_id = _rule.get('source_group_group_id', None)
_group_name = _rule.get('source_group_name', None)
if (cidr_ip == _cidr_ip or owner_id == _owner_id or
group_id == _group_id or group_name == _group_name):
rule_found = True
if not rule_found:
to_create.append(rule)
for _rule in _rules:
_ip_protocol = _rule.get('ip_protocol')
_to_port = _rule.get('to_port')
_from_port = _rule.get('from_port')
_cidr_ip = _rule.get('cidr_ip', None)
_owner_id = _rule.get('source_group_owner_id', None)
_group_id = _rule.get('source_group_group_id', None)
_group_name = _rule.get('source_group_name', None)
rule_found = False
for rule in rules:
cidr_ip = rule.get('cidr_ip', None)
group_name = rule.get('source_group_name', None)
group_id = rule.get('source_group_group_id', None)
owner_id = rule.get('source_group_owner_id', None)
if (rule['ip_protocol'] == _ip_protocol and
rule['from_port'] == _from_port and
rule['to_port'] == _to_port):
if (cidr_ip == _cidr_ip or owner_id == _owner_id or
group_id == _group_id or group_name == _group_name):
rule_found = True
if not rule_found:
# Can only supply name or id, not both. Since we're deleting
# entries, it doesn't matter which we pick.
_rule.pop('source_group_name', None)
to_delete.append(_rule)
return (to_delete, to_create)
def _rules_present(
name,
rules,
region,
key,
keyid,
profile):
ret = {'result': None, 'comment': '', 'changes': {}}
sg = __salt__['boto_secgroup.get_config'](name, None, region, key, keyid,
profile)
if not sg:
msg = '{0} security group configuration could not be retreived.'
ret['comment'] = msg.format(name)
ret['result'] = False
return ret
to_delete, to_create = _get_rule_changes(rules, sg['rules'])
if to_create or to_delete:
if __opts__['test']:
msg = 'Security group {0} set to have rules modified.'.format(name)
ret['comment'] = msg
return ret
if to_delete:
deleted = True
for rule in to_delete:
_deleted = __salt__['boto_secgroup.revoke'](
name, region=region, key=key, keyid=keyid, profile=profile,
**rule)
if not _deleted:
deleted = False
if deleted:
msg = 'Removed rules on {0} security group.'.format(name)
ret['comment'] = msg
ret['result'] = True
else:
msg = 'Failed to remove rules on {0} security group.'
ret['comment'] = msg.format(name)
ret['result'] = False
if to_create:
created = True
for rule in to_create:
_created = __salt__['boto_secgroup.authorize'](
name, region=region, key=key, keyid=keyid, profile=profile,
**rule)
if not _created:
created = False
if created:
msg = 'Created rules on {0} security group.'
ret['comment'] = ' '.join([ret['comment'], msg.format(name)])
if ret['result'] is not False:
ret['result'] = True
else:
msg = 'Failed to create rules on {0} security group.'
ret['comment'] = ' '.join([ret['comment'], msg.format(name)])
ret['result'] = False
ret['changes']['old'] = {'rules': sg['rules']}
sg = __salt__['boto_secgroup.get_config'](name, None, region, key,
keyid, profile)
ret['changes']['new'] = {'rules': sg['rules']}
return ret
def absent(
name,
region=None,
key=None,
keyid=None,
profile=None):
ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}
sg = __salt__['boto_secgroup.get_config'](name, None, region, key, keyid,
profile)
if sg:
if __opts__['test']:
ret['result'] = None
msg = 'Security group {0} is set to be removed.'.format(name)
ret['comment'] = msg
return ret
deleted = __salt__['boto_secgroup.delete'](name, None, region, key,
keyid, profile)
if deleted:
ret['result'] = True
ret['changes']['old'] = {'secgroup': sg}
ret['changes']['new'] = {'secgroup': None}
ret['comment'] = 'Security group {0} deleted.'.format(name)
else:
ret['result'] = False
msg = 'Failed to delete {0} security group.'.format(name)
ret['comment'] = msg
else:
ret['comment'] = '{0} security group does not exist.'.format(name)
return ret