diff --git a/library/cloud/ec2_group b/library/cloud/ec2_group index 1dd463cc8d6..bf40e7b83b7 100644 --- a/library/cloud/ec2_group +++ b/library/cloud/ec2_group @@ -24,8 +24,12 @@ options: required: false rules: description: - - List of firewall rules to enforce in this group (see example). - required: true + - List of firewall inbound rules to enforce in this group (see example). + required: false + rules_egress: + description: + - List of firewall outbound rules to enforce in this group (see example). + required: false region: description: - the EC2 region to use @@ -81,6 +85,11 @@ options: version_added: "1.6" requirements: [ "boto" ] + +notes: + - If a rule declares a group_name and that group doesn't exist, it will be + automatically created. In that case, group_desc should be provided as well. + The module will refuse to create a depended-on group without a description. ''' EXAMPLES = ''' @@ -113,6 +122,13 @@ EXAMPLES = ''' - proto: all # the containing group name may be specified here group_name: example + rules_egress: + - proto: tcp + from_port: 80 + to_port: 80 + group_name: example-other + # description to use if example-other needs to be created + group_desc: other example EC2 group ''' try: @@ -128,6 +144,55 @@ def addRulesToLookup(rules, prefix, dict): dict["%s-%s-%s-%s-%s-%s" % (prefix, rule.ip_protocol, rule.from_port, rule.to_port, grant.group_id, grant.cidr_ip)] = rule + +def get_target_from_rule(rule, name, groups): + """ + Returns tuple of (group_id, ip) after validating rule params. + + rule: Dict describing a rule. + name: Name of the security group being managed. + groups: Dict of all available security groups. + + AWS accepts an ip range or a security group as target of a rule. This + function validate the rule specification and return either a non-None + group_id or a non-None ip range. + """ + + group_id = None + group_name = None + ip = 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 'group_name' in rule: + module.fail_json(msg="Specify group_id OR group_name, not both") + elif 'group_id' in rule: + group_id = rule['group_id'] + elif 'group_name' in rule: + group_name = rule['group_name'] + if group_name in groups: + group_id = groups[group_name].id + elif group_name == name: + group_id = group.id + groups[group_id] = group + groups[group_name] = group + 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)) + 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 + groups[group_id] = auto_group + groups[group_name] = auto_group + target_group_created = True + elif 'cidr_ip' in rule: + ip = rule['cidr_ip'] + + return group_id, ip, target_group_created + + def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( @@ -135,6 +200,7 @@ def main(): description=dict(required=True), vpc_id=dict(), rules=dict(), + rules_egress=dict(), state = dict(default='present', choices=['present', 'absent']), ) ) @@ -147,6 +213,7 @@ def main(): description = module.params['description'] vpc_id = module.params['vpc_id'] rules = module.params['rules'] + rules_egress = module.params['rules_egress'] state = module.params.get('state') changed = False @@ -197,39 +264,29 @@ def main(): '''no match found, create it''' if not module.check_mode: group = ec2.create_security_group(name, description, vpc_id=vpc_id) + + # 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 + group = ec2.get_all_security_groups(group_ids=(group.id,))[0] 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) # Now, go through all provided rules and ensure they are there. if rules: for rule in rules: - group_id = None - group_name = None - ip = None - 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 'group_name' in rule: - module.fail_json(msg="Specify group_id OR group_name, not both") - elif 'group_id' in rule: - group_id = rule['group_id'] - elif 'group_name' in rule: - group_name = rule['group_name'] - if group_name in groups: - group_id = groups[group_name].id - elif group_name == name: - group_id = group.id - groups[group_id] = group - groups[group_name] = group - elif 'cidr_ip' in rule: - ip = rule['cidr_ip'] + group_id, ip, target_group_created = get_target_from_rule(rule, name, groups) + if target_group_created: + changed = True if rule['proto'] == 'all': rule['proto'] = -1 @@ -260,6 +317,58 @@ def main(): group.revoke(rule.ip_protocol, rule.from_port, rule.to_port, grant.cidr_ip, grantGroup) changed = True + # Manage egress rules + groupRules = {} + addRulesToLookup(group.rules_egress, 'out', groupRules) + + # Now, go through all provided rules and ensure they are there. + if rules_egress: + for rule in rules_egress: + group_id, ip, target_group_created = get_target_from_rule(rule, name, groups) + if target_group_created: + changed = True + + if rule['proto'] == 'all': + rule['proto'] = -1 + rule['from_port'] = None + rule['to_port'] = None + + # If rule already exists, don't later delete it + ruleId = "%s-%s-%s-%s-%s-%s" % ('out', rule['proto'], rule['from_port'], rule['to_port'], group_id, ip) + 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=ip) + changed = True + + # Finally, remove anything left in the groupRules -- these will be defunct rules + for rule in groupRules.itervalues(): + for grant in rule.grants: + 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 + if group: module.exit_json(changed=changed, group_id=group.id) else: