mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-27 04:41:26 -07:00
* Move profile and region checking to module_utils.ec2 Remove ProfileNotFound checking from individual modules There are plenty of `if not region:` checks that could be removed, once more thorough testing of this change has occured The ec2_asg, iam_managed_policy and ec2_vpc_subnet_facts modules would also benefit from this change but as they do not have tests and are marked stableinterface, they do not get this change.
969 lines
40 KiB
Python
969 lines
40 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
'status': ['stableinterface'],
|
|
'supported_by': 'core'}
|
|
|
|
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
|
|
options:
|
|
name:
|
|
description:
|
|
- Name of the security group.
|
|
- One of and only one of I(name) or I(group_id) is required.
|
|
- Required if I(state=present).
|
|
required: false
|
|
group_id:
|
|
description:
|
|
- Id of group to delete (works only with absent).
|
|
- One of and only one of I(name) or I(group_id) is required.
|
|
required: false
|
|
version_added: "2.4"
|
|
description:
|
|
description:
|
|
- Description of the security group. Required when C(state) is C(present).
|
|
required: false
|
|
vpc_id:
|
|
description:
|
|
- ID of the VPC to create the group in.
|
|
required: false
|
|
rules:
|
|
description:
|
|
- List of firewall inbound rules to enforce in this group (see example). If none are supplied,
|
|
no inbound rules will be enabled. Rules list may include its own name in `group_name`.
|
|
This allows idempotent loopback additions (e.g. allow group to access itself).
|
|
Rule sources list support was added in version 2.4. This allows to define multiple sources per
|
|
source type as well as multiple source types per rule. Prior to 2.4 an individual source is allowed.
|
|
In version 2.5 support for rule descriptions was added.
|
|
required: false
|
|
rules_egress:
|
|
description:
|
|
- List of firewall outbound rules to enforce in this group (see example). If none are supplied,
|
|
a default all-out rule is assumed. If an empty list is supplied, no outbound rules will be enabled.
|
|
Rule Egress sources list support was added in version 2.4. In version 2.5 support for rule descriptions
|
|
was added.
|
|
required: false
|
|
version_added: "1.6"
|
|
state:
|
|
version_added: "1.4"
|
|
description:
|
|
- Create or delete a security group
|
|
required: false
|
|
default: 'present'
|
|
choices: [ "present", "absent" ]
|
|
aliases: []
|
|
purge_rules:
|
|
version_added: "1.8"
|
|
description:
|
|
- Purge existing rules on security group that are not found in rules
|
|
required: false
|
|
default: 'true'
|
|
aliases: []
|
|
purge_rules_egress:
|
|
version_added: "1.8"
|
|
description:
|
|
- Purge existing rules_egress on security group that are not found in rules_egress
|
|
required: false
|
|
default: 'true'
|
|
aliases: []
|
|
tags:
|
|
version_added: "2.4"
|
|
description:
|
|
- A dictionary of one or more tags to assign to the security group.
|
|
required: false
|
|
purge_tags:
|
|
version_added: "2.4"
|
|
description:
|
|
- If yes, existing tags will be purged from the resource to match exactly what is defined by I(tags) parameter. If the I(tags) parameter is not set then
|
|
tags will not be modified.
|
|
required: false
|
|
default: yes
|
|
choices: [ 'yes', 'no' ]
|
|
|
|
extends_documentation_fragment:
|
|
- aws
|
|
- ec2
|
|
|
|
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 = '''
|
|
- name: example using security group rule descriptions
|
|
ec2_group:
|
|
name: "{{ name }}"
|
|
description: sg with rule descriptions
|
|
vpc_id: vpc-xxxxxxxx
|
|
profile: "{{ aws_profile }}"
|
|
region: us-east-1
|
|
rules:
|
|
- proto: tcp
|
|
ports:
|
|
- 80
|
|
cidr_ip: 0.0.0.0/0
|
|
rule_desc: allow all on port 80
|
|
|
|
- name: example ec2 group
|
|
ec2_group:
|
|
name: example
|
|
description: an example EC2 group
|
|
vpc_id: 12345
|
|
region: eu-west-1
|
|
aws_secret_key: SECRET
|
|
aws_access_key: ACCESS
|
|
rules:
|
|
- proto: tcp
|
|
from_port: 80
|
|
to_port: 80
|
|
cidr_ip: 0.0.0.0/0
|
|
- proto: tcp
|
|
from_port: 22
|
|
to_port: 22
|
|
cidr_ip: 10.0.0.0/8
|
|
- proto: tcp
|
|
from_port: 443
|
|
to_port: 443
|
|
group_id: amazon-elb/sg-87654321/amazon-elb-sg
|
|
- proto: tcp
|
|
from_port: 3306
|
|
to_port: 3306
|
|
group_id: 123412341234/sg-87654321/exact-name-of-sg
|
|
- proto: udp
|
|
from_port: 10050
|
|
to_port: 10050
|
|
cidr_ip: 10.0.0.0/8
|
|
- proto: udp
|
|
from_port: 10051
|
|
to_port: 10051
|
|
group_id: sg-12345678
|
|
- proto: icmp
|
|
from_port: 8 # icmp type, -1 = any type
|
|
to_port: -1 # icmp subtype, -1 = any subtype
|
|
cidr_ip: 10.0.0.0/8
|
|
- proto: all
|
|
# the containing group name may be specified here
|
|
group_name: example
|
|
rules_egress:
|
|
- proto: tcp
|
|
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
|
|
|
|
- name: example2 ec2 group
|
|
ec2_group:
|
|
name: example2
|
|
description: an example2 EC2 group
|
|
vpc_id: 12345
|
|
region: eu-west-1
|
|
rules:
|
|
# 'ports' rule keyword was introduced in version 2.4. It accepts a single port value or a list of values including ranges (from_port-to_port).
|
|
- proto: tcp
|
|
ports: 22
|
|
group_name: example-vpn
|
|
- proto: tcp
|
|
ports:
|
|
- 80
|
|
- 443
|
|
- 8080-8099
|
|
cidr_ip: 0.0.0.0/0
|
|
# Rule sources list support was added in version 2.4. This allows to define multiple sources per source type as well as multiple source types per rule.
|
|
- proto: tcp
|
|
ports:
|
|
- 6379
|
|
- 26379
|
|
group_name:
|
|
- example-vpn
|
|
- example-redis
|
|
- proto: tcp
|
|
ports: 5665
|
|
group_name: example-vpn
|
|
cidr_ip:
|
|
- 172.16.1.0/24
|
|
- 172.16.17.0/24
|
|
cidr_ipv6:
|
|
- 2607:F8B0::/32
|
|
- 64:ff9b::/96
|
|
group_id:
|
|
- sg-edcd9784
|
|
|
|
- name: "Delete group by its id"
|
|
ec2_group:
|
|
group_id: sg-33b4ee5b
|
|
state: absent
|
|
'''
|
|
|
|
RETURN = '''
|
|
group_name:
|
|
description: Security group name
|
|
sample: My Security Group
|
|
type: string
|
|
returned: on create/update
|
|
group_id:
|
|
description: Security group id
|
|
sample: sg-abcd1234
|
|
type: string
|
|
returned: on create/update
|
|
description:
|
|
description: Description of security group
|
|
sample: My Security Group
|
|
type: string
|
|
returned: on create/update
|
|
tags:
|
|
description: Tags associated with the security group
|
|
sample:
|
|
Name: My Security Group
|
|
Purpose: protecting stuff
|
|
type: dict
|
|
returned: on create/update
|
|
vpc_id:
|
|
description: ID of VPC to which the security group belongs
|
|
sample: vpc-abcd1234
|
|
type: string
|
|
returned: on create/update
|
|
ip_permissions:
|
|
description: Inbound rules associated with the security group.
|
|
sample:
|
|
- from_port: 8182
|
|
ip_protocol: tcp
|
|
ip_ranges:
|
|
- cidr_ip: "1.1.1.1/32"
|
|
ipv6_ranges: []
|
|
prefix_list_ids: []
|
|
to_port: 8182
|
|
user_id_group_pairs: []
|
|
type: list
|
|
returned: on create/update
|
|
ip_permissions_egress:
|
|
description: Outbound rules associated with the security group.
|
|
sample:
|
|
- ip_protocol: -1
|
|
ip_ranges:
|
|
- cidr_ip: "0.0.0.0/0"
|
|
ipv6_ranges: []
|
|
prefix_list_ids: []
|
|
user_id_group_pairs: []
|
|
type: list
|
|
returned: on create/update
|
|
owner_id:
|
|
description: AWS Account ID of the security group
|
|
sample: 123456789012
|
|
type: int
|
|
returned: on create/update
|
|
'''
|
|
|
|
import json
|
|
import re
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
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
|
|
from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list, compare_aws_tags
|
|
from ansible.module_utils.ec2 import AWSRetry
|
|
import traceback
|
|
|
|
try:
|
|
import botocore
|
|
except ImportError:
|
|
pass # caught by imported HAS_BOTO3
|
|
|
|
|
|
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
|
|
def get_security_groups_with_backoff(connection, **kwargs):
|
|
return connection.describe_security_groups(**kwargs)
|
|
|
|
|
|
def deduplicate_rules_args(rules):
|
|
"""Returns unique rules"""
|
|
if rules is None:
|
|
return None
|
|
return list(dict(zip((json.dumps(r, sort_keys=True) for r in rules), rules)).values())
|
|
|
|
|
|
def make_rule_key(prefix, rule, group_id, cidr_ip):
|
|
if 'proto' in rule:
|
|
proto, from_port, to_port = [rule.get(x, None) for x in ('proto', '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 add_rules_to_lookup(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', 'cidr_ipv6',
|
|
'group_id', 'group_name', 'group_desc',
|
|
'proto', 'from_port', 'to_port', 'rule_desc')
|
|
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))
|
|
|
|
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 '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')
|
|
|
|
|
|
def get_target_from_rule(module, client, rule, name, group, groups, vpc_id):
|
|
"""
|
|
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.
|
|
"""
|
|
|
|
FOREIGN_SECURITY_GROUP_REGEX = '^(\S+)/(sg-\S+)/(\S+)'
|
|
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 'cidr_ip' in rule and 'cidr_ipv6' in rule:
|
|
module.fail_json(msg="Specify cidr_ip OR cidr_ipv6, not both")
|
|
elif rule.get('group_id') 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 = dict(GroupId=group_id, GroupName=group_name)
|
|
groups[group_id] = group_instance
|
|
groups[group_name] = group_instance
|
|
elif 'group_id' in rule:
|
|
group_id = rule['group_id']
|
|
elif 'group_name' in rule:
|
|
group_name = rule['group_name']
|
|
if group_name == name:
|
|
group_id = group['GroupId']
|
|
groups[group_id] = group
|
|
groups[group_name] = group
|
|
elif group_name in groups and group.get('VpcId') and groups[group_name].get('VpcId'):
|
|
# both are VPC groups, this is ok
|
|
group_id = groups[group_name]['GroupId']
|
|
elif group_name in groups and not (group.get('VpcId') or groups[group_name].get('VpcId')):
|
|
# both are EC2 classic, this is ok
|
|
group_id = groups[group_name]['GroupId']
|
|
else:
|
|
# if we got here, either the target group does not exist, or there
|
|
# is a mix of EC2 classic + VPC groups. Mixing of EC2 classic + VPC
|
|
# is bad, so we have to create a new SG because no compatible group
|
|
# exists
|
|
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:
|
|
params = dict(GroupName=group_name, Description=rule['group_desc'])
|
|
if vpc_id:
|
|
params['VpcId'] = vpc_id
|
|
auto_group = client.create_security_group(**params)
|
|
group_id = auto_group['GroupId']
|
|
groups[group_id] = auto_group
|
|
groups[group_name] = auto_group
|
|
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, ipv6, target_group_created
|
|
|
|
|
|
def ports_expand(ports):
|
|
# takes a list of ports and returns a list of (port_from, port_to)
|
|
ports_expanded = []
|
|
for port in ports:
|
|
if not isinstance(port, str):
|
|
ports_expanded.append((port,) * 2)
|
|
elif '-' in port:
|
|
ports_expanded.append(tuple(p.strip() for p in port.split('-', 1)))
|
|
else:
|
|
ports_expanded.append((port.strip(),) * 2)
|
|
|
|
return ports_expanded
|
|
|
|
|
|
def rule_expand_ports(rule):
|
|
# takes a rule dict and returns a list of expanded rule dicts
|
|
if 'ports' not in rule:
|
|
return [rule]
|
|
|
|
ports = rule['ports'] if isinstance(rule['ports'], list) else [rule['ports']]
|
|
|
|
rule_expanded = []
|
|
for from_to in ports_expand(ports):
|
|
temp_rule = rule.copy()
|
|
del temp_rule['ports']
|
|
temp_rule['from_port'], temp_rule['to_port'] = from_to
|
|
rule_expanded.append(temp_rule)
|
|
|
|
return rule_expanded
|
|
|
|
|
|
def rules_expand_ports(rules):
|
|
# takes a list of rules and expands it based on 'ports'
|
|
if not rules:
|
|
return rules
|
|
|
|
return [rule for rule_complex in rules
|
|
for rule in rule_expand_ports(rule_complex)]
|
|
|
|
|
|
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', 'cidr_ipv6', 'group_id', 'group_name')
|
|
|
|
rule_expanded = []
|
|
for source in sources:
|
|
temp_rule = rule.copy()
|
|
for s in source_types_all:
|
|
temp_rule.pop(s, None)
|
|
temp_rule[source_type] = source
|
|
rule_expanded.append(temp_rule)
|
|
|
|
return rule_expanded
|
|
|
|
|
|
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', '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)]
|
|
|
|
|
|
def rules_expand_sources(rules):
|
|
# takes a list of rules and expands it based on 'cidr_ip', 'group_id', 'group_name'
|
|
if not rules:
|
|
return rules
|
|
|
|
return [rule for rule_complex in rules
|
|
for rule in rule_expand_sources(rule_complex)]
|
|
|
|
|
|
def update_rules_description(module, client, rule_type, group_id, ip_permissions):
|
|
try:
|
|
if rule_type == "in":
|
|
client.update_security_group_rule_descriptions_ingress(GroupId=group_id, IpPermissions=[ip_permissions])
|
|
if rule_type == "out":
|
|
client.update_security_group_rule_descriptions_egress(GroupId=group_id, IpPermissions=[ip_permissions])
|
|
except botocore.exceptions.ClientError as e:
|
|
module.fail_json(
|
|
msg="Unable to update rule description for group %s: %s" %
|
|
(group_id, e),
|
|
exceptin=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
|
|
|
|
|
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['GroupId'], thisip)
|
|
if rule_id in groupRules:
|
|
|
|
# update the rule description
|
|
if 'rule_desc' in rule:
|
|
desired_rule_desc = rule.get('rule_desc') or ''
|
|
current_rule = groupRules[rule_id][0].get('IpRanges') or groupRules[rule_id][0].get('Ipv6Ranges')
|
|
if desired_rule_desc != current_rule[0].get('Description', ''):
|
|
if not module.check_mode:
|
|
ip_permission = serialize_ip_grant(rule, thisip, ethertype)
|
|
update_rules_description(module, client, type, group['GroupId'], ip_permission)
|
|
changed = True
|
|
|
|
# remove the rule from groupRules to avoid purging it later
|
|
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['GroupId'],
|
|
IpPermissions=[ip_permission])
|
|
elif type == "out":
|
|
client.authorize_security_group_egress(GroupId=group['GroupId'],
|
|
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['GroupName'], 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}]}
|
|
|
|
if 'rule_desc' in rule:
|
|
permission['UserIdGroupPairs'][0]['Description'] = rule.get('rule_desc') or ''
|
|
|
|
return fix_port_and_protocol(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']}]
|
|
}
|
|
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]
|
|
}
|
|
return fix_port_and_protocol(permission)
|
|
|
|
|
|
def serialize_ip_grant(rule, thisip, ethertype):
|
|
permission = {'IpProtocol': rule['proto'],
|
|
'FromPort': rule['from_port'],
|
|
'ToPort': rule['to_port']}
|
|
if ethertype == "ipv4":
|
|
permission['IpRanges'] = [{'CidrIp': thisip}]
|
|
if 'rule_desc' in rule:
|
|
permission['IpRanges'][0]['Description'] = rule.get('rule_desc') or ''
|
|
elif ethertype == "ipv6":
|
|
permission['Ipv6Ranges'] = [{'CidrIpv6': thisip}]
|
|
if 'rule_desc' in rule:
|
|
permission['Ipv6Ranges'][0]['Description'] = rule.get('rule_desc') or ''
|
|
|
|
return fix_port_and_protocol(permission)
|
|
|
|
|
|
def fix_port_and_protocol(permission):
|
|
for key in ['FromPort', 'ToPort']:
|
|
if key in permission:
|
|
if permission[key] is None:
|
|
del permission[key]
|
|
else:
|
|
permission[key] = int(permission[key])
|
|
|
|
permission['IpProtocol'] = str(permission['IpProtocol'])
|
|
|
|
return permission
|
|
|
|
|
|
def check_rule_desc_update_for_group_grant(client, module, rule, group, groupRules, rule_id, group_id, rule_type, changed):
|
|
if 'rule_desc' in rule:
|
|
current_rule_description = rule.get('rule_desc') or ''
|
|
if current_rule_description != groupRules[rule_id][0]['UserIdGroupPairs'][0].get('Description', ''):
|
|
if not module.check_mode:
|
|
ip_permission = serialize_group_grant(group_id, rule)
|
|
update_rules_description(module, client, rule_type, group['GroupId'], ip_permission)
|
|
changed = True
|
|
return changed
|
|
|
|
|
|
def has_rule_description_attr(client):
|
|
return hasattr(client, "update_security_group_rule_descriptions_egress")
|
|
|
|
|
|
def main():
|
|
argument_spec = ec2_argument_spec()
|
|
argument_spec.update(dict(
|
|
name=dict(),
|
|
group_id=dict(),
|
|
description=dict(),
|
|
vpc_id=dict(),
|
|
rules=dict(type='list'),
|
|
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'),
|
|
tags=dict(required=False, type='dict', aliases=['resource_tags']),
|
|
purge_tags=dict(default=True, required=False, type='bool')
|
|
)
|
|
)
|
|
module = AnsibleModule(
|
|
argument_spec=argument_spec,
|
|
supports_check_mode=True,
|
|
required_one_of=[['name', 'group_id']],
|
|
required_if=[['state', 'present', ['name']]],
|
|
)
|
|
|
|
if not HAS_BOTO3:
|
|
module.fail_json(msg='boto3 required for this module')
|
|
|
|
name = module.params['name']
|
|
group_id = module.params['group_id']
|
|
description = module.params['description']
|
|
vpc_id = module.params['vpc_id']
|
|
rules = deduplicate_rules_args(rules_expand_sources(rules_expand_ports(module.params['rules'])))
|
|
rules_egress = deduplicate_rules_args(rules_expand_sources(rules_expand_ports(module.params['rules_egress'])))
|
|
state = module.params.get('state')
|
|
purge_rules = module.params['purge_rules']
|
|
purge_rules_egress = module.params['purge_rules_egress']
|
|
tags = module.params['tags']
|
|
purge_tags = module.params['purge_tags']
|
|
|
|
if state == 'present' and not description:
|
|
module.fail_json(msg='Must provide description when state is present.')
|
|
|
|
changed = False
|
|
region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
|
|
client = boto3_conn(module, conn_type='client', resource='ec2',
|
|
endpoint=ec2_url, region=region, **aws_connect_params)
|
|
|
|
if not has_rule_description_attr(client):
|
|
all_rules = rules if rules else [] + rules_egress if rules_egress else []
|
|
if any('rule_desc' in rule for rule in all_rules):
|
|
module.fail_json(msg="Using rule descriptions requires botocore version >= 1.7.2.")
|
|
|
|
group = None
|
|
groups = dict()
|
|
security_groups = []
|
|
# do get all security groups
|
|
# find if the group is present
|
|
try:
|
|
response = get_security_groups_with_backoff(client)
|
|
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 sg in security_groups:
|
|
groups[sg['GroupId']] = sg
|
|
groupName = sg['GroupName']
|
|
if groupName in groups:
|
|
# Prioritise groups from the current VPC
|
|
# even if current VPC is EC2-Classic
|
|
if groups[groupName].get('VpcId') == vpc_id:
|
|
# Group saved already matches current VPC, change nothing
|
|
pass
|
|
elif vpc_id is None and groups[groupName].get('VpcId') is None:
|
|
# We're in EC2 classic, and the group already saved is as well
|
|
# No VPC groups can be used alongside EC2 classic groups
|
|
pass
|
|
else:
|
|
# the current SG stored has no direct match, so we can replace it
|
|
groups[groupName] = sg
|
|
else:
|
|
groups[groupName] = sg
|
|
|
|
if group_id and sg['GroupId'] == group_id:
|
|
group = sg
|
|
elif groupName == name and (vpc_id is None or sg.get('VpcId') == vpc_id):
|
|
group = sg
|
|
|
|
# Ensure requested group is absent
|
|
if state == 'absent':
|
|
if group:
|
|
# found a match, delete it
|
|
try:
|
|
if not module.check_mode:
|
|
client.delete_security_group(GroupId=group['GroupId'])
|
|
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
|
|
else:
|
|
# no match found, no changes required
|
|
pass
|
|
|
|
# Ensure requested group is present
|
|
elif state == 'present':
|
|
if group:
|
|
# existing group
|
|
if group['Description'] != description:
|
|
module.warn("Group description does not match existing group. Descriptions cannot be changed without deleting "
|
|
"and re-creating the security group. Try using state=absent to delete, then rerunning this task.")
|
|
|
|
# if the group doesn't exist, create it now
|
|
else:
|
|
# no match found, create it
|
|
if not module.check_mode:
|
|
params = dict(GroupName=name, Description=description)
|
|
if vpc_id:
|
|
params['VpcId'] = vpc_id
|
|
group = client.create_security_group(**params)
|
|
# 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 True:
|
|
group = get_security_groups_with_backoff(client, GroupIds=[group['GroupId']])['SecurityGroups'][0]
|
|
if group.get('VpcId') and not group.get('IpPermissionsEgress'):
|
|
pass
|
|
else:
|
|
break
|
|
|
|
changed = True
|
|
|
|
if tags is not None:
|
|
current_tags = boto3_tag_list_to_ansible_dict(group.get('Tags', []))
|
|
tags_need_modify, tags_to_delete = compare_aws_tags(current_tags, tags, purge_tags)
|
|
if tags_to_delete:
|
|
try:
|
|
client.delete_tags(Resources=[group['GroupId']], Tags=[{'Key': tag} for tag in tags_to_delete])
|
|
except botocore.exceptions.ClientError as e:
|
|
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
|
changed = True
|
|
|
|
# Add/update tags
|
|
if tags_need_modify:
|
|
try:
|
|
client.create_tags(Resources=[group['GroupId']], Tags=ansible_dict_to_boto3_tag_list(tags_need_modify))
|
|
except botocore.exceptions.ClientError as e:
|
|
module.fail_json(msg=e.message, exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
|
changed = True
|
|
|
|
else:
|
|
module.fail_json(msg="Unsupported state requested: %s" % state)
|
|
|
|
# create a lookup for all existing rules on the group
|
|
ip_permission = []
|
|
if group:
|
|
# Manage ingress rules
|
|
groupRules = {}
|
|
add_rules_to_lookup(group['IpPermissions'], group['GroupId'], 'in', groupRules)
|
|
# Now, go through all provided rules and ensure they are there.
|
|
if rules is not None:
|
|
for rule in rules:
|
|
validate_rule(module, rule)
|
|
group_id, ip, ipv6, target_group_created = get_target_from_rule(module, client, rule, name,
|
|
group, groups, vpc_id)
|
|
if target_group_created:
|
|
changed = True
|
|
|
|
if rule['proto'] in ('all', '-1', -1):
|
|
rule['proto'] = -1
|
|
rule['from_port'] = None
|
|
rule['to_port'] = None
|
|
|
|
if group_id:
|
|
rule_id = make_rule_key('in', rule, group['GroupId'], group_id)
|
|
if rule_id in groupRules:
|
|
changed = check_rule_desc_update_for_group_grant(client, module, rule, group, groupRules,
|
|
rule_id, group_id, rule_type='in', changed=changed)
|
|
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['GroupId'], 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['GroupName'], 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():
|
|
ip_permission = serialize_revoke(grant, rule)
|
|
if not module.check_mode:
|
|
try:
|
|
client.revoke_security_group_ingress(GroupId=group['GroupId'], IpPermissions=[ip_permission])
|
|
except botocore.exceptions.ClientError as e:
|
|
module.fail_json(
|
|
msg="Unable to revoke ingress for security group '%s' - %s" %
|
|
(group['GroupName'], e),
|
|
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
|
changed = True
|
|
|
|
# Manage egress rules
|
|
groupRules = {}
|
|
add_rules_to_lookup(group['IpPermissionsEgress'], group['GroupId'], '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, ipv6, target_group_created = get_target_from_rule(module, client, rule, name,
|
|
group, groups, vpc_id)
|
|
if target_group_created:
|
|
changed = True
|
|
|
|
if rule['proto'] in ('all', '-1', -1):
|
|
rule['proto'] = -1
|
|
rule['from_port'] = None
|
|
rule['to_port'] = None
|
|
|
|
if group_id:
|
|
rule_id = make_rule_key('out', rule, group['GroupId'], group_id)
|
|
if rule_id in groupRules:
|
|
changed = check_rule_desc_update_for_group_grant(client, module, rule, group, groupRules,
|
|
rule_id, group_id, rule_type='out', changed=changed)
|
|
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['GroupId'], 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['GroupName'], 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")
|
|
elif vpc_id is not None:
|
|
# when no egress rules are specified and we're in a VPC,
|
|
# 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-' + group['GroupId'] + '-0.0.0.0/0'
|
|
if default_egress_rule not in groupRules:
|
|
if not module.check_mode:
|
|
ip_permission = [{'IpProtocol': '-1',
|
|
'IpRanges': [{'CidrIp': '0.0.0.0/0'}]
|
|
}
|
|
]
|
|
try:
|
|
client.authorize_security_group_egress(GroupId=group['GroupId'], 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['GroupName'],
|
|
e),
|
|
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
|
changed = True
|
|
else:
|
|
# make sure the default egress rule is not removed
|
|
del groupRules[default_egress_rule]
|
|
|
|
# Finally, remove anything left in the groupRules -- these will be defunct rules
|
|
if purge_rules_egress and vpc_id is not None:
|
|
for (rule, grant) in groupRules.values():
|
|
# 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['GroupId'], 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['GroupName'], e),
|
|
exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response))
|
|
changed = True
|
|
|
|
if group:
|
|
security_group = get_security_groups_with_backoff(client, GroupIds=[group['GroupId']])['SecurityGroups'][0]
|
|
security_group = camel_dict_to_snake_dict(security_group)
|
|
security_group['tags'] = boto3_tag_list_to_ansible_dict(security_group.get('tags', []),
|
|
tag_name_key_name='key', tag_value_key_name='value')
|
|
module.exit_json(changed=changed, **security_group)
|
|
else:
|
|
module.exit_json(changed=changed, group_id=None)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|