Use Boto3 for ec2_group Fixes #23507 (#25340)

* Use Boto3 for ec2_group

Currently boto doesn't support ipv6. To support ipv6 in ec2_group, we need boto3.
boto3 has significant API changes, which caused more re-factoring for ec2_group module.
Added additional integration test to test_ec2_group role.

* Follow the standard for boto3 ansible

Fixed imports. Use boto3 ansible exception with camel_dict_to_snake_dict.
Refactored the call to authorize/revoke  ingress and egress.

* Removed dependancy with module ipaddress

Added new parameter called cidr_ipv6 for specifying
ipv6 addresses inline with how boto3 handles ipv6 addresses.

* Updated integration test

* Added ipv6 integration test for ec2_group

* Set purge_rules to false for integration test

* Fixed import statements

Added example for ipv6.
Removed defining HAS_BOTO3 variable and import HAS_BOTO3 from ec2.
Cleaned up import statements.

* Fixed exception handling

* Add IAM permissions for ec2_group tests

Missing AuthorizeSecurityGroupEgress necessary for latest tests

* Wrapped botocore import in try/except block

Import just botocore to be more similar to other modules
This commit is contained in:
sramakr 2017-07-16 22:03:31 -04:00 committed by Will Thames
parent 879acf378d
commit b980a5c02a
3 changed files with 370 additions and 156 deletions

View file

@ -35,6 +35,7 @@
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ec2:AuthorizeSecurityGroupIngress", "ec2:AuthorizeSecurityGroupIngress",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:CreateTags", "ec2:CreateTags",
"ec2:DeleteRouteTable", "ec2:DeleteRouteTable",
"ec2:DeleteSecurityGroup", "ec2:DeleteSecurityGroup",

View file

