Update ec2_vol.py
Changed=true now reported on new volume.
Only detach volume when instance is specified as 'None' or '' rather than whenever instance is not specified at all
Fix regression caused by 6b27cdc
where by no volume is created if id or Name is not supplied
Remove unnecessary empty aliases
Corrected example to use acceptable parameter for ions
Added exception handling to get_all_instances call
Moved the attachment state validation code to attach_volume function rather than create_volume function
Refactored attach_volume and detach_volume so that changed state can be passed back to call
Created get_volume_info function so that state=present and state=list can return the same data. Also added instance_id as a returned value in attachment_set dict
Updated aws connection method so that boto profile can be used
This commit is contained in:
parent
f35ab5faca
commit
3360cef024
1 changed files with 123 additions and 89 deletions
|
@ -27,41 +27,35 @@ options:
|
||||||
- instance ID if you wish to attach the volume. Since 1.9 you can set to None to detach.
|
- instance ID if you wish to attach the volume. Since 1.9 you can set to None to detach.
|
||||||
required: false
|
required: false
|
||||||
default: null
|
default: null
|
||||||
aliases: []
|
|
||||||
name:
|
name:
|
||||||
description:
|
description:
|
||||||
- volume Name tag if you wish to attach an existing volume (requires instance)
|
- volume Name tag if you wish to attach an existing volume (requires instance)
|
||||||
required: false
|
required: false
|
||||||
default: null
|
default: null
|
||||||
aliases: []
|
|
||||||
version_added: "1.6"
|
version_added: "1.6"
|
||||||
id:
|
id:
|
||||||
description:
|
description:
|
||||||
- volume id if you wish to attach an existing volume (requires instance) or remove an existing volume
|
- volume id if you wish to attach an existing volume (requires instance) or remove an existing volume
|
||||||
required: false
|
required: false
|
||||||
default: null
|
default: null
|
||||||
aliases: []
|
|
||||||
version_added: "1.6"
|
version_added: "1.6"
|
||||||
volume_size:
|
volume_size:
|
||||||
description:
|
description:
|
||||||
- size of volume (in GB) to create.
|
- size of volume (in GB) to create.
|
||||||
required: false
|
required: false
|
||||||
default: null
|
default: null
|
||||||
aliases: []
|
|
||||||
volume_type:
|
volume_type:
|
||||||
description:
|
description:
|
||||||
- Type of EBS volume; standard (magnetic), gp2 (SSD), io1 (Provisioned IOPS). "Standard" is the old EBS default
|
- Type of EBS volume; standard (magnetic), gp2 (SSD), io1 (Provisioned IOPS). "Standard" is the old EBS default
|
||||||
and continues to remain the Ansible default for backwards compatibility.
|
and continues to remain the Ansible default for backwards compatibility.
|
||||||
required: false
|
required: false
|
||||||
default: standard
|
default: standard
|
||||||
aliases: []
|
|
||||||
version_added: "1.9"
|
version_added: "1.9"
|
||||||
iops:
|
iops:
|
||||||
description:
|
description:
|
||||||
- the provisioned IOPs you want to associate with this volume (integer).
|
- the provisioned IOPs you want to associate with this volume (integer).
|
||||||
required: false
|
required: false
|
||||||
default: 100
|
default: 100
|
||||||
aliases: []
|
|
||||||
version_added: "1.3"
|
version_added: "1.3"
|
||||||
encrypted:
|
encrypted:
|
||||||
description:
|
description:
|
||||||
|
@ -73,7 +67,6 @@ options:
|
||||||
- device id to override device mapping. Assumes /dev/sdf for Linux/UNIX and /dev/xvdf for Windows.
|
- device id to override device mapping. Assumes /dev/sdf for Linux/UNIX and /dev/xvdf for Windows.
|
||||||
required: false
|
required: false
|
||||||
default: null
|
default: null
|
||||||
aliases: []
|
|
||||||
zone:
|
zone:
|
||||||
description:
|
description:
|
||||||
- zone in which to create the volume, if unset uses the zone the instance is in (if set)
|
- zone in which to create the volume, if unset uses the zone the instance is in (if set)
|
||||||
|
@ -92,7 +85,6 @@ options:
|
||||||
required: false
|
required: false
|
||||||
default: "yes"
|
default: "yes"
|
||||||
choices: ["yes", "no"]
|
choices: ["yes", "no"]
|
||||||
aliases: []
|
|
||||||
version_added: "1.5"
|
version_added: "1.5"
|
||||||
state:
|
state:
|
||||||
description:
|
description:
|
||||||
|
@ -118,7 +110,7 @@ EXAMPLES = '''
|
||||||
- ec2_vol:
|
- ec2_vol:
|
||||||
instance: XXXXXX
|
instance: XXXXXX
|
||||||
volume_size: 5
|
volume_size: 5
|
||||||
iops: 200
|
iops: 100
|
||||||
device_name: sdd
|
device_name: sdd
|
||||||
|
|
||||||
# Example using snapshot id
|
# Example using snapshot id
|
||||||
|
@ -189,6 +181,7 @@ from distutils.version import LooseVersion
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import boto.ec2
|
import boto.ec2
|
||||||
|
from boto.exception import BotoServerError
|
||||||
HAS_BOTO = True
|
HAS_BOTO = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_BOTO = False
|
HAS_BOTO = False
|
||||||
|
@ -200,6 +193,11 @@ def get_volume(module, ec2):
|
||||||
zone = module.params.get('zone')
|
zone = module.params.get('zone')
|
||||||
filters = {}
|
filters = {}
|
||||||
volume_ids = None
|
volume_ids = None
|
||||||
|
|
||||||
|
# If no name or id supplied, just try volume creation based on module parameters
|
||||||
|
if id is None and name is None:
|
||||||
|
return None
|
||||||
|
|
||||||
if zone:
|
if zone:
|
||||||
filters['availability_zone'] = zone
|
filters['availability_zone'] = zone
|
||||||
if name:
|
if name:
|
||||||
|
@ -219,18 +217,20 @@ def get_volume(module, ec2):
|
||||||
module.fail_json(msg=msg)
|
module.fail_json(msg=msg)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if len(vols) > 1:
|
if len(vols) > 1:
|
||||||
module.fail_json(msg="Found more than one volume in zone (if specified) with name: %s" % name)
|
module.fail_json(msg="Found more than one volume in zone (if specified) with name: %s" % name)
|
||||||
return vols[0]
|
return vols[0]
|
||||||
|
|
||||||
def get_volumes(module, ec2):
|
def get_volumes(module, ec2):
|
||||||
|
|
||||||
instance = module.params.get('instance')
|
instance = module.params.get('instance')
|
||||||
|
|
||||||
if not instance:
|
|
||||||
module.fail_json(msg = "Instance must be specified to get volumes")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vols = ec2.get_all_volumes(filters={'attachment.instance-id': instance})
|
if not instance:
|
||||||
|
vols = ec2.get_all_volumes()
|
||||||
|
else:
|
||||||
|
vols = ec2.get_all_volumes(filters={'attachment.instance-id': instance})
|
||||||
except boto.exception.BotoServerError, e:
|
except boto.exception.BotoServerError, e:
|
||||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||||
return vols
|
return vols
|
||||||
|
@ -254,7 +254,9 @@ def boto_supports_volume_encryption():
|
||||||
"""
|
"""
|
||||||
return hasattr(boto, 'Version') and LooseVersion(boto.Version) >= LooseVersion('2.29.0')
|
return hasattr(boto, 'Version') and LooseVersion(boto.Version) >= LooseVersion('2.29.0')
|
||||||
|
|
||||||
|
|
||||||
def create_volume(module, ec2, zone):
|
def create_volume(module, ec2, zone):
|
||||||
|
changed = False
|
||||||
name = module.params.get('name')
|
name = module.params.get('name')
|
||||||
id = module.params.get('id')
|
id = module.params.get('id')
|
||||||
instance = module.params.get('instance')
|
instance = module.params.get('instance')
|
||||||
|
@ -267,30 +269,15 @@ def create_volume(module, ec2, zone):
|
||||||
if iops:
|
if iops:
|
||||||
volume_type = 'io1'
|
volume_type = 'io1'
|
||||||
|
|
||||||
if instance == 'None' or instance == '':
|
|
||||||
instance = None
|
|
||||||
|
|
||||||
volume = get_volume(module, ec2)
|
volume = get_volume(module, ec2)
|
||||||
if volume:
|
if volume is None:
|
||||||
if volume.attachment_state() is not None:
|
|
||||||
if instance is None:
|
|
||||||
return volume
|
|
||||||
adata = volume.attach_data
|
|
||||||
if adata.instance_id != instance:
|
|
||||||
module.fail_json(msg = "Volume %s is already attached to another instance: %s"
|
|
||||||
% (name or id, adata.instance_id))
|
|
||||||
else:
|
|
||||||
module.exit_json(msg="Volume %s is already mapped on instance %s: %s" %
|
|
||||||
(name or id, adata.instance_id, adata.device),
|
|
||||||
volume_id=id,
|
|
||||||
device=adata.device,
|
|
||||||
changed=False)
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
if boto_supports_volume_encryption():
|
if boto_supports_volume_encryption():
|
||||||
volume = ec2.create_volume(volume_size, zone, snapshot, volume_type, iops, encrypted)
|
volume = ec2.create_volume(volume_size, zone, snapshot, volume_type, iops, encrypted)
|
||||||
|
changed = True
|
||||||
else:
|
else:
|
||||||
volume = ec2.create_volume(volume_size, zone, snapshot, volume_type, iops)
|
volume = ec2.create_volume(volume_size, zone, snapshot, volume_type, iops)
|
||||||
|
changed = True
|
||||||
|
|
||||||
while volume.status != 'available':
|
while volume.status != 'available':
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
@ -301,52 +288,89 @@ def create_volume(module, ec2, zone):
|
||||||
except boto.exception.BotoServerError, e:
|
except boto.exception.BotoServerError, e:
|
||||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||||
|
|
||||||
return volume
|
return volume, changed
|
||||||
|
|
||||||
|
|
||||||
def attach_volume(module, ec2, volume, instance):
|
def attach_volume(module, ec2, volume, instance):
|
||||||
|
|
||||||
device_name = module.params.get('device_name')
|
device_name = module.params.get('device_name')
|
||||||
|
changed = False
|
||||||
if device_name and instance:
|
|
||||||
try:
|
|
||||||
attach = volume.attach(instance.id, device_name)
|
|
||||||
while volume.attachment_state() != 'attached':
|
|
||||||
time.sleep(3)
|
|
||||||
volume.update()
|
|
||||||
except boto.exception.BotoServerError, e:
|
|
||||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
|
||||||
|
|
||||||
# If device_name isn't set, make a choice based on best practices here:
|
# If device_name isn't set, make a choice based on best practices here:
|
||||||
# http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
|
# http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
|
||||||
|
|
||||||
# In future this needs to be more dynamic but combining block device mapping best practices
|
# In future this needs to be more dynamic but combining block device mapping best practices
|
||||||
# (bounds for devices, as above) with instance.block_device_mapping data would be tricky. For me ;)
|
# (bounds for devices, as above) with instance.block_device_mapping data would be tricky. For me ;)
|
||||||
|
|
||||||
# Use password data attribute to tell whether the instance is Windows or Linux
|
# Use password data attribute to tell whether the instance is Windows or Linux
|
||||||
if device_name is None and instance:
|
if device_name is None:
|
||||||
try:
|
try:
|
||||||
if not ec2.get_password_data(instance.id):
|
if not ec2.get_password_data(instance.id):
|
||||||
device_name = '/dev/sdf'
|
device_name = '/dev/sdf'
|
||||||
attach = volume.attach(instance.id, device_name)
|
|
||||||
while volume.attachment_state() != 'attached':
|
|
||||||
time.sleep(3)
|
|
||||||
volume.update()
|
|
||||||
else:
|
else:
|
||||||
device_name = '/dev/xvdf'
|
device_name = '/dev/xvdf'
|
||||||
attach = volume.attach(instance.id, device_name)
|
except boto.exception.BotoServerError, e:
|
||||||
while volume.attachment_state() != 'attached':
|
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||||
time.sleep(3)
|
|
||||||
volume.update()
|
if volume.attachment_state() is not None:
|
||||||
|
adata = volume.attach_data
|
||||||
|
if adata.instance_id != instance.id:
|
||||||
|
module.fail_json(msg = "Volume %s is already attached to another instance: %s"
|
||||||
|
% (volume.id, adata.instance_id))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
volume.attach(instance.id, device_name)
|
||||||
|
while volume.attachment_state() != 'attached':
|
||||||
|
time.sleep(3)
|
||||||
|
volume.update()
|
||||||
|
changed = True
|
||||||
except boto.exception.BotoServerError, e:
|
except boto.exception.BotoServerError, e:
|
||||||
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
|
||||||
|
|
||||||
def detach_volume(module, ec2):
|
return volume, changed
|
||||||
vol = get_volume(module, ec2)
|
|
||||||
if not vol or vol.attachment_state() is None:
|
def detach_volume(module, ec2, volume):
|
||||||
module.exit_json(changed=False)
|
|
||||||
else:
|
changed = False
|
||||||
vol.detach()
|
|
||||||
module.exit_json(changed=True)
|
if volume.attachment_state() is not None:
|
||||||
|
adata = volume.attach_data
|
||||||
|
volume.detach()
|
||||||
|
while volume.attachment_state() is not None:
|
||||||
|
time.sleep(3)
|
||||||
|
volume.update()
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
return volume, changed
|
||||||
|
|
||||||
|
def get_volume_info(volume, state):
|
||||||
|
|
||||||
|
# If we're just listing volumes then do nothing, else get the latest update for the volume
|
||||||
|
if state != 'list':
|
||||||
|
volume.update()
|
||||||
|
|
||||||
|
volume_info = {}
|
||||||
|
attachment = volume.attach_data
|
||||||
|
|
||||||
|
volume_info = {
|
||||||
|
'create_time': volume.create_time,
|
||||||
|
'id': volume.id,
|
||||||
|
'iops': volume.iops,
|
||||||
|
'size': volume.size,
|
||||||
|
'snapshot_id': volume.snapshot_id,
|
||||||
|
'status': volume.status,
|
||||||
|
'type': volume.type,
|
||||||
|
'zone': volume.zone,
|
||||||
|
'attachment_set': {
|
||||||
|
'attach_time': attachment.attach_time,
|
||||||
|
'device': attachment.device,
|
||||||
|
'instance_id': attachment.instance_id,
|
||||||
|
'status': attachment.status
|
||||||
|
},
|
||||||
|
'tags': volume.tags
|
||||||
|
}
|
||||||
|
|
||||||
|
return volume_info
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
argument_spec = ec2_argument_spec()
|
argument_spec = ec2_argument_spec()
|
||||||
|
@ -380,11 +404,30 @@ def main():
|
||||||
zone = module.params.get('zone')
|
zone = module.params.get('zone')
|
||||||
snapshot = module.params.get('snapshot')
|
snapshot = module.params.get('snapshot')
|
||||||
state = module.params.get('state')
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
# Ensure we have the zone or can get the zone
|
||||||
|
if instance is None and zone is None and state == 'present':
|
||||||
|
module.fail_json(msg="You must specify either instance or zone")
|
||||||
|
|
||||||
|
# Set volume detach flag
|
||||||
if instance == 'None' or instance == '':
|
if instance == 'None' or instance == '':
|
||||||
instance = None
|
instance = None
|
||||||
|
detach_vol_flag = True
|
||||||
|
else:
|
||||||
|
detach_vol_flag = False
|
||||||
|
|
||||||
|
# Set changed flag
|
||||||
|
changed = False
|
||||||
|
|
||||||
ec2 = ec2_connect(module)
|
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||||
|
|
||||||
|
if region:
|
||||||
|
try:
|
||||||
|
ec2 = connect_to_aws(boto.ec2, region, **aws_connect_params)
|
||||||
|
except (boto.exception.NoAuthHandlerFound, StandardError), e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="region must be specified")
|
||||||
|
|
||||||
if state == 'list':
|
if state == 'list':
|
||||||
returned_volumes = []
|
returned_volumes = []
|
||||||
|
@ -393,22 +436,7 @@ def main():
|
||||||
for v in vols:
|
for v in vols:
|
||||||
attachment = v.attach_data
|
attachment = v.attach_data
|
||||||
|
|
||||||
returned_volumes.append({
|
returned_volumes.append(get_volume_info(v, state))
|
||||||
'create_time': v.create_time,
|
|
||||||
'id': v.id,
|
|
||||||
'iops': v.iops,
|
|
||||||
'size': v.size,
|
|
||||||
'snapshot_id': v.snapshot_id,
|
|
||||||
'status': v.status,
|
|
||||||
'type': v.type,
|
|
||||||
'zone': v.zone,
|
|
||||||
'attachment_set': {
|
|
||||||
'attach_time': attachment.attach_time,
|
|
||||||
'device': attachment.device,
|
|
||||||
'status': attachment.status,
|
|
||||||
'deleteOnTermination': attachment.deleteOnTermination
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exit_json(changed=False, volumes=returned_volumes)
|
module.exit_json(changed=False, volumes=returned_volumes)
|
||||||
|
|
||||||
|
@ -419,8 +447,12 @@ def main():
|
||||||
# instance is specified but zone isn't.
|
# instance is specified but zone isn't.
|
||||||
# Useful for playbooks chaining instance launch with volume create + attach and where the
|
# Useful for playbooks chaining instance launch with volume create + attach and where the
|
||||||
# zone doesn't matter to the user.
|
# zone doesn't matter to the user.
|
||||||
|
inst = None
|
||||||
if instance:
|
if instance:
|
||||||
reservation = ec2.get_all_instances(instance_ids=instance)
|
try:
|
||||||
|
reservation = ec2.get_all_instances(instance_ids=instance)
|
||||||
|
except BotoServerError as e:
|
||||||
|
module.fail_json(msg=e.message)
|
||||||
inst = reservation[0].instances[0]
|
inst = reservation[0].instances[0]
|
||||||
zone = inst.placement
|
zone = inst.placement
|
||||||
|
|
||||||
|
@ -439,17 +471,19 @@ def main():
|
||||||
|
|
||||||
if volume_size and (id or snapshot):
|
if volume_size and (id or snapshot):
|
||||||
module.fail_json(msg="Cannot specify volume_size together with id or snapshot")
|
module.fail_json(msg="Cannot specify volume_size together with id or snapshot")
|
||||||
|
|
||||||
if state == 'absent':
|
|
||||||
delete_volume(module, ec2)
|
|
||||||
|
|
||||||
if state == 'present':
|
if state == 'present':
|
||||||
volume = create_volume(module, ec2, zone)
|
volume, changed = create_volume(module, ec2, zone)
|
||||||
if instance:
|
if detach_vol_flag:
|
||||||
attach_volume(module, ec2, volume, inst)
|
volume, changed = detach_volume(module, ec2, volume)
|
||||||
else:
|
elif inst is not None:
|
||||||
detach_volume(module, ec2)
|
volume, changed = attach_volume(module, ec2, volume, inst)
|
||||||
module.exit_json(volume_id=volume.id, device=device_name, volume_type=volume.type)
|
|
||||||
|
# Add device, volume_id and volume_type parameters separately to maintain backward compatability
|
||||||
|
volume_info = get_volume_info(volume, state)
|
||||||
|
module.exit_json(changed=changed, volume=volume_info, device=volume_info['attachment_set']['device'], volume_id=volume_info['id'], volume_type=volume_info['type'])
|
||||||
|
elif state == 'absent':
|
||||||
|
delete_volume(module, ec2)
|
||||||
|
|
||||||
# import module snippets
|
# import module snippets
|
||||||
from ansible.module_utils.basic import *
|
from ansible.module_utils.basic import *
|
||||||
|
|
Loading…
Reference in a new issue