Port ec2_tag to boto3 (#39712)

* Add volume manipulation to EC2 integration test policy

* Port ec2_tag to boto3
This commit is contained in:
flowerysong 2018-07-27 15:45:18 -04:00 committed by ansibot
parent 74af52533f
commit a08668cf00
3 changed files with 206 additions and 97 deletions

View file

@ -95,8 +95,10 @@
"ec2:AuthorizeSecurityGroupIngress",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:CreateTags",
"ec2:CreateVolume",
"ec2:DeleteRouteTable",
"ec2:DeleteSecurityGroup",
"ec2:DeleteVolume",
"ec2:RevokeSecurityGroupEgress",
"ec2:RevokeSecurityGroupIngress",
"ec2:RunInstances",

View file

@ -14,11 +14,12 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
DOCUMENTATION = '''
---
module: ec2_tag
short_description: create and remove tag(s) to ec2 resources.
short_description: create and remove tags on ec2 resources.
description:
- Creates, removes and lists tags from any EC2 resource. The resource is referenced by its resource id (e.g. an instance being i-XXXXXXX).
It is designed to be used with complex args (tags), see the examples. This module has a dependency on python-boto.
- Creates, removes and lists tags for any EC2 resource. The resource is referenced by its resource id (e.g. an instance being i-XXXXXXX).
It is designed to be used with complex args (tags), see the examples.
version_added: "1.3"
requirements: [ "boto3", "botocore" ]
options:
resource:
description:
@ -33,8 +34,17 @@ options:
description:
- a hash/dictionary of tags to add to the resource; '{"key":"value"}' and '{"key":"value","key":"value"}'
required: true
purge_tags:
description:
- Whether unspecified tags should be removed from the resource.
- "Note that when combined with C(state: absent), specified tags with non-matching values are not purged."
type: bool
default: no
version_added: '2.7'
author: "Lester Wade (@lwade)"
author:
- Lester Wade (@lwade)
- Paul Arthur (@flowerysong)
extends_documentation_fragment:
- aws
- ec2
@ -50,36 +60,6 @@ EXAMPLES = '''
Name: ubervol
env: prod
- name: Ensure one dbserver is running
ec2:
count_tag:
Name: dbserver
Env: production
exact_count: 1
group: '{{ security_group }}'
keypair: '{{ keypair }}'
image: '{{ image_id }}'
instance_tags:
Name: dbserver
Env: production
instance_type: '{{ instance_type }}'
region: eu-west-1
volumes:
- device_name: /dev/xvdb
device_type: standard
volume_size: 10
delete_on_termination: True
wait: True
register: ec2
- name: Retrieve all volumes for a queried instance
ec2_vol:
instance: '{{ item.id }}'
region: eu-west-1
state: list
with_items: '{{ ec2.tagged_instances }}'
register: ec2_vol
- name: Ensure all volumes are tagged
ec2_tag:
region: eu-west-1
@ -90,93 +70,112 @@ EXAMPLES = '''
Env: production
with_items: '{{ ec2_vol.volumes }}'
- name: Get EC2 facts
action: ec2_facts
- name: Retrieve all tags on an instance
ec2_tag:
region: '{{ ansible_ec2_placement_region }}'
resource: '{{ ansible_ec2_instance_id }}'
region: eu-west-1
resource: i-xxxxxxxxxxxxxxxxx
state: list
register: ec2_tags
- name: List tags, such as Name and env
debug:
msg: '{{ ec2_tags.tags.Name }} {{ ec2_tags.tags.env }}'
- name: Remove all tags except for Name from an instance
ec2_tag:
region: eu-west-1
resource: i-xxxxxxxxxxxxxxxxx
tags:
Name: ''
state: absent
purge_tags: true
'''
RETURN = '''
tags:
description: A dict containing the tags on the resource
returned: always
type: dict
added_tags:
description: A dict of tags that were added to the resource
returned: If tags were added
type: dict
removed_tags:
description: A dict of tags that were removed from the resource
returned: If tags were removed
type: dict
'''
from ansible.module_utils.aws.core import AnsibleAWSModule
from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list, compare_aws_tags
try:
import boto.ec2
HAS_BOTO = True
except ImportError:
HAS_BOTO = False
from botocore.exceptions import BotoCoreError, ClientError
except:
pass # Handled by AnsibleAWSModule
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ec2 import HAS_BOTO, ec2_argument_spec, ec2_connect
def get_tags(ec2, module, resource):
filters = [{'Name': 'resource-id', 'Values': [resource]}]
try:
return boto3_tag_list_to_ansible_dict(ec2.describe_tags(Filters=filters)['Tags'])
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg='Failed to fetch tags for resource {0}'.format(resource))
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(dict(
argument_spec = dict(
resource=dict(required=True),
tags=dict(type='dict'),
purge_tags=dict(type='bool', default=False),
state=dict(default='present', choices=['present', 'absent', 'list']),
)
)
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
required_if = [('state', 'present', ['tags']), ('state', 'absent', ['tags'])]
if not HAS_BOTO:
module.fail_json(msg='boto required for this module')
module = AnsibleAWSModule(argument_spec=argument_spec, required_if=required_if, supports_check_mode=True)
resource = module.params.get('resource')
tags = module.params.get('tags')
state = module.params.get('state')
resource = module.params['resource']
tags = module.params['tags']
state = module.params['state']
purge_tags = module.params['purge_tags']
ec2 = ec2_connect(module)
result = {'changed': False}
# We need a comparison here so that we can accurately report back changed status.
# Need to expand the gettags return format and compare with "tags" and then tag or detag as appropriate.
filters = {'resource-id': resource}
gettags = ec2.get_all_tags(filters=filters)
ec2 = module.client('ec2')
dictadd = {}
dictremove = {}
baddict = {}
tagdict = {}
for tag in gettags:
tagdict[tag.name] = tag.value
if state == 'present':
if not tags:
module.fail_json(msg="tags argument is required when state is present")
if set(tags.items()).issubset(set(tagdict.items())):
module.exit_json(msg="Tags already exists in %s." % resource, changed=False)
else:
for (key, value) in set(tags.items()):
if (key, value) not in set(tagdict.items()):
dictadd[key] = value
if not module.check_mode:
ec2.create_tags(resource, dictadd)
module.exit_json(msg="Tags %s created for resource %s." % (dictadd, resource), changed=True)
if state == 'absent':
if not tags:
module.fail_json(msg="tags argument is required when state is absent")
for (key, value) in set(tags.items()):
if (key, value) not in set(tagdict.items()):
baddict[key] = value
if set(baddict) == set(tags):
module.exit_json(msg="Nothing to remove here. Move along.", changed=False)
for (key, value) in set(tags.items()):
if (key, value) in set(tagdict.items()):
dictremove[key] = value
if not module.check_mode:
ec2.delete_tags(resource, dictremove)
module.exit_json(msg="Tags %s removed for resource %s." % (dictremove, resource), changed=True)
current_tags = get_tags(ec2, module, resource)
if state == 'list':
module.exit_json(changed=False, tags=tagdict)
module.exit_json(changed=False, tags=current_tags)
add_tags, remove = compare_aws_tags(current_tags, tags, purge_tags=purge_tags)
remove_tags = {}
if state == 'absent':
for key in tags:
if key in current_tags and current_tags[key] == tags[key]:
remove_tags[key] = tags[key]
for key in remove:
remove_tags[key] = current_tags[key]
if remove_tags:
result['changed'] = True
result['removed_tags'] = remove_tags
if not module.check_mode:
try:
ec2.delete_tags(Resources=[resource], Tags=ansible_dict_to_boto3_tag_list(remove_tags))
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg='Failed to remove tags {0} from resource {1}'.format(remove_tags, resource))
if state == 'present' and add_tags:
result['changed'] = True
result['added_tags'] = add_tags
current_tags.update(add_tags)
if not module.check_mode:
try:
ec2.create_tags(Resources=[resource], Tags=ansible_dict_to_boto3_tag_list(add_tags))
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg='Failed to set tags {0} on resource {1}'.format(add_tags, resource))
result['tags'] = get_tags(ec2, module, resource)
module.exit_json(**result)
if __name__ == '__main__':

