Add diff mode for ec2_group (#44533)
* Add (preview) diff mode support ec2_group * Add diff mode to some ec2_group integration tests * Remove unnecessary arguments and add comment to the module notes * Add changelog
This commit is contained in:
parent
b152515fcb
commit
79ecb4c41f
5 changed files with 149 additions and 5 deletions
4
changelogs/fragments/ec2_group_diff_mode_support.yml
Normal file
4
changelogs/fragments/ec2_group_diff_mode_support.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
minor_changes:
|
||||||
|
- ec2_group - Add diff mode support with and without check mode. This feature
|
||||||
|
is preview and may change when a common framework is adopted for AWS modules.
|
|
@ -117,6 +117,7 @@ class AnsibleAWSModule(object):
|
||||||
msg='Python modules "botocore" or "boto3" are missing, please install both')
|
msg='Python modules "botocore" or "boto3" are missing, please install both')
|
||||||
|
|
||||||
self.check_mode = self._module.check_mode
|
self.check_mode = self._module.check_mode
|
||||||
|
self._diff = self._module._diff
|
||||||
self._name = self._module._name
|
self._name = self._module._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -110,6 +110,7 @@ notes:
|
||||||
- If a rule declares a group_name and that group doesn't exist, it will be
|
- 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.
|
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.
|
The module will refuse to create a depended-on group without a description.
|
||||||
|
- Preview diff mode support is added in version 2.7.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EXAMPLES = '''
|
EXAMPLES = '''
|
||||||
|
@ -222,6 +223,7 @@ EXAMPLES = '''
|
||||||
- 64:ff9b::/96
|
- 64:ff9b::/96
|
||||||
group_id:
|
group_id:
|
||||||
- sg-edcd9784
|
- sg-edcd9784
|
||||||
|
diff: True
|
||||||
|
|
||||||
- name: "Delete group by its id"
|
- name: "Delete group by its id"
|
||||||
ec2_group:
|
ec2_group:
|
||||||
|
@ -847,6 +849,92 @@ def verify_rules_with_descriptions_permitted(client, module, rules, rules_egress
|
||||||
module.fail_json(msg="Using rule descriptions requires botocore version >= 1.7.2.")
|
module.fail_json(msg="Using rule descriptions requires botocore version >= 1.7.2.")
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
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'):
|
||||||
|
if rule.get('ports') and isinstance(rule.get('ports'), string_types):
|
||||||
|
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:
|
||||||
|
format_rule[rule_key] = [{source_type: rule[source_type]}]
|
||||||
|
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'])}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
argument_spec = dict(
|
argument_spec = dict(
|
||||||
name=dict(),
|
name=dict(),
|
||||||
|
@ -893,10 +981,15 @@ def main():
|
||||||
global current_account_id
|
global current_account_id
|
||||||
current_account_id = get_aws_account_id(module)
|
current_account_id = get_aws_account_id(module)
|
||||||
|
|
||||||
|
before = {}
|
||||||
|
after = {}
|
||||||
|
|
||||||
# Ensure requested group is absent
|
# Ensure requested group is absent
|
||||||
if state == 'absent':
|
if state == 'absent':
|
||||||
if group:
|
if group:
|
||||||
# found a match, delete it
|
# found a match, delete it
|
||||||
|
before = camel_dict_to_snake_dict(group, ignore_list=['Tags'])
|
||||||
|
before['tags'] = boto3_tag_list_to_ansible_dict(before.get('tags', []))
|
||||||
try:
|
try:
|
||||||
if not module.check_mode:
|
if not module.check_mode:
|
||||||
client.delete_security_group(GroupId=group['GroupId'])
|
client.delete_security_group(GroupId=group['GroupId'])
|
||||||
|
@ -913,6 +1006,8 @@ def main():
|
||||||
elif state == 'present':
|
elif state == 'present':
|
||||||
if group:
|
if group:
|
||||||
# existing group
|
# existing group
|
||||||
|
before = camel_dict_to_snake_dict(group, ignore_list=['Tags'])
|
||||||
|
before['tags'] = boto3_tag_list_to_ansible_dict(before.get('tags', []))
|
||||||
if group['Description'] != description:
|
if group['Description'] != description:
|
||||||
module.warn("Group description does not match existing group. Descriptions cannot be changed without deleting "
|
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.")
|
"and re-creating the security group. Try using state=absent to delete, then rerunning this task.")
|
||||||
|
@ -1009,12 +1104,19 @@ def main():
|
||||||
security_group = wait_for_rule_propagation(module, group, named_tuple_ingress_list, named_tuple_egress_list, purge_rules, purge_rules_egress)
|
security_group = wait_for_rule_propagation(module, group, named_tuple_ingress_list, named_tuple_egress_list, purge_rules, purge_rules_egress)
|
||||||
else:
|
else:
|
||||||
security_group = get_security_groups_with_backoff(client, GroupIds=[group['GroupId']])['SecurityGroups'][0]
|
security_group = get_security_groups_with_backoff(client, GroupIds=[group['GroupId']])['SecurityGroups'][0]
|
||||||
security_group = camel_dict_to_snake_dict(security_group)
|
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', []),
|
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:
|
else:
|
||||||
module.exit_json(changed=changed, group_id=None)
|
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)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -121,6 +121,29 @@
|
||||||
that:
|
that:
|
||||||
- result.ip_permissions_egress|length == 2
|
- result.ip_permissions_egress|length == 2
|
||||||
|
|
||||||
|
- name: Purge the second rule (CHECK MODE) (DIFF MODE)
|
||||||
|
ec2_group:
|
||||||
|
name: '{{ec2_group_name}}-egress-tests'
|
||||||
|
description: '{{ec2_group_description}}'
|
||||||
|
vpc_id: '{{ vpc_result.vpc.id }}'
|
||||||
|
rules_egress:
|
||||||
|
- proto: tcp
|
||||||
|
ports:
|
||||||
|
- 1212
|
||||||
|
cidr_ip: 1.2.1.2/32
|
||||||
|
<<: *aws_connection_info
|
||||||
|
state: present
|
||||||
|
register: result
|
||||||
|
check_mode: True
|
||||||
|
diff: True
|
||||||
|
|
||||||
|
- name: assert first rule will be left
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- result.changed
|
||||||
|
- result.diff.0.after.ip_permissions_egress|length == 1
|
||||||
|
- result.diff.0.after.ip_permissions_egress[0].ip_ranges[0].cidr_ip == '1.2.1.2/32'
|
||||||
|
|
||||||
- name: Purge the second rule
|
- name: Purge the second rule
|
||||||
ec2_group:
|
ec2_group:
|
||||||
name: '{{ec2_group_name}}-egress-tests'
|
name: '{{ec2_group_name}}-egress-tests'
|
||||||
|
|
|
@ -288,12 +288,15 @@
|
||||||
cidr_ipv6: "64:ff9b::/96"
|
cidr_ipv6: "64:ff9b::/96"
|
||||||
<<: *aws_connection_info
|
<<: *aws_connection_info
|
||||||
check_mode: true
|
check_mode: true
|
||||||
|
diff: true
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
- name: assert state=present (expected changed=true)
|
- name: assert state=present (expected changed=true)
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- 'result.changed'
|
- 'result.changed'
|
||||||
|
- 'result.diff.0.before.ip_permissions == result.diff.0.after.ip_permissions'
|
||||||
|
- 'result.diff.0.before.ip_permissions_egress != result.diff.0.after.ip_permissions_egress'
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
- name: test rules_egress state=present for ipv6 (expected changed=true)
|
- name: test rules_egress state=present for ipv6 (expected changed=true)
|
||||||
|
@ -330,12 +333,14 @@
|
||||||
vpc_id: '{{ vpc_result.vpc.id }}'
|
vpc_id: '{{ vpc_result.vpc.id }}'
|
||||||
<<: *aws_connection_info
|
<<: *aws_connection_info
|
||||||
check_mode: true
|
check_mode: true
|
||||||
|
diff: true
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
- name: assert group was removed
|
- name: assert group was removed
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- 'result.changed'
|
- 'result.changed'
|
||||||
|
- 'not result.diff.0.after'
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
- name: test state=absent (expected changed=true)
|
- name: test state=absent (expected changed=true)
|
||||||
|
@ -405,8 +410,13 @@
|
||||||
to_port: 8182
|
to_port: 8182
|
||||||
cidr_ip: "1.1.1.1/32"
|
cidr_ip: "1.1.1.1/32"
|
||||||
check_mode: true
|
check_mode: true
|
||||||
|
diff: true
|
||||||
register: check_result
|
register: check_result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- not check_result.changed
|
||||||
|
- check_result.diff.0.before.ip_permissions.0 == check_result.diff.0.after.ip_permissions.0
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
- name: add same rule to the existing group (expected changed=false)
|
- name: add same rule to the existing group (expected changed=false)
|
||||||
|
@ -877,12 +887,16 @@
|
||||||
tag1: test1
|
tag1: test1
|
||||||
tag2: test2
|
tag2: test2
|
||||||
check_mode: true
|
check_mode: true
|
||||||
|
diff: true
|
||||||
register: result
|
register: result
|
||||||
|
|
||||||
- name: assert that tags were added (expected changed=true)
|
- name: assert that tags were added (expected changed=true)
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- 'result.changed'
|
- 'result.changed'
|
||||||
|
- 'not result.diff.0.before.tags'
|
||||||
|
- 'result.diff.0.after.tags.tag1 == "test1"'
|
||||||
|
- 'result.diff.0.after.tags.tag2 == "test2"'
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
- name: test adding tags (expected changed=true)
|
- name: test adding tags (expected changed=true)
|
||||||
|
|
Loading…
Reference in a new issue