Port ec2_tag to boto3 (#39712)
* Add volume manipulation to EC2 integration test policy * Port ec2_tag to boto3
This commit is contained in:
parent
74af52533f
commit
a08668cf00
3 changed files with 206 additions and 97 deletions
|
@ -95,8 +95,10 @@
|
||||||
"ec2:AuthorizeSecurityGroupIngress",
|
"ec2:AuthorizeSecurityGroupIngress",
|
||||||
"ec2:AuthorizeSecurityGroupEgress",
|
"ec2:AuthorizeSecurityGroupEgress",
|
||||||
"ec2:CreateTags",
|
"ec2:CreateTags",
|
||||||
|
"ec2:CreateVolume",
|
||||||
"ec2:DeleteRouteTable",
|
"ec2:DeleteRouteTable",
|
||||||
"ec2:DeleteSecurityGroup",
|
"ec2:DeleteSecurityGroup",
|
||||||
|
"ec2:DeleteVolume",
|
||||||
"ec2:RevokeSecurityGroupEgress",
|
"ec2:RevokeSecurityGroupEgress",
|
||||||
"ec2:RevokeSecurityGroupIngress",
|
"ec2:RevokeSecurityGroupIngress",
|
||||||
"ec2:RunInstances",
|
"ec2:RunInstances",
|
||||||
|
|
|
@ -14,11 +14,12 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
DOCUMENTATION = '''
|
DOCUMENTATION = '''
|
||||||
---
|
---
|
||||||
module: ec2_tag
|
module: ec2_tag
|
||||||
short_description: create and remove tag(s) to ec2 resources.
|
short_description: create and remove tags on ec2 resources.
|
||||||
description:
|
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).
|
- 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. This module has a dependency on python-boto.
|
It is designed to be used with complex args (tags), see the examples.
|
||||||
version_added: "1.3"
|
version_added: "1.3"
|
||||||
|
requirements: [ "boto3", "botocore" ]
|
||||||
options:
|
options:
|
||||||
resource:
|
resource:
|
||||||
description:
|
description:
|
||||||
|
@ -33,8 +34,17 @@ options:
|
||||||
description:
|
description:
|
||||||
- a hash/dictionary of tags to add to the resource; '{"key":"value"}' and '{"key":"value","key":"value"}'
|
- a hash/dictionary of tags to add to the resource; '{"key":"value"}' and '{"key":"value","key":"value"}'
|
||||||
required: true
|
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:
|
extends_documentation_fragment:
|
||||||
- aws
|
- aws
|
||||||
- ec2
|
- ec2
|
||||||
|
@ -50,36 +60,6 @@ EXAMPLES = '''
|
||||||
Name: ubervol
|
Name: ubervol
|
||||||
env: prod
|
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
|
- name: Ensure all volumes are tagged
|
||||||
ec2_tag:
|
ec2_tag:
|
||||||
region: eu-west-1
|
region: eu-west-1
|
||||||
|
@ -90,93 +70,112 @@ EXAMPLES = '''
|
||||||
Env: production
|
Env: production
|
||||||
with_items: '{{ ec2_vol.volumes }}'
|
with_items: '{{ ec2_vol.volumes }}'
|
||||||
|
|
||||||
- name: Get EC2 facts
|
|
||||||
action: ec2_facts
|
|
||||||
|
|
||||||
- name: Retrieve all tags on an instance
|
- name: Retrieve all tags on an instance
|
||||||
ec2_tag:
|
ec2_tag:
|
||||||
region: '{{ ansible_ec2_placement_region }}'
|
region: eu-west-1
|
||||||
resource: '{{ ansible_ec2_instance_id }}'
|
resource: i-xxxxxxxxxxxxxxxxx
|
||||||
state: list
|
state: list
|
||||||
register: ec2_tags
|
register: ec2_tags
|
||||||
|
|
||||||
- name: List tags, such as Name and env
|
- name: Remove all tags except for Name from an instance
|
||||||
debug:
|
ec2_tag:
|
||||||
msg: '{{ ec2_tags.tags.Name }} {{ ec2_tags.tags.env }}'
|
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:
|
try:
|
||||||
import boto.ec2
|
from botocore.exceptions import BotoCoreError, ClientError
|
||||||
HAS_BOTO = True
|
except:
|
||||||
except ImportError:
|
pass # Handled by AnsibleAWSModule
|
||||||
HAS_BOTO = False
|
|
||||||
|
|
||||||
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():
|
def main():
|
||||||
argument_spec = ec2_argument_spec()
|
argument_spec = dict(
|
||||||
argument_spec.update(dict(
|
|
||||||
resource=dict(required=True),
|
resource=dict(required=True),
|
||||||
tags=dict(type='dict'),
|
tags=dict(type='dict'),
|
||||||
|
purge_tags=dict(type='bool', default=False),
|
||||||
state=dict(default='present', choices=['present', 'absent', 'list']),
|
state=dict(default='present', choices=['present', 'absent', 'list']),
|
||||||
)
|
)
|
||||||
)
|
required_if = [('state', 'present', ['tags']), ('state', 'absent', ['tags'])]
|
||||||
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
|
|
||||||
|
|
||||||
if not HAS_BOTO:
|
module = AnsibleAWSModule(argument_spec=argument_spec, required_if=required_if, supports_check_mode=True)
|
||||||
module.fail_json(msg='boto required for this module')
|
|
||||||
|
|
||||||
resource = module.params.get('resource')
|
resource = module.params['resource']
|
||||||
tags = module.params.get('tags')
|
tags = module.params['tags']
|
||||||
state = module.params.get('state')
|
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.
|
ec2 = module.client('ec2')
|
||||||
# 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)
|
|
||||||
|
|
||||||
dictadd = {}
|
current_tags = get_tags(ec2, module, resource)
|
||||||
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)
|
|
||||||
|
|
||||||
if state == 'list':
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -1,2 +1,110 @@
|
||||||
---
|
---
|
||||||
# tasks file for test_ec2_tag
|
# 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
|
||||||
|
|
Loading…
Reference in a new issue