2014-09-26 01:01:01 +00:00
#!/usr/bin/python
# -*- coding: utf-8 -*-
2015-07-04 03:57:53 +00:00
# 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/>.
2014-09-26 01:01:01 +00:00
2017-08-16 03:16:38 +00:00
ANSIBLE_METADATA = { ' metadata_version ' : ' 1.1 ' ,
2017-03-14 16:07:22 +00:00
' status ' : [ ' stableinterface ' ] ,
2017-09-06 15:15:41 +00:00
' supported_by ' : ' core ' }
2017-03-14 16:07:22 +00:00
2014-09-26 01:01:01 +00:00
DOCUMENTATION = '''
- - -
module : ec2_group
2015-06-15 18:41:22 +00:00
author : " Andrew de Quincey (@adq) "
2014-09-26 01:01:01 +00:00
version_added : " 1.3 "
2017-07-17 02:03:31 +00:00
requirements : [ boto3 ]
2014-09-26 01:01:01 +00:00
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 .
2017-06-26 13:07:29 +00:00
- 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 "
2014-09-26 01:01:01 +00:00
description :
description :
2017-03-07 19:55:17 +00:00
- Description of the security group . Required when C ( state ) is C ( present ) .
required : false
2014-09-26 01:01:01 +00:00
vpc_id :
description :
- ID of the VPC to create the group in .
required : false
rules :
description :
2017-03-17 20:14:20 +00:00
- 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 ` .
2017-06-12 06:55:19 +00:00
This allows idempotent loopback additions ( e . g . allow group to access itself ) .
2017-05-31 12:51:33 +00:00
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 .
2017-10-25 01:18:56 +00:00
In version 2.5 support for rule descriptions was added .
2014-09-26 01:01:01 +00:00
required : false
rules_egress :
description :
2017-03-17 20:14:20 +00:00
- 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 .
2017-10-25 01:18:56 +00:00
Rule Egress sources list support was added in version 2.4 . In version 2.5 support for rule descriptions
was added .
2014-09-26 01:01:01 +00:00
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 :
2014-11-29 05:05:22 +00:00
- Purge existing rules_egress on security group that are not found in rules_egress
2014-09-26 01:01:01 +00:00
required : false
default : ' true '
aliases : [ ]
2017-08-22 15:11:38 +00:00
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
2018-04-24 17:05:50 +00:00
type : bool
2014-09-26 01:01:01 +00:00
2016-12-08 02:33:38 +00:00
extends_documentation_fragment :
- aws
- ec2
2014-09-26 01:01:01 +00:00
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 .
2018-08-23 23:43:18 +00:00
- Preview diff mode support is added in version 2.7 .
2014-09-26 01:01:01 +00:00
'''
EXAMPLES = '''
2017-10-25 01:18:56 +00:00
- 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
2014-09-26 01:01:01 +00:00
- name : example ec2 group
2014-12-01 20:14:57 +00:00
ec2_group :
2014-09-26 01:01:01 +00:00
name : example
description : an example EC2 group
vpc_id : 12345
2017-04-13 19:11:00 +00:00
region : eu - west - 1
2014-09-26 01:01:01 +00:00
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
2015-05-22 10:34:41 +00:00
- proto : tcp
from_port : 443
to_port : 443
2018-05-24 15:53:21 +00:00
# this should only be needed for EC2 Classic security group rules
# because in a VPC an ELB will use a user-account security group
2015-05-22 10:34:41 +00:00
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
2014-09-26 01:01:01 +00:00
- 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
2015-09-22 13:56:13 +00:00
- 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
2014-09-26 01:01:01 +00:00
- proto : all
# the containing group name may be specified here
group_name : example
2018-02-22 23:24:25 +00:00
- proto : all
# in the 'proto' attribute, if you specify -1, all, or a protocol number other than tcp, udp, icmp, or 58 (ICMPv6),
# traffic on all ports is allowed, regardless of any ports you specify
from_port : 10050 # this value is ignored
to_port : 10050 # this value is ignored
cidr_ip : 10.0 .0 .0 / 8
2014-09-26 01:01:01 +00:00
rules_egress :
- proto : tcp
from_port : 80
to_port : 80
2014-10-23 12:19:23 +00:00
cidr_ip : 0.0 .0 .0 / 0
2017-07-17 02:03:31 +00:00
cidr_ipv6 : 64 : ff9b : : / 96
2014-09-26 01:01:01 +00:00
group_name : example - other
# description to use if example-other needs to be created
group_desc : other example EC2 group
2017-01-29 16:29:54 +00:00
- name : example2 ec2 group
ec2_group :
name : example2
description : an example2 EC2 group
vpc_id : 12345
2017-04-13 19:11:00 +00:00
region : eu - west - 1
2017-01-29 16:29:54 +00:00
rules :
2017-04-04 18:09:28 +00:00
# '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).
2017-01-29 16:29:54 +00:00
- proto : tcp
ports : 22
group_name : example - vpn
- proto : tcp
ports :
- 80
- 443
- 8080 - 8099
cidr_ip : 0.0 .0 .0 / 0
2017-04-04 18:09:28 +00:00
# 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.
2017-01-29 16:29:54 +00:00
- 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
2017-07-17 02:03:31 +00:00
cidr_ipv6 :
- 2607 : F8B0 : : / 32
- 64 : ff9b : : / 96
2017-01-29 16:29:54 +00:00
group_id :
- sg - edcd9784
2018-08-23 23:43:18 +00:00
diff : True
2017-06-26 13:07:29 +00:00
- name : " Delete group by its id "
ec2_group :
group_id : sg - 33 b4ee5b
state : absent
2014-09-26 01:01:01 +00:00
'''
2017-08-01 10:53:43 +00:00
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
'''
2017-05-12 18:54:25 +00:00
import json
2017-01-29 16:29:54 +00:00
import re
2018-09-05 17:34:26 +00:00
import itertools
2018-09-17 18:31:41 +00:00
from copy import deepcopy
2018-05-24 15:53:21 +00:00
from time import sleep
from collections import namedtuple
2018-06-12 16:15:16 +00:00
from ansible . module_utils . aws . core import AnsibleAWSModule , is_boto3_error_code
2018-05-24 15:53:21 +00:00
from ansible . module_utils . aws . iam import get_aws_account_id
from ansible . module_utils . aws . waiters import get_waiter
from ansible . module_utils . ec2 import AWSRetry , camel_dict_to_snake_dict , compare_aws_tags
from ansible . module_utils . ec2 import ansible_dict_to_boto3_filter_list , boto3_tag_list_to_ansible_dict , ansible_dict_to_boto3_tag_list
2017-12-20 19:57:47 +00:00
from ansible . module_utils . network . common . utils import to_ipv6_network , to_subnet
2018-05-24 15:53:21 +00:00
from ansible . module_utils . _text import to_text
from ansible . module_utils . six import string_types
2017-01-29 16:29:54 +00:00
2014-09-26 01:01:01 +00:00
try :
2018-05-24 15:53:21 +00:00
from botocore . exceptions import BotoCoreError , ClientError
2014-09-26 01:01:01 +00:00
except ImportError :
2018-05-24 15:53:21 +00:00
pass # caught by AnsibleAWSModule
Rule = namedtuple ( ' Rule ' , [ ' port_range ' , ' protocol ' , ' target ' , ' target_type ' , ' description ' ] )
valid_targets = set ( [ ' ipv4 ' , ' ipv6 ' , ' group ' , ' ip_prefix ' ] )
current_account_id = None
def rule_cmp ( a , b ) :
""" Compare rules without descriptions """
for prop in [ ' port_range ' , ' protocol ' , ' target ' , ' target_type ' ] :
2018-09-05 17:34:26 +00:00
if prop == ' port_range ' and to_text ( a . protocol ) == to_text ( b . protocol ) :
# equal protocols can interchange `(-1, -1)` and `(None, None)`
if a . port_range in ( ( None , None ) , ( - 1 , - 1 ) ) and b . port_range in ( ( None , None ) , ( - 1 , - 1 ) ) :
continue
elif getattr ( a , prop ) != getattr ( b , prop ) :
return False
elif getattr ( a , prop ) != getattr ( b , prop ) :
2018-05-24 15:53:21 +00:00
return False
return True
def rules_to_permissions ( rules ) :
return [ to_permission ( rule ) for rule in rules ]
def to_permission ( rule ) :
# take a Rule, output the serialized grant
perm = {
' IpProtocol ' : rule . protocol ,
}
perm [ ' FromPort ' ] , perm [ ' ToPort ' ] = rule . port_range
if rule . target_type == ' ipv4 ' :
perm [ ' IpRanges ' ] = [ {
' CidrIp ' : rule . target ,
} ]
if rule . description :
perm [ ' IpRanges ' ] [ 0 ] [ ' Description ' ] = rule . description
elif rule . target_type == ' ipv6 ' :
perm [ ' Ipv6Ranges ' ] = [ {
' CidrIpv6 ' : rule . target ,
} ]
if rule . description :
perm [ ' Ipv6Ranges ' ] [ 0 ] [ ' Description ' ] = rule . description
elif rule . target_type == ' group ' :
if isinstance ( rule . target , tuple ) :
pair = { }
if rule . target [ 0 ] :
pair [ ' UserId ' ] = rule . target [ 0 ]
2018-09-06 19:06:03 +00:00
# group_id/group_name are mutually exclusive - give group_id more precedence as it is more specific
if rule . target [ 1 ] :
2018-05-24 15:53:21 +00:00
pair [ ' GroupId ' ] = rule . target [ 1 ]
2018-09-06 19:06:03 +00:00
elif rule . target [ 2 ] :
2018-05-24 15:53:21 +00:00
pair [ ' GroupName ' ] = rule . target [ 2 ]
perm [ ' UserIdGroupPairs ' ] = [ pair ]
else :
perm [ ' UserIdGroupPairs ' ] = [ {
' GroupId ' : rule . target
} ]
if rule . description :
perm [ ' UserIdGroupPairs ' ] [ 0 ] [ ' Description ' ] = rule . description
elif rule . target_type == ' ip_prefix ' :
perm [ ' PrefixListIds ' ] = [ {
' PrefixListId ' : rule . target ,
} ]
if rule . description :
perm [ ' PrefixListIds ' ] [ 0 ] [ ' Description ' ] = rule . description
elif rule . target_type not in valid_targets :
raise ValueError ( ' Invalid target type for rule {0} ' . format ( rule ) )
return fix_port_and_protocol ( perm )
def rule_from_group_permission ( perm ) :
def ports_from_permission ( p ) :
if ' FromPort ' not in p and ' ToPort ' not in p :
return ( None , None )
return ( int ( perm [ ' FromPort ' ] ) , int ( perm [ ' ToPort ' ] ) )
# outputs a rule tuple
for target_key , target_subkey , target_type in [
( ' IpRanges ' , ' CidrIp ' , ' ipv4 ' ) ,
( ' Ipv6Ranges ' , ' CidrIpv6 ' , ' ipv6 ' ) ,
( ' PrefixListIds ' , ' PrefixListId ' , ' ip_prefix ' ) ,
] :
if target_key not in perm :
continue
for r in perm [ target_key ] :
# there may be several IP ranges here, which is ok
yield Rule (
ports_from_permission ( perm ) ,
2018-09-05 17:34:26 +00:00
to_text ( perm [ ' IpProtocol ' ] ) ,
2018-05-24 15:53:21 +00:00
r [ target_subkey ] ,
target_type ,
r . get ( ' Description ' )
)
if ' UserIdGroupPairs ' in perm and perm [ ' UserIdGroupPairs ' ] :
for pair in perm [ ' UserIdGroupPairs ' ] :
target = pair [ ' GroupId ' ]
if pair . get ( ' UserId ' , ' ' ) . startswith ( ' amazon- ' ) :
# amazon-elb and amazon-prefix rules don't need
# group-id specified, so remove it when querying
# from permission
target = (
target [ 0 ] ,
None ,
target [ 2 ] ,
)
2018-09-06 19:06:03 +00:00
elif ' VpcPeeringConnectionId ' in pair or pair [ ' UserId ' ] != current_account_id :
target = (
pair . get ( ' UserId ' , None ) ,
pair . get ( ' GroupId ' , None ) ,
pair . get ( ' GroupName ' , None ) ,
)
2018-05-24 15:53:21 +00:00
yield Rule (
ports_from_permission ( perm ) ,
2018-09-05 17:34:26 +00:00
to_text ( perm [ ' IpProtocol ' ] ) ,
2018-05-24 15:53:21 +00:00
target ,
' group ' ,
pair . get ( ' Description ' )
)
2017-03-26 13:33:29 +00:00
2014-09-26 01:01:01 +00:00
2017-08-01 10:53:43 +00:00
@AWSRetry.backoff ( tries = 5 , delay = 5 , backoff = 2.0 )
def get_security_groups_with_backoff ( connection , * * kwargs ) :
return connection . describe_security_groups ( * * kwargs )
2018-05-24 15:53:21 +00:00
@AWSRetry.backoff ( tries = 5 , delay = 5 , backoff = 2.0 )
def sg_exists_with_backoff ( connection , * * kwargs ) :
try :
return connection . describe_security_groups ( * * kwargs )
2018-06-12 16:15:16 +00:00
except is_boto3_error_code ( ' InvalidGroup.NotFound ' ) :
2018-05-24 15:53:21 +00:00
return { ' SecurityGroups ' : [ ] }
2017-05-12 18:54:25 +00:00
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 ( ) )
2014-11-14 02:22:51 +00:00
def validate_rule ( module , rule ) :
2018-05-24 15:53:21 +00:00
VALID_PARAMS = ( ' cidr_ip ' , ' cidr_ipv6 ' , ' ip_prefix ' ,
2014-11-14 02:22:51 +00:00
' group_id ' , ' group_name ' , ' group_desc ' ,
2017-10-25 01:18:56 +00:00
' proto ' , ' from_port ' , ' to_port ' , ' rule_desc ' )
2016-09-02 19:09:58 +00:00
if not isinstance ( rule , dict ) :
module . fail_json ( msg = ' Invalid rule parameter type [ %s ]. ' % type ( rule ) )
2014-11-14 02:22:51 +00:00
for k in rule :
if k not in VALID_PARAMS :
2018-05-24 15:53:21 +00:00
module . fail_json ( msg = ' Invalid rule parameter \' {0} \' for rule: {1} ' . format ( k , rule ) )
2014-11-14 02:22:51 +00:00
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 ' )
2017-07-17 02:03:31 +00:00
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 " )
2014-11-14 02:22:51 +00:00
elif ' group_id ' in rule and ' group_name ' in rule :
module . fail_json ( msg = ' Specify group_id OR group_name, not both ' )
2017-08-01 10:53:43 +00:00
def get_target_from_rule ( module , client , rule , name , group , groups , vpc_id ) :
2014-09-26 01:01:01 +00:00
"""
2018-05-24 15:53:21 +00:00
Returns tuple of ( target_type , target , group_created ) after validating rule params .
2014-09-26 01:01:01 +00:00
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 .
"""
2018-05-24 15:53:21 +00:00
FOREIGN_SECURITY_GROUP_REGEX = r ' ^([^/]+)/?(sg- \ S+)?/( \ S+) '
2014-09-26 01:01:01 +00:00
group_id = None
group_name = None
target_group_created = False
2017-07-17 02:03:31 +00:00
2018-05-24 15:53:21 +00:00
validate_rule ( module , rule )
2018-09-06 19:06:03 +00:00
if rule . get ( ' group_id ' ) and re . match ( FOREIGN_SECURITY_GROUP_REGEX , rule [ ' group_id ' ] ) :
2015-05-22 10:34:41 +00:00
# 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 ( )
2018-05-24 15:53:21 +00:00
group_instance = dict ( UserId = owner_id , GroupId = group_id , GroupName = group_name )
2015-05-22 10:34:41 +00:00
groups [ group_id ] = group_instance
groups [ group_name ] = group_instance
2018-09-06 19:06:03 +00:00
# group_id/group_name are mutually exclusive - give group_id more precedence as it is more specific
2018-05-24 15:53:21 +00:00
if group_id and group_name :
2018-09-06 19:06:03 +00:00
group_name = None
2018-05-24 15:53:21 +00:00
return ' group ' , ( owner_id , group_id , group_name ) , False
2014-09-26 01:01:01 +00:00
elif ' group_id ' in rule :
2018-05-24 15:53:21 +00:00
return ' group ' , rule [ ' group_id ' ] , False
2014-09-26 01:01:01 +00:00
elif ' group_name ' in rule :
group_name = rule [ ' group_name ' ]
2015-04-27 18:26:13 +00:00
if group_name == name :
2017-08-01 10:53:43 +00:00
group_id = group [ ' GroupId ' ]
2014-09-26 01:01:01 +00:00
groups [ group_id ] = group
groups [ group_name ] = group
2017-09-13 18:19:05 +00:00
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
2017-08-01 10:53:43 +00:00
group_id = groups [ group_name ] [ ' GroupId ' ]
2014-09-26 01:01:01 +00:00
else :
2018-05-24 15:53:21 +00:00
auto_group = None
filters = { ' group-name ' : group_name }
if vpc_id :
filters [ ' vpc-id ' ] = vpc_id
2017-09-13 18:19:05 +00:00
# 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
2014-09-26 01:01:01 +00:00
if not rule . get ( ' group_desc ' , ' ' ) . strip ( ) :
2018-05-24 15:53:21 +00:00
# retry describing the group once
try :
auto_group = get_security_groups_with_backoff ( client , Filters = ansible_dict_to_boto3_filter_list ( filters ) ) . get ( ' SecurityGroups ' , [ ] ) [ 0 ]
2018-06-12 16:15:16 +00:00
except ( is_boto3_error_code ( ' InvalidGroup.NotFound ' ) , IndexError ) :
2018-05-24 15:53:21 +00:00
module . fail_json ( msg = " group %s will be automatically created by rule %s but "
" no description was provided " % ( group_name , rule ) )
2018-06-12 16:15:16 +00:00
except ClientError as e : # pylint: disable=duplicate-except
2018-05-24 15:53:21 +00:00
module . fail_json_aws ( e )
elif not module . check_mode :
2017-08-01 10:53:43 +00:00
params = dict ( GroupName = group_name , Description = rule [ ' group_desc ' ] )
if vpc_id :
params [ ' VpcId ' ] = vpc_id
2018-05-24 15:53:21 +00:00
try :
auto_group = client . create_security_group ( * * params )
get_waiter (
client , ' security_group_exists ' ,
) . wait (
GroupIds = [ auto_group [ ' GroupId ' ] ] ,
)
2018-06-12 16:15:16 +00:00
except is_boto3_error_code ( ' InvalidGroup.Duplicate ' ) :
2018-05-24 15:53:21 +00:00
# The group exists, but didn't show up in any of our describe-security-groups calls
# Try searching on a filter for the name, and allow a retry window for AWS to update
# the model on their end.
try :
auto_group = get_security_groups_with_backoff ( client , Filters = ansible_dict_to_boto3_filter_list ( filters ) ) . get ( ' SecurityGroups ' , [ ] ) [ 0 ]
except IndexError as e :
module . fail_json ( msg = " Could not create or use existing group ' {0} ' in rule. Make sure the group exists " . format ( group_name ) )
except ClientError as e :
module . fail_json_aws (
e ,
msg = " Could not create or use existing group ' {0} ' in rule. Make sure the group exists " . format ( group_name ) )
if auto_group is not None :
2017-08-01 10:53:43 +00:00
group_id = auto_group [ ' GroupId ' ]
2014-09-26 01:01:01 +00:00
groups [ group_id ] = auto_group
groups [ group_name ] = auto_group
target_group_created = True
2018-05-24 15:53:21 +00:00
return ' group ' , group_id , target_group_created
2014-09-26 01:01:01 +00:00
elif ' cidr_ip ' in rule :
2018-05-24 15:53:21 +00:00
return ' ipv4 ' , validate_ip ( module , rule [ ' cidr_ip ' ] ) , False
2017-07-17 02:03:31 +00:00
elif ' cidr_ipv6 ' in rule :
2018-05-24 15:53:21 +00:00
return ' ipv6 ' , validate_ip ( module , rule [ ' cidr_ipv6 ' ] ) , False
elif ' ip_prefix ' in rule :
return ' ip_prefix ' , rule [ ' ip_prefix ' ] , False
2014-09-26 01:01:01 +00:00
2018-05-24 15:53:21 +00:00
module . fail_json ( msg = " Could not match target for rule {0} " . format ( rule ) , failed_rule = rule )
2014-09-26 01:01:01 +00:00
2017-01-29 16:29:54 +00:00
def ports_expand ( ports ) :
# takes a list of ports and returns a list of (port_from, port_to)
ports_expanded = [ ]
for port in ports :
2018-05-24 15:53:21 +00:00
if not isinstance ( port , string_types ) :
2017-01-29 16:29:54 +00:00
ports_expanded . append ( ( port , ) * 2 )
elif ' - ' in port :
2018-05-24 15:53:21 +00:00
ports_expanded . append ( tuple ( int ( p . strip ( ) ) for p in port . split ( ' - ' , 1 ) ) )
2017-01-29 16:29:54 +00:00
else :
2018-05-24 15:53:21 +00:00
ports_expanded . append ( ( int ( port . strip ( ) ) , ) * 2 )
2017-01-29 16:29:54 +00:00
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 :
2018-05-24 15:53:21 +00:00
if isinstance ( rule . get ( ' from_port ' ) , string_types ) :
rule [ ' from_port ' ] = int ( rule . get ( ' from_port ' ) )
if isinstance ( rule . get ( ' to_port ' ) , string_types ) :
rule [ ' to_port ' ] = int ( rule . get ( ' to_port ' ) )
2017-01-29 16:29:54 +00:00
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 ' ]
2018-05-24 15:53:21 +00:00
temp_rule [ ' from_port ' ] , temp_rule [ ' to_port ' ] = sorted ( from_to )
2017-01-29 16:29:54 +00:00
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 ] ]
2018-05-24 15:53:21 +00:00
source_types_all = ( ' cidr_ip ' , ' cidr_ipv6 ' , ' group_id ' , ' group_name ' , ' ip_prefix ' )
2017-01-29 16:29:54 +00:00
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
2018-05-24 15:53:21 +00:00
source_types = ( stype for stype in ( ' cidr_ip ' , ' cidr_ipv6 ' , ' group_id ' , ' group_name ' , ' ip_prefix ' ) if stype in rule )
2017-01-29 16:29:54 +00:00
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 ) ]
2017-10-25 01:18:56 +00:00
def update_rules_description ( module , client , rule_type , group_id , ip_permissions ) :
2018-05-24 15:53:21 +00:00
if module . check_mode :
return
2017-10-25 01:18:56 +00:00
try :
if rule_type == " in " :
2018-05-24 15:53:21 +00:00
client . update_security_group_rule_descriptions_ingress ( GroupId = group_id , IpPermissions = ip_permissions )
2017-10-25 01:18:56 +00:00
if rule_type == " out " :
2018-05-24 15:53:21 +00:00
client . update_security_group_rule_descriptions_egress ( GroupId = group_id , IpPermissions = ip_permissions )
except ( ClientError , BotoCoreError ) as e :
module . fail_json_aws ( e , msg = " Unable to update rule description for group %s " % group_id )
2017-07-17 02:03:31 +00:00
2017-08-22 15:11:38 +00:00
def fix_port_and_protocol ( permission ) :
2018-05-24 15:53:21 +00:00
for key in ( ' FromPort ' , ' ToPort ' ) :
2017-08-22 15:11:38 +00:00
if key in permission :
if permission [ key ] is None :
del permission [ key ]
else :
permission [ key ] = int ( permission [ key ] )
2018-05-24 15:53:21 +00:00
permission [ ' IpProtocol ' ] = to_text ( permission [ ' IpProtocol ' ] )
2017-08-22 15:11:38 +00:00
return permission
2017-08-02 00:26:38 +00:00
2018-05-24 15:53:21 +00:00
def remove_old_permissions ( client , module , revoke_ingress , revoke_egress , group_id ) :
if revoke_ingress :
revoke ( client , module , revoke_ingress , group_id , ' in ' )
if revoke_egress :
revoke ( client , module , revoke_egress , group_id , ' out ' )
return bool ( revoke_ingress or revoke_egress )
def revoke ( client , module , ip_permissions , group_id , rule_type ) :
if not module . check_mode :
try :
if rule_type == ' in ' :
client . revoke_security_group_ingress ( GroupId = group_id , IpPermissions = ip_permissions )
elif rule_type == ' out ' :
client . revoke_security_group_egress ( GroupId = group_id , IpPermissions = ip_permissions )
except ( BotoCoreError , ClientError ) as e :
rules = ' ingress rules ' if rule_type == ' in ' else ' egress rules '
module . fail_json_aws ( e , " Unable to revoke {0} : {1} " . format ( rules , ip_permissions ) )
def add_new_permissions ( client , module , new_ingress , new_egress , group_id ) :
if new_ingress :
authorize ( client , module , new_ingress , group_id , ' in ' )
if new_egress :
authorize ( client , module , new_egress , group_id , ' out ' )
return bool ( new_ingress or new_egress )
def authorize ( client , module , ip_permissions , group_id , rule_type ) :
if not module . check_mode :
try :
if rule_type == ' in ' :
client . authorize_security_group_ingress ( GroupId = group_id , IpPermissions = ip_permissions )
elif rule_type == ' out ' :
client . authorize_security_group_egress ( GroupId = group_id , IpPermissions = ip_permissions )
except ( BotoCoreError , ClientError ) as e :
rules = ' ingress rules ' if rule_type == ' in ' else ' egress rules '
module . fail_json_aws ( e , " Unable to authorize {0} : {1} " . format ( rules , ip_permissions ) )
def validate_ip ( module , cidr_ip ) :
split_addr = cidr_ip . split ( ' / ' )
if len ( split_addr ) == 2 :
# this_ip is a IPv4 or IPv6 CIDR that may or may not have host bits set
# Get the network bits.
try :
ip = to_subnet ( split_addr [ 0 ] , split_addr [ 1 ] )
except ValueError :
ip = to_ipv6_network ( split_addr [ 0 ] ) + " / " + split_addr [ 1 ]
if ip != cidr_ip :
module . warn ( " One of your CIDR addresses ( {0} ) has host bits set. To get rid of this warning, "
" check the network mask and make sure that only network bits are set: {1} . " . format ( cidr_ip , ip ) )
return ip
return cidr_ip
def update_tags ( client , module , group_id , current_tags , tags , purge_tags ) :
tags_need_modify , tags_to_delete = compare_aws_tags ( current_tags , tags , purge_tags )
if not module . check_mode :
if tags_to_delete :
try :
client . delete_tags ( Resources = [ group_id ] , Tags = [ { ' Key ' : tag } for tag in tags_to_delete ] )
except ( BotoCoreError , ClientError ) as e :
module . fail_json_aws ( e , msg = " Unable to delete tags {0} " . format ( tags_to_delete ) )
# Add/update tags
if tags_need_modify :
try :
client . create_tags ( Resources = [ group_id ] , Tags = ansible_dict_to_boto3_tag_list ( tags_need_modify ) )
except ( BotoCoreError , ClientError ) as e :
module . fail_json ( e , msg = " Unable to add tags {0} " . format ( tags_need_modify ) )
return bool ( tags_need_modify or tags_to_delete )
def update_rule_descriptions ( module , group_id , present_ingress , named_tuple_ingress_list , present_egress , named_tuple_egress_list ) :
changed = False
client = module . client ( ' ec2 ' )
ingress_needs_desc_update = [ ]
egress_needs_desc_update = [ ]
for present_rule in present_egress :
needs_update = [ r for r in named_tuple_egress_list if rule_cmp ( r , present_rule ) and r . description != present_rule . description ]
for r in needs_update :
named_tuple_egress_list . remove ( r )
egress_needs_desc_update . extend ( needs_update )
for present_rule in present_ingress :
needs_update = [ r for r in named_tuple_ingress_list if rule_cmp ( r , present_rule ) and r . description != present_rule . description ]
for r in needs_update :
named_tuple_ingress_list . remove ( r )
ingress_needs_desc_update . extend ( needs_update )
if ingress_needs_desc_update :
update_rules_description ( module , client , ' in ' , group_id , rules_to_permissions ( ingress_needs_desc_update ) )
changed | = True
if egress_needs_desc_update :
update_rules_description ( module , client , ' out ' , group_id , rules_to_permissions ( egress_needs_desc_update ) )
changed | = True
2017-10-25 01:18:56 +00:00
return changed
2018-05-24 15:53:21 +00:00
def create_security_group ( client , module , name , description , vpc_id ) :
if not module . check_mode :
params = dict ( GroupName = name , Description = description )
if vpc_id :
params [ ' VpcId ' ] = vpc_id
try :
group = client . create_security_group ( * * params )
except ( BotoCoreError , ClientError ) as e :
module . fail_json_aws ( e , msg = " Unable to create security group " )
# 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 :
sleep ( 3 )
group = get_security_groups_with_backoff ( client , GroupIds = [ group [ ' GroupId ' ] ] ) [ ' SecurityGroups ' ] [ 0 ]
if group . get ( ' VpcId ' ) and not group . get ( ' IpPermissionsEgress ' ) :
pass
else :
break
return group
return None
def wait_for_rule_propagation ( module , group , desired_ingress , desired_egress , purge_ingress , purge_egress ) :
group_id = group [ ' GroupId ' ]
tries = 6
def await_rules ( group , desired_rules , purge , rule_key ) :
for i in range ( tries ) :
current_rules = set ( sum ( [ list ( rule_from_group_permission ( p ) ) for p in group [ rule_key ] ] , [ ] ) )
if purge and len ( current_rules ^ set ( desired_rules ) ) == 0 :
return group
2018-09-05 17:34:26 +00:00
elif purge :
conflicts = current_rules ^ set ( desired_rules )
# For cases where set comparison is equivalent, but invalid port/proto exist
for a , b in itertools . combinations ( conflicts , 2 ) :
if rule_cmp ( a , b ) :
conflicts . discard ( a )
conflicts . discard ( b )
if not len ( conflicts ) :
return group
2018-05-24 15:53:21 +00:00
elif current_rules . issuperset ( desired_rules ) and not purge :
return group
sleep ( 10 )
group = get_security_groups_with_backoff ( module . client ( ' ec2 ' ) , GroupIds = [ group_id ] ) [ ' SecurityGroups ' ] [ 0 ]
module . warn ( " Ran out of time waiting for {0} {1} . Current: {2} , Desired: {3} " . format ( group_id , rule_key , current_rules , desired_rules ) )
return group
group = get_security_groups_with_backoff ( module . client ( ' ec2 ' ) , GroupIds = [ group_id ] ) [ ' SecurityGroups ' ] [ 0 ]
if ' VpcId ' in group and module . params . get ( ' rules_egress ' ) is not None :
group = await_rules ( group , desired_egress , purge_egress , ' IpPermissionsEgress ' )
return await_rules ( group , desired_ingress , purge_ingress , ' IpPermissions ' )
def group_exists ( client , module , vpc_id , group_id , name ) :
params = { ' Filters ' : [ ] }
if group_id :
params [ ' GroupIds ' ] = [ group_id ]
if name :
# Add name to filters rather than params['GroupNames']
# because params['GroupNames'] only checks the default vpc if no vpc is provided
params [ ' Filters ' ] . append ( { ' Name ' : ' group-name ' , ' Values ' : [ name ] } )
if vpc_id :
params [ ' Filters ' ] . append ( { ' Name ' : ' vpc-id ' , ' Values ' : [ vpc_id ] } )
# Don't filter by description to maintain backwards compatibility
try :
security_groups = sg_exists_with_backoff ( client , * * params ) . get ( ' SecurityGroups ' , [ ] )
all_groups = get_security_groups_with_backoff ( client ) . get ( ' SecurityGroups ' , [ ] )
2018-06-12 16:15:16 +00:00
except ( BotoCoreError , ClientError ) as e : # pylint: disable=duplicate-except
2018-05-24 15:53:21 +00:00
module . fail_json_aws ( e , msg = " Error in describe_security_groups " )
if security_groups :
groups = dict ( ( group [ ' GroupId ' ] , group ) for group in all_groups )
groups . update ( dict ( ( group [ ' GroupName ' ] , group ) for group in all_groups ) )
2018-09-18 19:33:19 +00:00
if vpc_id :
vpc_wins = dict ( ( group [ ' GroupName ' ] , group ) for group in all_groups if group [ ' VpcId ' ] == vpc_id )
groups . update ( vpc_wins )
2018-05-24 15:53:21 +00:00
# maintain backwards compatibility by using the last matching group
return security_groups [ - 1 ] , groups
return None , { }
def verify_rules_with_descriptions_permitted ( client , module , rules , rules_egress ) :
if not hasattr ( client , " update_security_group_rule_descriptions_egress " ) :
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. " )
2017-10-25 01:18:56 +00:00
2018-08-23 23:43:18 +00:00
def get_diff_final_resource ( client , module , security_group ) :
def get_account_id ( security_group , module ) :
try :
owner_id = security_group . get ( ' owner_id ' , module . client ( ' sts ' ) . get_caller_identity ( ) [ ' Account ' ] )
except ( BotoCoreError , ClientError ) as e :
owner_id = " Unable to determine owner_id: {0} " . format ( to_text ( e ) )
return owner_id
def get_final_tags ( security_group_tags , specified_tags , purge_tags ) :
if specified_tags is None :
return security_group_tags
tags_need_modify , tags_to_delete = compare_aws_tags ( security_group_tags , specified_tags , purge_tags )
end_result_tags = dict ( ( k , v ) for k , v in specified_tags . items ( ) if k not in tags_to_delete )
end_result_tags . update ( dict ( ( k , v ) for k , v in security_group_tags . items ( ) if k not in tags_to_delete ) )
end_result_tags . update ( tags_need_modify )
return end_result_tags
def get_final_rules ( client , module , security_group_rules , specified_rules , purge_rules ) :
if specified_rules is None :
return security_group_rules
if purge_rules :
final_rules = [ ]
else :
final_rules = list ( security_group_rules )
2018-09-17 18:31:41 +00:00
specified_rules = flatten_nested_targets ( module , deepcopy ( specified_rules ) )
2018-08-23 23:43:18 +00:00
for rule in specified_rules :
format_rule = {
' from_port ' : None , ' to_port ' : None , ' ip_protocol ' : rule . get ( ' proto ' , ' tcp ' ) ,
' ip_ranges ' : [ ] , ' ipv6_ranges ' : [ ] , ' prefix_list_ids ' : [ ] , ' user_id_group_pairs ' : [ ]
}
if rule . get ( ' proto ' , ' tcp ' ) in ( ' all ' , ' -1 ' , - 1 ) :
format_rule [ ' ip_protocol ' ] = ' -1 '
format_rule . pop ( ' from_port ' )
format_rule . pop ( ' to_port ' )
elif rule . get ( ' ports ' ) :
2018-09-17 18:31:41 +00:00
if rule . get ( ' ports ' ) and ( isinstance ( rule [ ' ports ' ] , string_types ) or isinstance ( rule [ ' ports ' ] , int ) ) :
2018-08-23 23:43:18 +00:00
rule [ ' ports ' ] = [ rule [ ' ports ' ] ]
for port in rule . get ( ' ports ' ) :
if isinstance ( port , string_types ) and ' - ' in port :
format_rule [ ' from_port ' ] , format_rule [ ' to_port ' ] = port . split ( ' - ' )
else :
format_rule [ ' from_port ' ] = format_rule [ ' to_port ' ] = port
elif rule . get ( ' from_port ' ) or rule . get ( ' to_port ' ) :
format_rule [ ' from_port ' ] = rule . get ( ' from_port ' , rule . get ( ' to_port ' ) )
format_rule [ ' to_port ' ] = rule . get ( ' to_port ' , rule . get ( ' from_port ' ) )
for source_type in ( ' cidr_ip ' , ' cidr_ipv6 ' , ' prefix_list_id ' ) :
if rule . get ( source_type ) :
rule_key = { ' cidr_ip ' : ' ip_ranges ' , ' cidr_ipv6 ' : ' ipv6_ranges ' , ' prefix_list_id ' : ' prefix_list_ids ' } . get ( source_type )
if rule . get ( ' rule_desc ' ) :
format_rule [ rule_key ] = [ { source_type : rule [ source_type ] , ' description ' : rule [ ' rule_desc ' ] } ]
else :
2018-09-17 18:31:41 +00:00
if not isinstance ( rule [ source_type ] , list ) :
rule [ source_type ] = [ rule [ source_type ] ]
format_rule [ rule_key ] = [ { source_type : target } for target in rule [ source_type ] ]
2018-08-23 23:43:18 +00:00
if rule . get ( ' group_id ' ) or rule . get ( ' group_name ' ) :
rule_sg = camel_dict_to_snake_dict ( group_exists ( client , module , module . params [ ' vpc_id ' ] , rule . get ( ' group_id ' ) , rule . get ( ' group_name ' ) ) [ 0 ] )
format_rule [ ' user_id_group_pairs ' ] = [ {
' description ' : rule_sg . get ( ' description ' , rule_sg . get ( ' group_desc ' ) ) ,
' group_id ' : rule_sg . get ( ' group_id ' , rule . get ( ' group_id ' ) ) ,
' group_name ' : rule_sg . get ( ' group_name ' , rule . get ( ' group_name ' ) ) ,
' peering_status ' : rule_sg . get ( ' peering_status ' ) ,
' user_id ' : rule_sg . get ( ' user_id ' , get_account_id ( security_group , module ) ) ,
' vpc_id ' : rule_sg . get ( ' vpc_id ' , module . params [ ' vpc_id ' ] ) ,
' vpc_peering_connection_id ' : rule_sg . get ( ' vpc_peering_connection_id ' )
} ]
for k , v in format_rule [ ' user_id_group_pairs ' ] [ 0 ] . items ( ) :
if v is None :
format_rule [ ' user_id_group_pairs ' ] [ 0 ] . pop ( k )
final_rules . append ( format_rule )
# Order final rules consistently
final_rules . sort ( key = lambda x : x . get ( ' cidr_ip ' , x . get ( ' ip_ranges ' , x . get ( ' ipv6_ranges ' , x . get ( ' prefix_list_ids ' , x . get ( ' user_id_group_pairs ' ) ) ) ) ) )
return final_rules
security_group_ingress = security_group . get ( ' ip_permissions ' , [ ] )
specified_ingress = module . params [ ' rules ' ]
purge_ingress = module . params [ ' purge_rules ' ]
security_group_egress = security_group . get ( ' ip_permissions_egress ' , [ ] )
specified_egress = module . params [ ' rules_egress ' ]
purge_egress = module . params [ ' purge_rules_egress ' ]
return {
' description ' : module . params [ ' description ' ] ,
' group_id ' : security_group . get ( ' group_id ' , ' sg-xxxxxxxx ' ) ,
' group_name ' : security_group . get ( ' group_name ' , module . params [ ' name ' ] ) ,
' ip_permissions ' : get_final_rules ( client , module , security_group_ingress , specified_ingress , purge_ingress ) ,
' ip_permissions_egress ' : get_final_rules ( client , module , security_group_egress , specified_egress , purge_egress ) ,
' owner_id ' : get_account_id ( security_group , module ) ,
' tags ' : get_final_tags ( security_group . get ( ' tags ' , { } ) , module . params [ ' tags ' ] , module . params [ ' purge_tags ' ] ) ,
' vpc_id ' : security_group . get ( ' vpc_id ' , module . params [ ' vpc_id ' ] ) }
2018-09-17 18:31:41 +00:00
def flatten_nested_targets ( module , rules ) :
def _flatten ( targets ) :
for target in targets :
if isinstance ( target , list ) :
for t in _flatten ( target ) :
yield t
elif isinstance ( target , string_types ) :
yield target
if rules is not None :
for rule in rules :
target_list_type = None
if isinstance ( rule . get ( ' cidr_ip ' ) , list ) :
target_list_type = ' cidr_ip '
elif isinstance ( rule . get ( ' cidr_ipv6 ' ) , list ) :
target_list_type = ' cidr_ipv6 '
if target_list_type is not None :
rule [ target_list_type ] = list ( _flatten ( rule [ target_list_type ] ) )
return rules
2014-09-26 01:01:01 +00:00
def main ( ) :
2018-05-24 15:53:21 +00:00
argument_spec = dict (
2017-06-26 13:07:29 +00:00
name = dict ( ) ,
group_id = dict ( ) ,
description = dict ( ) ,
vpc_id = dict ( ) ,
2017-01-29 07:28:53 +00:00
rules = dict ( type = ' list ' ) ,
rules_egress = dict ( type = ' list ' ) ,
2017-03-17 20:14:20 +00:00
state = dict ( default = ' present ' , type = ' str ' , choices = [ ' present ' , ' absent ' ] ) ,
2017-01-29 07:28:53 +00:00
purge_rules = dict ( default = True , required = False , type = ' bool ' ) ,
2017-08-22 15:11:38 +00:00
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 ' )
2017-01-29 07:28:53 +00:00
)
2018-05-24 15:53:21 +00:00
module = AnsibleAWSModule (
2014-09-26 01:01:01 +00:00
argument_spec = argument_spec ,
supports_check_mode = True ,
2017-06-26 13:07:29 +00:00
required_one_of = [ [ ' name ' , ' group_id ' ] ] ,
required_if = [ [ ' state ' , ' present ' , [ ' name ' ] ] ] ,
2014-09-26 01:01:01 +00:00
)
name = module . params [ ' name ' ]
2017-06-26 13:07:29 +00:00
group_id = module . params [ ' group_id ' ]
2014-09-26 01:01:01 +00:00
description = module . params [ ' description ' ]
vpc_id = module . params [ ' vpc_id ' ]
2018-09-17 18:31:41 +00:00
rules = flatten_nested_targets ( module , deepcopy ( module . params [ ' rules ' ] ) )
rules_egress = flatten_nested_targets ( module , deepcopy ( module . params [ ' rules_egress ' ] ) )
rules = deduplicate_rules_args ( rules_expand_sources ( rules_expand_ports ( rules ) ) )
rules_egress = deduplicate_rules_args ( rules_expand_sources ( rules_expand_ports ( rules_egress ) ) )
2014-09-26 01:01:01 +00:00
state = module . params . get ( ' state ' )
purge_rules = module . params [ ' purge_rules ' ]
purge_rules_egress = module . params [ ' purge_rules_egress ' ]
2017-08-22 15:11:38 +00:00
tags = module . params [ ' tags ' ]
purge_tags = module . params [ ' purge_tags ' ]
2014-09-26 01:01:01 +00:00
2017-03-07 19:55:17 +00:00
if state == ' present ' and not description :
module . fail_json ( msg = ' Must provide description when state is present. ' )
2014-09-26 01:01:01 +00:00
changed = False
2018-05-24 15:53:21 +00:00
client = module . client ( ' ec2 ' )
2017-10-25 01:18:56 +00:00
2018-05-24 15:53:21 +00:00
verify_rules_with_descriptions_permitted ( client , module , rules , rules_egress )
group , groups = group_exists ( client , module , vpc_id , group_id , name )
group_created_new = not bool ( group )
2014-09-26 01:01:01 +00:00
2018-05-24 15:53:21 +00:00
global current_account_id
current_account_id = get_aws_account_id ( module )
2014-09-26 01:01:01 +00:00
2018-08-23 23:43:18 +00:00
before = { }
after = { }
2014-09-26 01:01:01 +00:00
# Ensure requested group is absent
if state == ' absent ' :
if group :
2017-03-26 13:33:29 +00:00
# found a match, delete it
2018-08-23 23:43:18 +00:00
before = camel_dict_to_snake_dict ( group , ignore_list = [ ' Tags ' ] )
before [ ' tags ' ] = boto3_tag_list_to_ansible_dict ( before . get ( ' tags ' , [ ] ) )
2014-09-26 01:01:01 +00:00
try :
2016-03-30 18:48:20 +00:00
if not module . check_mode :
2017-08-01 10:53:43 +00:00
client . delete_security_group ( GroupId = group [ ' GroupId ' ] )
2018-05-24 15:53:21 +00:00
except ( BotoCoreError , ClientError ) as e :
module . fail_json_aws ( e , msg = " Unable to delete security group ' %s ' " % group )
2014-09-26 01:01:01 +00:00
else :
group = None
changed = True
else :
2017-03-26 13:33:29 +00:00
# no match found, no changes required
pass
2014-09-26 01:01:01 +00:00
# Ensure requested group is present
elif state == ' present ' :
if group :
2017-03-26 13:33:29 +00:00
# existing group
2018-08-23 23:43:18 +00:00
before = camel_dict_to_snake_dict ( group , ignore_list = [ ' Tags ' ] )
before [ ' tags ' ] = boto3_tag_list_to_ansible_dict ( before . get ( ' tags ' , [ ] ) )
2017-08-01 10:53:43 +00:00
if group [ ' Description ' ] != description :
2017-10-18 13:21:55 +00:00
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. " )
2014-09-26 01:01:01 +00:00
else :
2017-03-26 13:33:29 +00:00
# no match found, create it
2018-05-24 15:53:21 +00:00
group = create_security_group ( client , module , name , description , vpc_id )
2014-09-26 01:01:01 +00:00
changed = True
2017-08-22 15:11:38 +00:00
2018-02-21 19:48:51 +00:00
if tags is not None and group is not None :
2017-08-22 15:11:38 +00:00
current_tags = boto3_tag_list_to_ansible_dict ( group . get ( ' Tags ' , [ ] ) )
2018-05-24 15:53:21 +00:00
changed | = update_tags ( client , module , group [ ' GroupId ' ] , current_tags , tags , purge_tags )
2017-08-22 15:11:38 +00:00
2014-09-26 01:01:01 +00:00
if group :
2018-05-24 15:53:21 +00:00
named_tuple_ingress_list = [ ]
named_tuple_egress_list = [ ]
current_ingress = sum ( [ list ( rule_from_group_permission ( p ) ) for p in group [ ' IpPermissions ' ] ] , [ ] )
current_egress = sum ( [ list ( rule_from_group_permission ( p ) ) for p in group [ ' IpPermissionsEgress ' ] ] , [ ] )
for new_rules , rule_type , named_tuple_rule_list in [ ( rules , ' in ' , named_tuple_ingress_list ) ,
( rules_egress , ' out ' , named_tuple_egress_list ) ] :
if new_rules is None :
continue
for rule in new_rules :
target_type , target , target_group_created = get_target_from_rule (
module , client , rule , name , group , groups , vpc_id )
changed | = target_group_created
if rule . get ( ' proto ' , ' tcp ' ) in ( ' all ' , ' -1 ' , - 1 ) :
rule [ ' proto ' ] = ' -1 '
2014-09-26 01:01:01 +00:00
rule [ ' from_port ' ] = None
rule [ ' to_port ' ] = None
2018-09-05 17:34:26 +00:00
try :
int ( rule . get ( ' proto ' , ' tcp ' ) )
rule [ ' proto ' ] = to_text ( rule . get ( ' proto ' , ' tcp ' ) )
rule [ ' from_port ' ] = None
rule [ ' to_port ' ] = None
except ValueError :
# rule does not use numeric protocol spec
pass
2014-09-26 01:01:01 +00:00
2018-05-24 15:53:21 +00:00
named_tuple_rule_list . append (
Rule (
port_range = ( rule [ ' from_port ' ] , rule [ ' to_port ' ] ) ,
2018-09-05 17:34:26 +00:00
protocol = to_text ( rule . get ( ' proto ' , ' tcp ' ) ) ,
2018-05-24 15:53:21 +00:00
target = target , target_type = target_type ,
description = rule . get ( ' rule_desc ' ) ,
)
)
# List comprehensions for rules to add, rules to modify, and rule ids to determine purging
new_ingress_permissions = [ to_permission ( r ) for r in ( set ( named_tuple_ingress_list ) - set ( current_ingress ) ) ]
new_egress_permissions = [ to_permission ( r ) for r in ( set ( named_tuple_egress_list ) - set ( current_egress ) ) ]
present_ingress = list ( set ( named_tuple_ingress_list ) . union ( set ( current_ingress ) ) )
present_egress = list ( set ( named_tuple_egress_list ) . union ( set ( current_egress ) ) )
if module . params . get ( ' rules_egress ' ) is None and ' VpcId ' in group :
2017-09-13 18:19:05 +00:00
# when no egress rules are specified and we're in a VPC,
2014-09-26 01:01:01 +00:00
# we add in a default allow all out rule, which was the
# default behavior before egress rules were added
2018-05-24 15:53:21 +00:00
rule = Rule ( ( None , None ) , ' -1 ' , ' 0.0.0.0/0 ' , ' ipv4 ' , None )
if rule in current_egress :
named_tuple_egress_list . append ( rule )
if rule not in current_egress :
current_egress . append ( rule )
2014-09-26 01:01:01 +00:00
2018-05-24 15:53:21 +00:00
# List comprehensions for rules to add, rules to modify, and rule ids to determine purging
present_ingress = list ( set ( named_tuple_ingress_list ) . union ( set ( current_ingress ) ) )
present_egress = list ( set ( named_tuple_egress_list ) . union ( set ( current_egress ) ) )
if purge_rules :
revoke_ingress = [ to_permission ( r ) for r in set ( present_ingress ) - set ( named_tuple_ingress_list ) ]
else :
revoke_ingress = [ ]
if purge_rules_egress and module . params . get ( ' rules_egress ' ) is not None :
if module . params . get ( ' rules_egress ' ) is [ ] :
revoke_egress = [
to_permission ( r ) for r in set ( present_egress ) - set ( named_tuple_egress_list )
if r != Rule ( ( None , None ) , ' -1 ' , ' 0.0.0.0/0 ' , ' ipv4 ' , None )
]
else :
revoke_egress = [ to_permission ( r ) for r in set ( present_egress ) - set ( named_tuple_egress_list ) ]
else :
revoke_egress = [ ]
changed | = update_rule_descriptions ( module , group [ ' GroupId ' ] , present_ingress , named_tuple_ingress_list , present_egress , named_tuple_egress_list )
# Revoke old rules
changed | = remove_old_permissions ( client , module , revoke_ingress , revoke_egress , group [ ' GroupId ' ] )
rule_msg = ' Revoking {0} , and egress {1} ' . format ( revoke_ingress , revoke_egress )
new_ingress_permissions = [ to_permission ( r ) for r in ( set ( named_tuple_ingress_list ) - set ( current_ingress ) ) ]
new_ingress_permissions = rules_to_permissions ( set ( named_tuple_ingress_list ) - set ( current_ingress ) )
new_egress_permissions = rules_to_permissions ( set ( named_tuple_egress_list ) - set ( current_egress ) )
# Authorize new rules
changed | = add_new_permissions ( client , module , new_ingress_permissions , new_egress_permissions , group [ ' GroupId ' ] )
if group_created_new and module . params . get ( ' rules ' ) is None and module . params . get ( ' rules_egress ' ) is None :
# A new group with no rules provided is already being awaited.
# When it is created we wait for the default egress rule to be added by AWS
security_group = get_security_groups_with_backoff ( client , GroupIds = [ group [ ' GroupId ' ] ] ) [ ' SecurityGroups ' ] [ 0 ]
elif changed and not module . check_mode :
security_group = wait_for_rule_propagation ( module , group , named_tuple_ingress_list , named_tuple_egress_list , purge_rules , purge_rules_egress )
else :
security_group = get_security_groups_with_backoff ( client , GroupIds = [ group [ ' GroupId ' ] ] ) [ ' SecurityGroups ' ] [ 0 ]
2018-08-23 23:43:18 +00:00
security_group = camel_dict_to_snake_dict ( security_group , ignore_list = [ ' Tags ' ] )
security_group [ ' tags ' ] = boto3_tag_list_to_ansible_dict ( security_group . get ( ' tags ' , [ ] ) )
2014-09-26 01:01:01 +00:00
else :
2018-08-23 23:43:18 +00:00
security_group = { ' group_id ' : None }
if module . _diff :
if module . params [ ' state ' ] == ' present ' :
after = get_diff_final_resource ( client , module , security_group )
security_group [ ' diff ' ] = [ { ' before ' : before , ' after ' : after } ]
module . exit_json ( changed = changed , * * security_group )
2014-09-26 01:01:01 +00:00
2017-08-22 15:11:38 +00:00
2016-12-05 17:08:15 +00:00
if __name__ == ' __main__ ' :
main ( )