* 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:
parent
879acf378d
commit
b980a5c02a
3 changed files with 370 additions and 156 deletions
|
@ -35,6 +35,7 @@
|
|||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:AuthorizeSecurityGroupIngress",
|
||||
"ec2:AuthorizeSecurityGroupEgress",
|
||||
"ec2:CreateTags",
|
||||
"ec2:DeleteRouteTable",
|
||||
"ec2:DeleteSecurityGroup",
|
||||
|
|
|
@ -19,12 +19,12 @@ ANSIBLE_METADATA = {'metadata_version': '1.0',
|
|||
'status': ['stableinterface'],
|
||||
'supported_by': 'curated'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ec2_group
|
||||
author: "Andrew de Quincey (@adq)"
|
||||
version_added: "1.3"
|
||||
requirements: [ boto3 ]
|
||||
short_description: maintain an ec2 VPC security group.
|
||||
description:
|
||||
- maintains ec2 security groups. This module has a dependency on python-boto >= 2.5
|
||||
|
@ -143,6 +143,7 @@ EXAMPLES = '''
|
|||
from_port: 80
|
||||
to_port: 80
|
||||
cidr_ip: 0.0.0.0/0
|
||||
cidr_ipv6: 64:ff9b::/96
|
||||
group_name: example-other
|
||||
# description to use if example-other needs to be created
|
||||
group_desc: other example EC2 group
|
||||
|
@ -178,6 +179,9 @@ EXAMPLES = '''
|
|||
cidr_ip:
|
||||
- 172.16.1.0/24
|
||||
- 172.16.17.0/24
|
||||
cidr_ipv6:
|
||||
- 2607:F8B0::/32
|
||||
- 64:ff9b::/96
|
||||
group_id:
|
||||
- sg-edcd9784
|
||||
|
||||
|
@ -191,17 +195,17 @@ import json
|
|||
import re
|
||||
import time
|
||||
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:
|
||||
import boto.ec2
|
||||
from boto.ec2.securitygroup import SecurityGroup
|
||||
from boto.exception import BotoServerError
|
||||
HAS_BOTO = True
|
||||
import botocore
|
||||
except ImportError:
|
||||
HAS_BOTO = False
|
||||
|
||||
import traceback
|
||||
pass # caught by imported HAS_BOTO3
|
||||
|
||||
|
||||
def deduplicate_rules_args(rules):
|
||||
|
@ -212,35 +216,33 @@ def deduplicate_rules_args(rules):
|
|||
|
||||
|
||||
def make_rule_key(prefix, rule, group_id, cidr_ip):
|
||||
"""Creates a unique key for an individual group rule"""
|
||||
if isinstance(rule, dict):
|
||||
if 'proto' in rule:
|
||||
proto, from_port, to_port = [rule.get(x, None) for x in ('proto', 'from_port', 'to_port')]
|
||||
# fix for 11177
|
||||
if proto not in ['icmp', 'tcp', 'udp'] and from_port == -1 and to_port == -1:
|
||||
from_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')]
|
||||
|
||||
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:
|
||||
from_port = 'none'
|
||||
to_port = 'none'
|
||||
key = "%s-%s-%s-%s-%s-%s" % (prefix, proto, from_port, to_port, group_id, cidr_ip)
|
||||
return key.lower().replace('-none', '-None')
|
||||
|
||||
|
||||
def addRulesToLookup(rules, prefix, rules_dict):
|
||||
for rule in rules:
|
||||
for grant in rule.grants:
|
||||
rules_dict[make_rule_key(prefix, rule, grant.group_id, grant.cidr_ip)] = (rule, grant)
|
||||
def add_rules_to_loopkup(ipPermissions, group_id, prefix, dict):
|
||||
for rule in ipPermissions:
|
||||
for groupGrant in rule.get('UserIdGroupPairs'):
|
||||
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):
|
||||
VALID_PARAMS = ('cidr_ip',
|
||||
VALID_PARAMS = ('cidr_ip', 'cidr_ipv6',
|
||||
'group_id', 'group_name', 'group_desc',
|
||||
'proto', 'from_port', 'to_port')
|
||||
|
||||
if not isinstance(rule, dict):
|
||||
module.fail_json(msg='Invalid rule parameter type [%s].' % type(rule))
|
||||
|
||||
for k in rule:
|
||||
if k not in VALID_PARAMS:
|
||||
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')
|
||||
elif 'group_name' in rule and 'cidr_ip' in rule:
|
||||
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:
|
||||
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_name = None
|
||||
ip = None
|
||||
ipv6 = None
|
||||
target_group_created = False
|
||||
|
||||
if 'group_id' in rule and 'cidr_ip' in rule:
|
||||
module.fail_json(msg="Specify group_id OR cidr_ip, not both")
|
||||
elif 'group_name' in rule and 'cidr_ip' in rule:
|
||||
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:
|
||||
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
|
||||
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_name] = group_instance
|
||||
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
|
||||
else:
|
||||
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:
|
||||
auto_group = ec2.create_security_group(group_name, rule['group_desc'], vpc_id=vpc_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
|
||||
elif 'cidr_ip' in rule:
|
||||
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):
|
||||
|
@ -351,7 +370,7 @@ def rules_expand_ports(rules):
|
|||
def rule_expand_source(rule, 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]]
|
||||
source_types_all = ('cidr_ip', 'group_id', 'group_name')
|
||||
source_types_all = ('cidr_ip', 'cidr_ipv6', 'group_id', 'group_name')
|
||||
|
||||
rule_expanded = []
|
||||
for source in sources:
|
||||
|
@ -366,7 +385,7 @@ def rule_expand_source(rule, source_type):
|
|||
|
||||
def rule_expand_sources(rule):
|
||||
# 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
|
||||
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)]
|
||||
|
||||
|
||||
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():
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
|
@ -392,8 +484,7 @@ def main():
|
|||
rules_egress=dict(type='list'),
|
||||
state=dict(default='present', type='str', choices=['present', 'absent']),
|
||||
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(
|
||||
|
@ -403,8 +494,8 @@ def main():
|
|||
required_if=[['state', 'present', ['name']]],
|
||||
)
|
||||
|
||||
if not HAS_BOTO:
|
||||
module.fail_json(msg='boto required for this module')
|
||||
if not HAS_BOTO3:
|
||||
module.fail_json(msg='boto3 required for this module')
|
||||
|
||||
name = module.params['name']
|
||||
group_id = module.params['group_id']
|
||||
|
@ -420,32 +511,43 @@ def main():
|
|||
module.fail_json(msg='Must provide description when state is present.')
|
||||
|
||||
changed = False
|
||||
|
||||
ec2 = ec2_connect(module)
|
||||
|
||||
# find the group if present
|
||||
region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
|
||||
if not region:
|
||||
module.fail_json(msg="The AWS region must be specified as an "
|
||||
"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
|
||||
groups = {}
|
||||
|
||||
groups = dict()
|
||||
security_groups = []
|
||||
# do get all security groups
|
||||
# find if the group is present
|
||||
try:
|
||||
security_groups = ec2.get_all_security_groups()
|
||||
except BotoServerError as e:
|
||||
module.fail_json(msg="Error in get_all_security_groups: %s" % e.message, exception=traceback.format_exc())
|
||||
response = client.describe_security_groups()
|
||||
if 'SecurityGroups' in response:
|
||||
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:
|
||||
groups[curGroup.id] = curGroup
|
||||
if curGroup.name in groups:
|
||||
for sg in security_groups:
|
||||
curGroup = ec2.SecurityGroup(sg['GroupId'])
|
||||
groups[curGroup.id] = ec2.SecurityGroup(curGroup.id)
|
||||
groupName = curGroup.group_name
|
||||
if groupName in groups:
|
||||
# Prioritise groups from the current VPC
|
||||
if vpc_id is None or curGroup.vpc_id == vpc_id:
|
||||
groups[curGroup.name] = curGroup
|
||||
groups[groupName] = curGroup
|
||||
else:
|
||||
groups[curGroup.name] = curGroup
|
||||
groups[groupName] = curGroup
|
||||
|
||||
if group_id:
|
||||
if curGroup.id == group_id:
|
||||
group = curGroup
|
||||
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
|
||||
|
||||
# Ensure requested group is absent
|
||||
|
@ -455,8 +557,9 @@ def main():
|
|||
try:
|
||||
if not module.check_mode:
|
||||
group.delete()
|
||||
except BotoServerError as e:
|
||||
module.fail_json(msg="Unable to delete security group '%s' - %s" % (group, e.message), exception=traceback.format_exc())
|
||||
except botocore.exceptions.ClientError as e:
|
||||
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:
|
||||
group = None
|
||||
changed = True
|
||||
|
@ -469,40 +572,41 @@ def main():
|
|||
if group:
|
||||
# existing group
|
||||
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
|
||||
else:
|
||||
# no match found, create it
|
||||
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
|
||||
# to 0.0.0.0/0 is added automatically but it's not
|
||||
# reflected in the object returned by the AWS API
|
||||
# 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
|
||||
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)
|
||||
|
||||
group = ec2.get_all_security_groups(group_ids=(group.id,))[0]
|
||||
group = ec2.SecurityGroup(groupId)
|
||||
changed = True
|
||||
else:
|
||||
module.fail_json(msg="Unsupported state requested: %s" % state)
|
||||
|
||||
# create a lookup for all existing rules on the group
|
||||
if group:
|
||||
|
||||
# Manage ingress rules
|
||||
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.
|
||||
if rules is not None:
|
||||
ip_permission = []
|
||||
for rule in rules:
|
||||
validate_rule(module, rule)
|
||||
|
||||
group_id, ip, target_group_created = get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id)
|
||||
group_id, ip, ipv6, target_group_created = get_target_from_rule(module, ec2, rule, name,
|
||||
group, groups, vpc_id)
|
||||
if target_group_created:
|
||||
changed = True
|
||||
|
||||
|
@ -511,49 +615,63 @@ def main():
|
|||
rule['from_port'] = None
|
||||
rule['to_port'] = None
|
||||
|
||||
# Convert ip to list we can iterate over
|
||||
if not isinstance(ip, list):
|
||||
ip = [ip]
|
||||
|
||||
# If rule already exists, don't later delete it
|
||||
for thisip in ip:
|
||||
ruleId = make_rule_key('in', rule, group_id, thisip)
|
||||
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
|
||||
if group_id:
|
||||
rule_id = make_rule_key('in', rule, group.id, group_id)
|
||||
if rule_id in groupRules:
|
||||
del groupRules[rule_id]
|
||||
else:
|
||||
del groupRules[ruleId]
|
||||
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
|
||||
if ip and not isinstance(ip, list):
|
||||
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
|
||||
changed, ip_permission = authorize_ip("in", changed, client, group, groupRules, ipv6, ip_permission,
|
||||
module, rule, "ipv6")
|
||||
# Finally, remove anything left in the groupRules -- these will be defunct rules
|
||||
if purge_rules:
|
||||
for (rule, grant) in groupRules.values():
|
||||
grantGroup = None
|
||||
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]
|
||||
ip_permission = serialize_revoke(grant, rule)
|
||||
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
|
||||
|
||||
# Manage egress rules
|
||||
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.
|
||||
if rules_egress is not None:
|
||||
for rule in rules_egress:
|
||||
validate_rule(module, rule)
|
||||
|
||||
group_id, ip, target_group_created = get_target_from_rule(module, ec2, rule, name, group, groups, vpc_id)
|
||||
group_id, ip, ipv6, target_group_created = get_target_from_rule(module, ec2, rule, name,
|
||||
group, groups, vpc_id)
|
||||
if target_group_created:
|
||||
changed = True
|
||||
|
||||
|
@ -562,45 +680,58 @@ def main():
|
|||
rule['from_port'] = None
|
||||
rule['to_port'] = None
|
||||
|
||||
# Convert ip to list we can iterate over
|
||||
if not isinstance(ip, list):
|
||||
ip = [ip]
|
||||
|
||||
# If rule already exists, don't later delete it
|
||||
for thisip in ip:
|
||||
ruleId = make_rule_key('out', rule, group_id, thisip)
|
||||
if ruleId in groupRules:
|
||||
del groupRules[ruleId]
|
||||
# Otherwise, add new rule
|
||||
if group_id:
|
||||
rule_id = make_rule_key('out', rule, group.id, group_id)
|
||||
if rule_id in groupRules:
|
||||
del groupRules[rule_id]
|
||||
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)
|
||||
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
|
||||
if not isinstance(ip, list):
|
||||
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
|
||||
changed, ip_permission = authorize_ip("out", changed, client, group, groupRules, ipv6,
|
||||
ip_permission, module, rule, "ipv6")
|
||||
else:
|
||||
# when no egress rules are specified,
|
||||
# we add in a default allow all out rule, which was the
|
||||
# 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 not module.check_mode:
|
||||
ec2.authorize_security_group_egress(
|
||||
group_id=group.id,
|
||||
ip_protocol=-1,
|
||||
from_port=None,
|
||||
to_port=None,
|
||||
src_group_id=None,
|
||||
cidr_ip='0.0.0.0/0'
|
||||
)
|
||||
ip_permission = [{'IpProtocol': '-1',
|
||||
'IpRanges': [{'CidrIp': '0.0.0.0/0'}]
|
||||
}
|
||||
]
|
||||
try:
|
||||
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 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
|
||||
else:
|
||||
# make sure the default egress rule is not removed
|
||||
|
@ -609,24 +740,24 @@ def main():
|
|||
# Finally, remove anything left in the groupRules -- these will be defunct rules
|
||||
if purge_rules_egress:
|
||||
for (rule, grant) in groupRules.values():
|
||||
grantGroup = None
|
||||
if grant.group_id:
|
||||
grantGroup = groups[grant.group_id].id
|
||||
if not module.check_mode:
|
||||
ec2.revoke_security_group_egress(
|
||||
group_id=group.id,
|
||||
ip_protocol=rule.ip_protocol,
|
||||
from_port=rule.from_port,
|
||||
to_port=rule.to_port,
|
||||
src_group_id=grantGroup,
|
||||
cidr_ip=grant.cidr_ip)
|
||||
changed = True
|
||||
# we shouldn't be revoking 0.0.0.0 egress
|
||||
if grant != '0.0.0.0/0':
|
||||
ip_permission = serialize_revoke(grant, rule)
|
||||
if not module.check_mode:
|
||||
try:
|
||||
client.revoke_security_group_egress(GroupId=group.group_id, IpPermissions=[ip_permission])
|
||||
except botocore.exceptions.ClientError as e:
|
||||
module.fail_json(msg="Unable to revoke egress for ip %s security group '%s' - %s" %
|
||||
(grant,
|
||||
group.group_name,
|
||||
e),
|
||||
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
||||
changed = True
|
||||
|
||||
if group:
|
||||
module.exit_json(changed=changed, group_id=group.id)
|
||||
else:
|
||||
module.exit_json(changed=changed, group_id=None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -63,21 +63,6 @@
|
|||
- 'result.failed'
|
||||
- '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
|
||||
ec2_group:
|
||||
|
@ -91,7 +76,7 @@
|
|||
assert:
|
||||
that:
|
||||
- 'result.failed'
|
||||
- 'result.msg.startswith("No handler was ready to authenticate.")'
|
||||
- '"Unable to locate credentials" in result.msg'
|
||||
|
||||
# ============================================================
|
||||
- name: test environment variable EC2_REGION
|
||||
|
@ -107,7 +92,7 @@
|
|||
assert:
|
||||
that:
|
||||
- 'result.failed'
|
||||
- 'result.msg.startswith("No handler was ready to authenticate.")'
|
||||
- '"Unable to locate credentials" in result.msg'
|
||||
|
||||
# ============================================================
|
||||
- name: test invalid ec2_url parameter
|
||||
|
@ -123,7 +108,7 @@
|
|||
assert:
|
||||
that:
|
||||
- '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
|
||||
|
@ -139,7 +124,7 @@
|
|||
assert:
|
||||
that:
|
||||
- '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
|
||||
|
@ -157,7 +142,7 @@
|
|||
assert:
|
||||
that:
|
||||
- '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
|
||||
|
@ -174,7 +159,7 @@
|
|||
assert:
|
||||
that:
|
||||
- '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
|
||||
|
@ -243,6 +228,103 @@
|
|||
- 'not result.changed'
|
||||
- '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)
|
||||
ec2_group:
|
||||
|
|
Loading…
Reference in a new issue