Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ec2_vol support MultiAttach disk #365

Merged
merged 12 commits into from
Jun 24, 2021
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
minor_changes:
- ec2_vol - add parameter ``multi_attach`` to support Multi-Attach on volume creation/update (https://github.com/ansible-collections/amazon.aws/pull/362).
breaking_changes:
- ec2_vol_info - return ``attachment_set`` is now a list of attachments with Multi-Attach support on disk. (https://github.com/ansible-collections/amazon.aws/pull/362).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change and needs to be documented as such. Next release is a major release so I think we can get away with this (? @jillr ?)

91 changes: 62 additions & 29 deletions plugins/modules/ec2_vol.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@
- Requires at least botocore version 1.19.27.
type: int
version_added: 1.4.0
multi_attach:
abikouo marked this conversation as resolved.
Show resolved Hide resolved
description:
- If set to C(yes), Multi-Attach will be enabled when creating the volume.
- When you create a new volume, Multi-Attach is disabled by default.
- This parameter is supported with io1 and io2 volumes only.
type: bool
version_added: 2.0.0
author: "Lester Wade (@lwade)"
extends_documentation_fragment:
- amazon.aws.aws
Expand Down Expand Up @@ -189,6 +196,14 @@
volume_type: gp2
device_name: /dev/xvdf

# Create new volume with multi-attach enabled
- amazon.aws.ec2_vol:
zone: XXXXXX
multi_attach: true
volume_size: 4
volume_type: io1
iops: 102

# Attach an existing volume to instance. The volume will be deleted upon instance termination.
- amazon.aws.ec2_vol:
instance: XXXXXX
Expand Down Expand Up @@ -218,13 +233,13 @@
returned: when success
type: str
sample: {
"attachment_set": {
"attachment_set": [{
"attach_time": "2015-10-23T00:22:29.000Z",
"deleteOnTermination": "false",
"device": "/dev/sdf",
"instance_id": "i-8356263c",
"status": "attached"
},
}],
"create_time": "2015-10-21T14:36:08.870Z",
"encrypted": false,
"id": "vol-35b333d9",
Expand Down Expand Up @@ -408,14 +423,23 @@ def update_volume(module, ec2_conn, volume):
throughput_changed = True
req_obj['Throughput'] = target_throughput

changed = iops_changed or size_changed or type_changed or throughput_changed
target_multi_attach = module.params.get('multi_attach')
multi_attach_changed = False
if target_multi_attach is not None:
original_multi_attach = volume['multi_attach_enabled']
if target_multi_attach != original_multi_attach:
multi_attach_changed = True
req_obj['MultiAttachEnabled'] = target_multi_attach
abikouo marked this conversation as resolved.
Show resolved Hide resolved

changed = iops_changed or size_changed or type_changed or throughput_changed or multi_attach_changed

if changed:
response = ec2_conn.modify_volume(**req_obj)

volume['size'] = response.get('VolumeModification').get('TargetSize')
volume['volume_type'] = response.get('VolumeModification').get('TargetVolumeType')
volume['iops'] = response.get('VolumeModification').get('TargetIops')
volume['multi_attach_enabled'] = response.get('VolumeModification').get('TargetMultiAttachEnabled')
if module.botocore_at_least("1.19.27"):
volume['throughput'] = response.get('VolumeModification').get('TargetThroughput')

Expand All @@ -431,6 +455,7 @@ def create_volume(module, ec2_conn, zone):
volume_type = module.params.get('volume_type')
snapshot = module.params.get('snapshot')
throughput = module.params.get('throughput')
multi_attach = module.params.get('multi_attach')

volume = get_volume(module, ec2_conn)

Expand Down Expand Up @@ -458,6 +483,8 @@ def create_volume(module, ec2_conn, zone):

if throughput:
additional_params['Throughput'] = int(throughput)
if multi_attach:
additional_params['MultiAttachEnabled'] = True

create_vol_response = ec2_conn.create_volume(
aws_retry=True,
Expand Down Expand Up @@ -489,11 +516,13 @@ def attach_volume(module, ec2_conn, volume_dict, instance_dict, device_name):

attachment_data = get_attachment_data(volume_dict, wanted_state='attached')
if attachment_data:
if attachment_data.get('instance_id', None) != instance_dict['instance_id']:
module.fail_json(msg="Volume {0} is already attached to another instance: {1}".format(volume_dict['volume_id'],
attachment_data.get('instance_id', None)))
else:
return volume_dict, changed
if not volume_dict['multi_attach_enabled']:
# volumes without MultiAttach Enabled can be attached to 1 instance only
if attachment_data[0].get('instance_id', None) != instance_dict['instance_id']:
module.fail_json(msg="Volume {0} is already attached to another instance: {1}".format(volume_dict['volume_id'],
attachment_data[0].get('instance_id', None)))
else:
return volume_dict, changed

try:
attach_response = ec2_conn.attach_volume(aws_retry=True, Device=device_name,
Expand Down Expand Up @@ -557,17 +586,22 @@ def modify_dot_attribute(module, ec2_conn, instance_dict, device_name):
def get_attachment_data(volume_dict, wanted_state=None):
changed = False

attachment_data = {}
attachment_data = []
if not volume_dict:
return attachment_data
for data in volume_dict.get('attachments', []):
if wanted_state and wanted_state == data['state']:
attachment_data = data
break
else:
# No filter, return first
attachment_data = data
break
resource = volume_dict.get('attachments', [])
if wanted_state:
# filter 'state', return attachment matching wanted state
resource = [data for data in resource if data['state'] == wanted_state]

for data in resource:
attachment_data.append({
'attach_time': data.get('attach_time', None),
'device': data.get('device', None),
'instance_id': data.get('instance_id', None),
'status': data.get('state', None),
'delete_on_termination': data.get('delete_on_termination', None)
})

return attachment_data

Expand All @@ -576,8 +610,9 @@ def detach_volume(module, ec2_conn, volume_dict):
changed = False

attachment_data = get_attachment_data(volume_dict, wanted_state='attached')
if attachment_data:
ec2_conn.detach_volume(aws_retry=True, VolumeId=volume_dict['volume_id'])
# The ID of the instance must be specified if you are detaching a Multi-Attach enabled volume.
for attachment in attachment_data:
ec2_conn.detach_volume(aws_retry=True, InstanceId=attachment['instance_id'], VolumeId=volume_dict['volume_id'])
waiter = ec2_conn.get_waiter('volume_available')
waiter.wait(
VolumeIds=[volume_dict['volume_id']],
Expand All @@ -602,13 +637,8 @@ def get_volume_info(module, volume, tags=None):
'status': volume.get('state'),
'type': volume.get('volume_type'),
'zone': volume.get('availability_zone'),
'attachment_set': {
'attach_time': attachment_data.get('attach_time', None),
'device': attachment_data.get('device', None),
'instance_id': attachment_data.get('instance_id', None),
'status': attachment_data.get('state', None),
'deleteOnTermination': attachment_data.get('delete_on_termination', None)
},
'attachment_set': attachment_data,
'multi_attach_enabled': volume.get('multi_attach_enabled'),
'tags': tags
}

Expand Down Expand Up @@ -659,6 +689,7 @@ def main():
modify_volume=dict(default=False, type='bool'),
throughput=dict(type='int'),
purge_tags=dict(type='bool', default=False),
multi_attach=dict(type='bool'),
)

module = AnsibleAWSModule(
Expand All @@ -681,6 +712,7 @@ def main():
iops = module.params.get('iops')
volume_type = module.params.get('volume_type')
throughput = module.params.get('throughput')
multi_attach = module.params.get('multi_attach')

if state == 'list':
module.deprecate(
Expand Down Expand Up @@ -717,6 +749,9 @@ def main():
if throughput < 125 or throughput > 1000:
module.fail_json(msg='Throughput values must be between 125 and 1000.')

if multi_attach is True and volume_type not in ('io1', 'io2'):
module.fail_json(msg='multi_attach is only supported for io1 and io2 volumes.')

# Set changed flag
changed = False

Expand Down Expand Up @@ -777,8 +812,6 @@ def main():
changed=False
)

attach_state_changed = False

if volume:
volume, changed = update_volume(module, ec2_conn, volume)
else:
Expand All @@ -799,7 +832,7 @@ def main():
if tags_changed:
changed = True

module.exit_json(changed=changed, volume=volume_info, device=volume_info['attachment_set']['device'],
module.exit_json(changed=changed, volume=volume_info, device=device_name,
volume_id=volume_info['id'], volume_type=volume_info['type'])
elif state == 'absent':
if not name and not param_id:
Expand Down
25 changes: 18 additions & 7 deletions plugins/modules/ec2_vol_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@
description: The Availability Zone of the volume.
type: str
sample: "us-east-1b"
throughput:
description: The throughput that the volume supports, in MiB/s.
type: int
sample: 131
'''

try:
Expand All @@ -128,6 +132,16 @@ def get_volume_info(volume, region):

attachment = volume["attachments"]

attachment_data = []
for data in volume["attachments"]:
attachment_data.append({
'attach_time': data.get('attach_time', None),
'device': data.get('device', None),
'instance_id': data.get('instance_id', None),
'status': data.get('state', None),
'delete_on_termination': data.get('delete_on_termination', None)
})

volume_info = {
'create_time': volume["create_time"],
'id': volume["volume_id"],
Expand All @@ -139,16 +153,13 @@ def get_volume_info(volume, region):
'type': volume["volume_type"],
'zone': volume["availability_zone"],
'region': region,
'attachment_set': {
'attach_time': attachment[0]["attach_time"] if len(attachment) > 0 else None,
'device': attachment[0]["device"] if len(attachment) > 0 else None,
'instance_id': attachment[0]["instance_id"] if len(attachment) > 0 else None,
'status': attachment[0]["state"] if len(attachment) > 0 else None,
'delete_on_termination': attachment[0]["delete_on_termination"] if len(attachment) > 0 else None
},
'attachment_set': attachment_data,
'tags': boto3_tag_list_to_ansible_dict(volume['tags']) if "tags" in volume else None
}

if 'throughput' in volume:
volume_info['throughput'] = volume["throughput"]

return volume_info


Expand Down
Loading