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:
Rob 2015-10-01 13:23:41 +10:00 committed by wimnat
parent f35ab5faca
commit 3360cef024

View file

@ -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 *