View file

@ -1,2 +1,110 @@
---
# tasks file for test_ec2_tag
- name: Set up AWS connection info
set_fact:
aws_connection_info: &aws_connection_info
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
security_token: "{{ security_token }}"
region: "{{ aws_region }}"
no_log: true
- block:
- name: Create an EC2 volume so we have something to tag
ec2_vol:
name: "{{ resource_prefix }} ec2_tag volume"
volume_size: 1
state: present
zone: "{{ aws_region }}a"
<<: *aws_connection_info
register: volume
- name: List the tags
ec2_tag:
resource: "{{ volume.volume_id }}"
state: list
<<: *aws_connection_info
register: result
- assert:
that:
- result.tags | length == 1
- result.tags.Name == '{{ resource_prefix }} ec2_tag volume'
- name: Set some new tags
ec2_tag:
resource: "{{ volume.volume_id }}"
state: present
tags:
foo: foo
bar: baz
<<: *aws_connection_info
register: result
- assert:
that:
- result is changed
- result.tags | length == 3
- result.added_tags | length == 2
- result.tags.Name == '{{ resource_prefix }} ec2_tag volume'
- result.tags.foo == 'foo'
- result.tags.bar == 'baz'
- name: Remove a tag
ec2_tag:
resource: "{{ volume.volume_id }}"
state: absent
tags:
foo: foo
<<: *aws_connection_info
register: result
- assert:
that:
- result is changed
- result.tags | length == 2
- "'added_tags' not in result"
- result.removed_tags | length == 1
- result.tags.Name == '{{ resource_prefix }} ec2_tag volume'
- result.tags.bar == 'baz'
- name: Set an exclusive tag
ec2_tag:
resource: "{{ volume.volume_id }}"
purge_tags: true
tags:
baz: quux
<<: *aws_connection_info
register: result
- assert:
that:
- result is changed
- result.tags | length == 1
- result.added_tags | length == 1
- result.removed_tags | length == 2
- result.tags.baz == 'quux'
- name: Remove all tags
ec2_tag:
resource: "{{ volume.volume_id }}"
purge_tags: true
tags: {}
<<: *aws_connection_info
register: result
- assert:
that:
- result is changed
- result.tags | length == 0
always:
- name: Remove the volume
ec2_vol:
id: "{{ volume.volume_id }}"
state: absent
<<: *aws_connection_info
register: result
until: result is not failed
ignore_errors: yes
retries: 10