@ -19,12 +19,12 @@ ANSIBLE_METADATA = {'metadata_version': '1.0',
'status': ['stableinterface'], 'status': ['stableinterface'],
'supported_by': 'curated'} 'supported_by': 'curated'}
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: ec2_group module: ec2_group
author: "Andrew de Quincey (@adq)" author: "Andrew de Quincey (@adq)"
version_added: "1.3" version_added: "1.3"
requirements: [ boto3 ]
short_description: maintain an ec2 VPC security group. short_description: maintain an ec2 VPC security group.
description: description:
- maintains ec2 security groups. This module has a dependency on python-boto >= 2.5 - maintains ec2 security groups. This module has a dependency on python-boto >= 2.5
@ -143,6 +143,7 @@ EXAMPLES = '''
from_port: 80 from_port: 80
to_port: 80 to_port: 80
cidr_ip: 0.0.0.0/0 cidr_ip: 0.0.0.0/0
cidr_ipv6: 64:ff9b::/96
group_name: example-other group_name: example-other
# description to use if example-other needs to be created # description to use if example-other needs to be created
group_desc: other example EC2 group group_desc: other example EC2 group
@ -178,6 +179,9 @@ EXAMPLES = '''
cidr_ip: cidr_ip:
- 172.16.1.0/24 - 172.16.1.0/24
- 172.16.17.0/24 - 172.16.17.0/24
cidr_ipv6:
- 2607:F8B0::/32
- 64:ff9b::/96
group_id: group_id:
- sg-edcd9784 - sg-edcd9784
@ -191,17 +195,17 @@ import json
import re import re
import time import time
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ec2 import ec2_connect, ec2_argument_spec from ansible.module_utils.ec2 import boto3_conn
from ansible.module_utils.ec2 import get_aws_connection_info
from ansible.module_utils.ec2 import ec2_argument_spec
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
from ansible.module_utils.ec2 import HAS_BOTO3
import traceback
try: try:
import boto.ec2 import botocore
from boto.ec2.securitygroup import SecurityGroup
from boto.exception import BotoServerError
HAS_BOTO = True
except ImportError: except ImportError:
HAS_BOTO = False pass # caught by imported HAS_BOTO3
import traceback
def deduplicate_rules_args(rules): def deduplicate_rules_args(rules):
@ -212,35 +216,33 @@ def deduplicate_rules_args(rules):
def make_rule_key(prefix, rule, group_id, cidr_ip): def make_rule_key(prefix, rule, group_id, cidr_ip):
"""Creates a unique key for an individual group rule""" if 'proto' in rule:
if isinstance(rule, dict):
proto, from_port, to_port = [rule.get(x, None) for x in ('proto', 'from_port', 'to_port')] proto, from_port, to_port = [rule.get(x, None) for x in ('proto', 'from_port', 'to_port')]
# fix for 11177 elif 'IpProtocol' in rule:
proto, from_port, to_port = [rule.get(x, None) for x in ('IpProtocol', 'FromPort', 'ToPort')]
if proto not in ['icmp', 'tcp', 'udp'] and from_port == -1 and to_port == -1: if proto not in ['icmp', 'tcp', 'udp'] and from_port == -1 and to_port == -1:
from_port = 'none' from_port = 'none'
to_port = 'none' to_port = 'none'
else: # isinstance boto.ec2.securitygroup.IPPermissions
proto, from_port, to_port = [getattr(rule, x, None) for x in ('ip_protocol', 'from_port', 'to_port')]
key = "%s-%s-%s-%s-%s-%s" % (prefix, proto, from_port, to_port, group_id, cidr_ip) key = "%s-%s-%s-%s-%s-%s" % (prefix, proto, from_port, to_port, group_id, cidr_ip)
return key.lower().replace('-none', '-None') return key.lower().replace('-none', '-None')
def addRulesToLookup(rules, prefix, rules_dict): def add_rules_to_loopkup(ipPermissions, group_id, prefix, dict):
for rule in rules: for rule in ipPermissions:
for grant in rule.grants: for groupGrant in rule.get('UserIdGroupPairs'):
rules_dict[make_rule_key(prefix, rule, grant.group_id, grant.cidr_ip)] = (rule, grant) dict[make_rule_key(prefix, rule, group_id, groupGrant.get('GroupId'))] = (rule, groupGrant)
for ipv4Grants in rule.get('IpRanges'):
dict[make_rule_key(prefix, rule, group_id, ipv4Grants.get('CidrIp'))] = (rule, ipv4Grants)
for ipv6Grants in rule.get('Ipv6Ranges'):
dict[make_rule_key(prefix, rule, group_id, ipv6Grants.get('CidrIpv6'))] = (rule, ipv6Grants)
def validate_rule(module, rule): def validate_rule(module, rule):
VALID_PARAMS = ('cidr_ip', VALID_PARAMS = ('cidr_ip', 'cidr_ipv6',
'group_id', 'group_name', 'group_desc', 'group_id', 'group_name', 'group_desc',
'proto', 'from_port', 'to_port') 'proto', 'from_port', 'to_port')
if not isinstance(rule, dict): if not isinstance(rule, dict):
module.fail_json(msg='Invalid rule parameter type [%s].' % type(rule)) module.fail_json(msg='Invalid rule parameter type [%s].' % type(rule))
for k in rule: for k in rule:
if k not in VALID_PARAMS: if k not in VALID_PARAMS:
module.fail_json(msg='Invalid rule parameter \'{}\''.format(k)) module.fail_json(msg='Invalid rule parameter \'{}\''.format(k))
@ -249,6 +251,12 @@ def validate_rule(module, rule):
module.fail_json(msg='Specify group_id OR cidr_ip, not both') module.fail_json(msg='Specify group_id OR cidr_ip, not both')
elif 'group_name' in rule and 'cidr_ip' in rule: elif 'group_name' in rule and 'cidr_ip' in rule:
module.fail_json(msg='Specify group_name OR cidr_ip, not both') module.fail_json(msg='Specify group_name OR cidr_ip, not both')
elif 'group_id' in rule and 'cidr_ipv6' in rule:
module.fail_json(msg="Specify group_id OR cidr_ipv6, not both")
elif 'group_name' in rule and 'cidr_ipv6' in rule:
module.fail_json(msg="Specify group_name OR cidr_ipv6, not both")
elif 'cidr_ip' in rule and 'cidr_ipv6' in rule:
module.fail_json(msg="Specify cidr_ip OR cidr_ipv6, not both")
elif 'group_id' in rule and 'group_name' in rule: elif 'group_id' in rule and 'group_name' in rule:
module.fail_json(msg='Specify group_id OR group_name, not both') module.fail_json(msg='Specify group_id OR group_name, not both')
@ -270,17 +278,25 @@ def get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id):
group_id = None group_id = None
group_name = None group_name = None
ip = None ip = None
ipv6 = None
target_group_created = False target_group_created = False
if 'group_id' in rule and 'cidr_ip' in rule: if 'group_id' in rule and 'cidr_ip' in rule:
module.fail_json(msg="Specify group_id OR cidr_ip, not both") module.fail_json(msg="Specify group_id OR cidr_ip, not both")
elif 'group_name' in rule and 'cidr_ip' in rule: elif 'group_name' in rule and 'cidr_ip' in rule:
module.fail_json(msg="Specify group_name OR cidr_ip, not both") module.fail_json(msg="Specify group_name OR cidr_ip, not both")
elif 'group_id' in rule and 'cidr_ipv6' in rule:
module.fail_json(msg="Specify group_id OR cidr_ipv6, not both")
elif 'group_name' in rule and 'cidr_ipv6' in rule:
module.fail_json(msg="Specify group_name OR cidr_ipv6, not both")
elif 'group_id' in rule and 'group_name' in rule: elif 'group_id' in rule and 'group_name' in rule:
module.fail_json(msg="Specify group_id OR group_name, not both") module.fail_json(msg="Specify group_id OR group_name, not both")
elif rule.get('group_id') and re.match(FOREIGN_SECURITY_GROUP_REGEX, rule['group_id']): elif 'cidr_ip' in rule and 'cidr_ipv6' in rule:
module.fail_json(msg="Specify cidr_ip OR cidr_ipv6, not both")
elif 'group_id' in rule and re.match(FOREIGN_SECURITY_GROUP_REGEX, rule['group_id']):
# this is a foreign Security Group. Since you can't fetch it you must create an instance of it # this is a foreign Security Group. Since you can't fetch it you must create an instance of it
owner_id, group_id, group_name = re.match(FOREIGN_SECURITY_GROUP_REGEX, rule['group_id']).groups() owner_id, group_id, group_name = re.match(FOREIGN_SECURITY_GROUP_REGEX, rule['group_id']).groups()
group_instance = SecurityGroup(owner_id=owner_id, name=group_name, id=group_id) group_instance = SecurityGroup(owner_id=owner_id, group_name=group_name, id=group_id)
groups[group_id] = group_instance groups[group_id] = group_instance
groups[group_name] = group_instance groups[group_name] = group_instance
elif 'group_id' in rule: elif 'group_id' in rule:
@ -295,7 +311,8 @@ def get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id):
group_id = groups[group_name].id group_id = groups[group_name].id
else: else:
if not rule.get('group_desc', '').strip(): if not rule.get('group_desc', '').strip():
module.fail_json(msg="group %s will be automatically created by rule %s and no description was provided" % (group_name, rule)) module.fail_json(msg="group %s will be automatically created by rule %s and "
"no description was provided" % (group_name, rule))
if not module.check_mode: if not module.check_mode:
auto_group = ec2.create_security_group(group_name, rule['group_desc'], vpc_id=vpc_id) auto_group = ec2.create_security_group(group_name, rule['group_desc'], vpc_id=vpc_id)
group_id = auto_group.id group_id = auto_group.id
@ -304,8 +321,10 @@ def get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id):
target_group_created = True target_group_created = True
elif 'cidr_ip' in rule: elif 'cidr_ip' in rule:
ip = rule['cidr_ip'] ip = rule['cidr_ip']
elif 'cidr_ipv6' in rule:
ipv6 = rule['cidr_ipv6']
return group_id, ip, target_group_created return group_id, ip, ipv6, target_group_created
def ports_expand(ports): def ports_expand(ports):
@ -351,7 +370,7 @@ def rules_expand_ports(rules):
def rule_expand_source(rule, source_type): def rule_expand_source(rule, source_type):
# takes a rule dict and returns a list of expanded rule dicts for specified source_type # takes a rule dict and returns a list of expanded rule dicts for specified source_type
sources = rule[source_type] if isinstance(rule[source_type], list) else [rule[source_type]] sources = rule[source_type] if isinstance(rule[source_type], list) else [rule[source_type]]
source_types_all = ('cidr_ip', 'group_id', 'group_name') source_types_all = ('cidr_ip', 'cidr_ipv6', 'group_id', 'group_name')
rule_expanded = [] rule_expanded = []
for source in sources: for source in sources:
@ -366,7 +385,7 @@ def rule_expand_source(rule, source_type):
def rule_expand_sources(rule): def rule_expand_sources(rule):
# takes a rule dict and returns a list of expanded rule discts # takes a rule dict and returns a list of expanded rule discts
source_types = (stype for stype in ('cidr_ip', 'group_id', 'group_name') if stype in rule) source_types = (stype for stype in ('cidr_ip', 'cidr_ipv6', 'group_id', 'group_name') if stype in rule)
return [r for stype in source_types return [r for stype in source_types
for r in rule_expand_source(rule, stype)] for r in rule_expand_source(rule, stype)]
@ -381,6 +400,79 @@ def rules_expand_sources(rules):
for rule in rule_expand_sources(rule_complex)] for rule in rule_expand_sources(rule_complex)]
def authorize_ip(type, changed, client, group, groupRules,
ip, ip_permission, module, rule, ethertype):
# If rule already exists, don't later delete it
for thisip in ip:
rule_id = make_rule_key(type, rule, group.id, thisip)
if rule_id in groupRules:
del groupRules[rule_id]
else:
if not module.check_mode:
ip_permission = serialize_ip_grant(rule, thisip, ethertype)
if ip_permission:
try:
if type == "in":
client.authorize_security_group_ingress(GroupId=group.group_id,
IpPermissions=[ip_permission])
elif type == "out":
client.authorize_security_group_egress(GroupId=group.group_id,
IpPermissions=[ip_permission])
except botocore.exceptions.ClientError as e:
module.fail_json(msg="Unable to authorize %s for ip %s security group '%s' - %s" %
(type, thisip, group.group_name, e),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
changed = True
return changed, ip_permission
def serialize_group_grant(group_id, rule):
permission = {'IpProtocol': rule['proto'],
'FromPort': rule['from_port'],
'ToPort': rule['to_port'],
'UserIdGroupPairs': [{'GroupId': group_id}]}
return permission
def serialize_revoke(grant, rule):
permission = dict()
fromPort = rule['FromPort'] if 'FromPort' in rule else None
toPort = rule['ToPort'] if 'ToPort' in rule else None
if 'GroupId' in grant:
permission = {'IpProtocol': rule['IpProtocol'],
'FromPort': fromPort,
'ToPort': toPort,
'UserIdGroupPairs': [{'GroupId': grant['GroupId'], 'UserId': grant['UserId']}]
}
elif 'CidrIp' in grant:
permission = {'IpProtocol': rule['IpProtocol'],
'FromPort': fromPort,
'ToPort': toPort,
'IpRanges': [grant]
}
elif 'CidrIpv6' in grant:
permission = {'IpProtocol': rule['IpProtocol'],
'FromPort': fromPort,
'ToPort': toPort,
'Ipv6Ranges': [grant]
}
if rule['IpProtocol'] in ('all', '-1', -1):
del permission['FromPort']
del permission['ToPort']
return permission
def serialize_ip_grant(rule, thisip, ethertype):
permission = {'IpProtocol': rule['proto'],
'FromPort': rule['from_port'],
'ToPort': rule['to_port']}
if ethertype == "ipv4":
permission.update({'IpRanges': [{'CidrIp': thisip}]})
elif ethertype == "ipv6":
permission.update({'Ipv6Ranges': [{'CidrIpv6': thisip}]})
return permission
def main(): def main():
argument_spec = ec2_argument_spec() argument_spec = ec2_argument_spec()
argument_spec.update(dict( argument_spec.update(dict(
@ -392,8 +484,7 @@ def main():
rules_egress=dict(type='list'), rules_egress=dict(type='list'),
state=dict(default='present', type='str', choices=['present', 'absent']), state=dict(default='present', type='str', choices=['present', 'absent']),
purge_rules=dict(default=True, required=False, type='bool'), purge_rules=dict(default=True, required=False, type='bool'),
purge_rules_egress=dict(default=True, required=False, type='bool'), purge_rules_egress=dict(default=True, required=False, type='bool')
) )
) )
module = AnsibleModule( module = AnsibleModule(
@ -403,8 +494,8 @@ def main():
required_if=[['state', 'present', ['name']]], required_if=[['state', 'present', ['name']]],
) )
if not HAS_BOTO: if not HAS_BOTO3:
module.fail_json(msg='boto required for this module') module.fail_json(msg='boto3 required for this module')
name = module.params['name'] name = module.params['name']
group_id = module.params['group_id'] group_id = module.params['group_id']
@ -420,32 +511,43 @@ def main():
module.fail_json(msg='Must provide description when state is present.') module.fail_json(msg='Must provide description when state is present.')
changed = False changed = False
region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
ec2 = ec2_connect(module) if not region:
module.fail_json(msg="The AWS region must be specified as an "
# find the group if present "environment variable or in the AWS credentials "
"profile.")
client, ec2 = boto3_conn(module, conn_type='both', resource='ec2', endpoint=ec2_url, region=region, **aws_connect_params)
group = None group = None
groups = {} groups = dict()
security_groups = []
# do get all security groups
# find if the group is present
try: try:
security_groups = ec2.get_all_security_groups() response = client.describe_security_groups()
except BotoServerError as e: if 'SecurityGroups' in response:
module.fail_json(msg="Error in get_all_security_groups: %s" % e.message, exception=traceback.format_exc()) security_groups = response.get('SecurityGroups')
except botocore.exceptions.NoCredentialsError as e:
module.fail_json(msg="Error in describe_security_groups: %s" % "Unable to locate credentials", exception=traceback.format_exc())
except botocore.exceptions.ClientError as e:
module.fail_json(msg="Error in describe_security_groups: %s" % e, exception=traceback.format_exc(),
**camel_dict_to_snake_dict(e.response))
for curGroup in security_groups: for sg in security_groups:
groups[curGroup.id] = curGroup curGroup = ec2.SecurityGroup(sg['GroupId'])
if curGroup.name in groups: groups[curGroup.id] = ec2.SecurityGroup(curGroup.id)
groupName = curGroup.group_name
if groupName in groups:
# Prioritise groups from the current VPC # Prioritise groups from the current VPC
if vpc_id is None or curGroup.vpc_id == vpc_id: if vpc_id is None or curGroup.vpc_id == vpc_id:
groups[curGroup.name] = curGroup groups[groupName] = curGroup
else: else:
groups[curGroup.name] = curGroup groups[groupName] = curGroup
if group_id: if group_id:
if curGroup.id == group_id: if curGroup.id == group_id:
group = curGroup group = curGroup
else: else:
if curGroup.name == name and (vpc_id is None or curGroup.vpc_id == vpc_id): if groupName == name and (vpc_id is None or curGroup.vpc_id == vpc_id):
group = curGroup group = curGroup
# Ensure requested group is absent # Ensure requested group is absent
@ -455,8 +557,9 @@ def main():
try: try:
if not module.check_mode: if not module.check_mode:
group.delete() group.delete()
except BotoServerError as e: except botocore.exceptions.ClientError as e:
module.fail_json(msg="Unable to delete security group '%s' - %s" % (group, e.message), exception=traceback.format_exc()) module.fail_json(msg="Unable to delete security group '%s' - %s" % (group, e),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
else: else:
group = None group = None
changed = True changed = True
@ -469,40 +572,41 @@ def main():
if group: if group:
# existing group # existing group
if group.description != description: if group.description != description:
module.fail_json(msg="Group description does not match existing group. ec2_group does not support this case.") module.fail_json(
msg="Group description does not match existing group. ec2_group does not support this case.")
# if the group doesn't exist, create it now # if the group doesn't exist, create it now
else: else:
# no match found, create it # no match found, create it
if not module.check_mode: if not module.check_mode:
group = ec2.create_security_group(name, description, vpc_id=vpc_id) group = client.create_security_group(GroupName=name, Description=description)
groupId = group.get('GroupId')
# When a group is created, an egress_rule ALLOW ALL # When a group is created, an egress_rule ALLOW ALL
# to 0.0.0.0/0 is added automatically but it's not # to 0.0.0.0/0 is added automatically but it's not
# reflected in the object returned by the AWS API # reflected in the object returned by the AWS API
# call. We re-read the group for getting an updated object # call. We re-read the group for getting an updated object
# amazon sometimes takes a couple seconds to update the security group so wait till it exists # amazon sometimes takes a couple seconds to update the security group so wait till it exists
while len(ec2.get_all_security_groups(filters={'group_id': group.id})) == 0: while len(client.describe_security_groups(GroupIds=[groupId])
['SecurityGroups'][0]['IpPermissionsEgress']) == 0:
time.sleep(0.1) time.sleep(0.1)
group = ec2.get_all_security_groups(group_ids=(group.id,))[0] group = ec2.SecurityGroup(groupId)
changed = True changed = True
else: else:
module.fail_json(msg="Unsupported state requested: %s" % state) module.fail_json(msg="Unsupported state requested: %s" % state)
# create a lookup for all existing rules on the group # create a lookup for all existing rules on the group
if group: if group:
# Manage ingress rules # Manage ingress rules
groupRules = {} groupRules = {}
addRulesToLookup(group.rules, 'in', groupRules) add_rules_to_loopkup(group.ip_permissions, group.id, 'in', groupRules)
# Now, go through all provided rules and ensure they are there. # Now, go through all provided rules and ensure they are there.
if rules is not None: if rules is not None:
ip_permission = []
for rule in rules: for rule in rules:
validate_rule(module, rule) validate_rule(module, rule)
group_id, ip, ipv6, target_group_created = get_target_from_rule(module, ec2, rule, name,
group_id, ip, target_group_created = get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id) group, groups, vpc_id)
if target_group_created: if target_group_created:
changed = True changed = True
@ -511,49 +615,63 @@ def main():
rule['from_port'] = None rule['from_port'] = None
rule['to_port'] = None rule['to_port'] = None
if group_id:
rule_id = make_rule_key('in', rule, group.id, group_id)
if rule_id in groupRules:
del groupRules[rule_id]
else:
if not module.check_mode:
ip_permission = serialize_group_grant(group_id, rule)
if ip_permission:
ips = ip_permission
if vpc_id:
[useridpair.update({'VpcId': vpc_id}) for useridpair in
ip_permission.get('UserIdGroupPairs')]
try:
client.authorize_security_group_ingress(GroupId=group.group_id, IpPermissions=[ips])
except botocore.exceptions.ClientError as e:
module.fail_json(
msg="Unable to authorize ingress for group %s security group '%s' - %s" %
(group_id, group.group_name, e),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
changed = True
elif ip:
# Convert ip to list we can iterate over # Convert ip to list we can iterate over
if not isinstance(ip, list): if ip and not isinstance(ip, list):
ip = [ip] ip = [ip]
changed, ip_permission = authorize_ip("in", changed, client, group, groupRules, ip, ip_permission,
module, rule, "ipv4")
elif ipv6:
# Convert ip to list we can iterate over
if not isinstance(ipv6, list):
ipv6 = [ipv6]
# If rule already exists, don't later delete it # If rule already exists, don't later delete it
for thisip in ip: changed, ip_permission = authorize_ip("in", changed, client, group, groupRules, ipv6, ip_permission,
ruleId = make_rule_key('in', rule, group_id, thisip) module, rule, "ipv6")
if ruleId not in groupRules:
grantGroup = None
if group_id:
grantGroup = groups[group_id]
if not module.check_mode:
group.authorize(rule['proto'], rule['from_port'], rule['to_port'], thisip, grantGroup)
changed = True
else:
del groupRules[ruleId]
# Finally, remove anything left in the groupRules -- these will be defunct rules # Finally, remove anything left in the groupRules -- these will be defunct rules
if purge_rules: if purge_rules:
for (rule, grant) in groupRules.values(): for (rule, grant) in groupRules.values():
grantGroup = None ip_permission = serialize_revoke(grant, rule)
if grant.group_id:
if grant.owner_id != group.owner_id:
# this is a foreign Security Group. Since you can't fetch it you must create an instance of it
group_instance = SecurityGroup(owner_id=grant.owner_id, name=grant.name, id=grant.group_id)
groups[grant.group_id] = group_instance
groups[grant.name] = group_instance
grantGroup = groups[grant.group_id]
if not module.check_mode: if not module.check_mode:
group.revoke(rule.ip_protocol, rule.from_port, rule.to_port, grant.cidr_ip, grantGroup) try:
client.revoke_security_group_ingress(GroupId=group.group_id, IpPermissions=[ip_permission])
except botocore.exceptions.ClientError as e:
module.fail_json(
msg="Unable to revoke ingress for security group '%s' - %s" %
(group.group_name, e),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
changed = True changed = True
# Manage egress rules # Manage egress rules
groupRules = {} groupRules = {}
addRulesToLookup(group.rules_egress, 'out', groupRules) add_rules_to_loopkup(group.ip_permissions_egress, group.id, 'out', groupRules)
# Now, go through all provided rules and ensure they are there. # Now, go through all provided rules and ensure they are there.
if rules_egress is not None: if rules_egress is not None:
for rule in rules_egress: for rule in rules_egress:
validate_rule(module, rule) validate_rule(module, rule)
group_id, ip, ipv6, target_group_created = get_target_from_rule(module, ec2, rule, name,
group_id, ip, target_group_created = get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id) group, groups, vpc_id)
if target_group_created: if target_group_created:
changed = True changed = True
@ -562,45 +680,58 @@ def main():
rule['from_port'] = None rule['from_port'] = None
rule['to_port'] = None rule['to_port'] = None
if group_id:
rule_id = make_rule_key('out', rule, group.id, group_id)
if rule_id in groupRules:
del groupRules[rule_id]
else:
if not module.check_mode:
ip_permission = serialize_group_grant(group_id, rule)
if ip_permission:
ips = ip_permission
if vpc_id:
[useridpair.update({'VpcId': vpc_id}) for useridpair in
ip_permission.get('UserIdGroupPairs')]
try:
client.authorize_security_group_egress(GroupId=group.group_id, IpPermissions=[ips])
except botocore.exceptions.ClientError as e:
module.fail_json(
msg="Unable to authorize egress for group %s security group '%s' - %s" %
(group_id, group.group_name, e),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
changed = True
elif ip:
# Convert ip to list we can iterate over # Convert ip to list we can iterate over
if not isinstance(ip, list): if not isinstance(ip, list):
ip = [ip] ip = [ip]
changed, ip_permission = authorize_ip("out", changed, client, group, groupRules, ip,
ip_permission, module, rule, "ipv4")
elif ipv6:
# Convert ip to list we can iterate over
if not isinstance(ipv6, list):
ipv6 = [ipv6]
# If rule already exists, don't later delete it # If rule already exists, don't later delete it
for thisip in ip: changed, ip_permission = authorize_ip("out", changed, client, group, groupRules, ipv6,
ruleId = make_rule_key('out', rule, group_id, thisip) ip_permission, module, rule, "ipv6")
if ruleId in groupRules:
del groupRules[ruleId]
# Otherwise, add new rule
else:
grantGroup = None
if group_id:
grantGroup = groups[group_id].id
if not module.check_mode:
ec2.authorize_security_group_egress(
group_id=group.id,
ip_protocol=rule['proto'],
from_port=rule['from_port'],
to_port=rule['to_port'],
src_group_id=grantGroup,
cidr_ip=thisip)
changed = True
else: else:
# when no egress rules are specified, # when no egress rules are specified,
# we add in a default allow all out rule, which was the # we add in a default allow all out rule, which was the
# default behavior before egress rules were added # default behavior before egress rules were added
default_egress_rule = 'out--1-None-None-None-0.0.0.0/0' default_egress_rule = 'out--1-None-None-' + group.id + '-0.0.0.0/0'
if default_egress_rule not in groupRules: if default_egress_rule not in groupRules:
if not module.check_mode: if not module.check_mode:
ec2.authorize_security_group_egress( ip_permission = [{'IpProtocol': '-1',
group_id=group.id, 'IpRanges': [{'CidrIp': '0.0.0.0/0'}]
ip_protocol=-1, }
from_port=None, ]
to_port=None, try:
src_group_id=None, client.authorize_security_group_egress(GroupId=group.group_id, IpPermissions=ip_permission)
cidr_ip='0.0.0.0/0' except botocore.exceptions.ClientError as e:
) module.fail_json(msg="Unable to authorize egress for ip %s security group '%s' - %s" %
('0.0.0.0/0',
group.group_name,
e),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
changed = True changed = True
else: else:
# make sure the default egress rule is not removed # make sure the default egress rule is not removed
@ -609,17 +740,18 @@ def main():
# Finally, remove anything left in the groupRules -- these will be defunct rules # Finally, remove anything left in the groupRules -- these will be defunct rules
if purge_rules_egress: if purge_rules_egress:
for (rule, grant) in groupRules.values(): for (rule, grant) in groupRules.values():
grantGroup = None # we shouldn't be revoking 0.0.0.0 egress
if grant.group_id: if grant != '0.0.0.0/0':
grantGroup = groups[grant.group_id].id ip_permission = serialize_revoke(grant, rule)
if not module.check_mode: if not module.check_mode:
ec2.revoke_security_group_egress( try:
group_id=group.id, client.revoke_security_group_egress(GroupId=group.group_id, IpPermissions=[ip_permission])
ip_protocol=rule.ip_protocol, except botocore.exceptions.ClientError as e:
from_port=rule.from_port, module.fail_json(msg="Unable to revoke egress for ip %s security group '%s' - %s" %
to_port=rule.to_port, (grant,
src_group_id=grantGroup, group.group_name,
cidr_ip=grant.cidr_ip) e),
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
changed = True changed = True
if group: if group:
@ -627,6 +759,5 @@ def main():
else: else:
module.exit_json(changed=changed, group_id=None) module.exit_json(changed=changed, group_id=None)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -63,21 +63,6 @@
- 'result.failed' - 'result.failed'
- 'result.msg == "Must provide description when state is present."' - 'result.msg == "Must provide description when state is present."'
# ============================================================
- name: test invalid region parameter
ec2_group:
name='{{ec2_group_name}}'
description='{{ec2_group_description}}'
region='asdf querty 1234'
register: result
ignore_errors: true
- name: assert invalid region parameter
assert:
that:
- 'result.failed'
- 'result.msg.startswith("Region asdf querty 1234 does not seem to be available for aws module boto.ec2. If the region definitely exists, you may need to upgrade boto or extend with endpoints_path")'
# ============================================================ # ============================================================
- name: test valid region parameter - name: test valid region parameter
ec2_group: ec2_group:
@ -91,7 +76,7 @@
assert: assert:
that: that:
- 'result.failed' - 'result.failed'
- 'result.msg.startswith("No handler was ready to authenticate.")' - '"Unable to locate credentials" in result.msg'
# ============================================================ # ============================================================
- name: test environment variable EC2_REGION - name: test environment variable EC2_REGION
@ -107,7 +92,7 @@
assert: assert:
that: that:
- 'result.failed' - 'result.failed'
- 'result.msg.startswith("No handler was ready to authenticate.")' - '"Unable to locate credentials" in result.msg'
# ============================================================ # ============================================================
- name: test invalid ec2_url parameter - name: test invalid ec2_url parameter
@ -123,7 +108,7 @@
assert: assert:
that: that:
- 'result.failed' - 'result.failed'
- 'result.msg.startswith("No handler was ready to authenticate.")' - 'result.msg.startswith("The AWS region must be specified as an environment variable or in the AWS credentials profile")'
# ============================================================ # ============================================================
- name: test valid ec2_url parameter - name: test valid ec2_url parameter
@ -139,7 +124,7 @@
assert: assert:
that: that:
- 'result.failed' - 'result.failed'
- 'result.msg.startswith("No handler was ready to authenticate.")' - 'result.msg.startswith("The AWS region must be specified as an environment variable or in the AWS credentials profile")'
# ============================================================ # ============================================================
- name: test credentials from environment - name: test credentials from environment
@ -157,7 +142,7 @@
assert: assert:
that: that:
- 'result.failed' - 'result.failed'
- '"Error in get_all_security_groups: AWS was not able to validate the provided access credentials" in result.msg' - '"validate the provided access credentials" in result.msg'
# ============================================================ # ============================================================
- name: test credential parameters - name: test credential parameters
@ -174,7 +159,7 @@
assert: assert:
that: that:
- 'result.failed' - 'result.failed'
- '"Error in get_all_security_groups: AWS was not able to validate the provided access credentials" in result.msg' - '"validate the provided access credentials" in result.msg'
# ============================================================ # ============================================================
- name: test state=absent - name: test state=absent
@ -243,6 +228,103 @@
- 'not result.changed' - 'not result.changed'
- 'result.group_id.startswith("sg-")' - 'result.group_id.startswith("sg-")'
# ============================================================
- name: test state=present for ipv6 (expected changed=true)
ec2_group:
name: '{{ec2_group_name}}'
description: '{{ec2_group_description}}'
ec2_region: '{{ec2_region}}'
ec2_access_key: '{{ec2_access_key}}'
ec2_secret_key: '{{ec2_secret_key}}'
security_token: '{{security_token}}'
state: present
rules:
- proto: "tcp"
from_port: 8182
to_port: 8182
cidr_ipv6: "64:ff9b::/96"
register: result
- name: assert state=present (expected changed=true)
assert:
that:
- 'result.changed'
- 'result.group_id.startswith("sg-")'
# ============================================================
- name: test rules_egress state=present for ipv6 (expected changed=true)
ec2_group:
name: '{{ec2_group_name}}'
description: '{{ec2_group_description}}'
ec2_region: '{{ec2_region}}'
ec2_access_key: '{{ec2_access_key}}'
ec2_secret_key: '{{ec2_secret_key}}'
security_token: '{{security_token}}'
state: present
rules:
- proto: "tcp"
from_port: 8182
to_port: 8182
cidr_ipv6: "64:ff9b::/96"
rules_egress:
- proto: "tcp"
from_port: 8181
to_port: 8181
cidr_ipv6: "64:ff9b::/96"
register: result
- name: assert state=present (expected changed=true)
assert:
that:
- 'result.changed'
- 'result.group_id.startswith("sg-")'
# ============================================================
- name: test state=present for ipv4 (expected changed=true)
ec2_group:
name: '{{ec2_group_name}}'
description: '{{ec2_group_description}}'
ec2_region: '{{ec2_region}}'
ec2_access_key: '{{ec2_access_key}}'
ec2_secret_key: '{{ec2_secret_key}}'
security_token: '{{security_token}}'
state: present
rules:
- proto: "tcp"
from_port: 8182
to_port: 8182
cidr_ip: "1.1.1.1/32"
register: result
- name: assert state=present (expected changed=true)
assert:
that:
- 'result.changed'
- 'result.group_id.startswith("sg-")'
# ============================================================
- name: add same rule to the existing group (expected changed=false)
ec2_group:
name: '{{ec2_group_name}}'
description: '{{ec2_group_description}}'
ec2_region: '{{ec2_region}}'
ec2_access_key: '{{ec2_access_key}}'
ec2_secret_key: '{{ec2_secret_key}}'
security_token: '{{security_token}}'
state: present
rules:
- proto: "tcp"
from_port: 8182
to_port: 8182
cidr_ip: "1.1.1.1/32"
register: result
- name: assert state=present (expected changed=false)
assert:
that:
- 'not result.changed'
- 'result.group_id.startswith("sg-")'
# ============================================================ # ============================================================
- name: test state=absent (expected changed=true) - name: test state=absent (expected changed=true)
ec2_group: ec2_group: