EC2: implement ModifyVolumes and DescribeVolumesModifications (#5399)

This commit is contained in:
Viren Nadkarni 2022-08-24 14:53:21 +05:30 committed by GitHub
parent 52c1edce23
commit 37fa5f8bf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 151 additions and 3 deletions

View File

@ -1856,7 +1856,7 @@
- [ ] describe_volume_attribute - [ ] describe_volume_attribute
- [ ] describe_volume_status - [ ] describe_volume_status
- [X] describe_volumes - [X] describe_volumes
- [ ] describe_volumes_modifications - [X] describe_volumes_modifications
- [X] describe_vpc_attribute - [X] describe_vpc_attribute
- [ ] describe_vpc_classic_link - [ ] describe_vpc_classic_link
- [ ] describe_vpc_classic_link_dns_support - [ ] describe_vpc_classic_link_dns_support
@ -1998,7 +1998,7 @@
- [X] modify_transit_gateway - [X] modify_transit_gateway
- [ ] modify_transit_gateway_prefix_list_reference - [ ] modify_transit_gateway_prefix_list_reference
- [X] modify_transit_gateway_vpc_attachment - [X] modify_transit_gateway_vpc_attachment
- [ ] modify_volume - [X] modify_volume
- [ ] modify_volume_attribute - [ ] modify_volume_attribute
- [X] modify_vpc_attribute - [X] modify_vpc_attribute
- [ ] modify_vpc_endpoint - [ ] modify_vpc_endpoint
@ -6418,4 +6418,4 @@
- workspaces - workspaces
- workspaces-web - workspaces-web
- xray - xray
</details> </details>

View File

@ -9,6 +9,7 @@ from ..exceptions import (
InvalidVolumeAttachmentError, InvalidVolumeAttachmentError,
InvalidVolumeDetachmentError, InvalidVolumeDetachmentError,
InvalidParameterDependency, InvalidParameterDependency,
InvalidParameterValueError,
) )
from .core import TaggedEC2Resource from .core import TaggedEC2Resource
from ..utils import ( from ..utils import (
@ -19,6 +20,35 @@ from ..utils import (
) )
class VolumeModification(object):
def __init__(self, volume, target_size=None, target_volume_type=None):
if not any([target_size, target_volume_type]):
raise InvalidParameterValueError(
"Invalid input: Must specify at least one of size or type"
)
self.volume = volume
self.original_size = volume.size
self.original_volume_type = volume.volume_type
self.target_size = target_size or volume.size
self.target_volume_type = target_volume_type or volume.volume_type
self.start_time = utc_date_and_time()
self.end_time = utc_date_and_time()
def get_filter_value(self, filter_name):
if filter_name == "original-size":
return self.original_size
elif filter_name == "original-volume-type":
return self.original_volume_type
elif filter_name == "target-size":
return self.target_size
elif filter_name == "target-volume-type":
return self.target_volume_type
elif filter_name == "volume-id":
return self.volume.id
class VolumeAttachment(CloudFormationModel): class VolumeAttachment(CloudFormationModel):
def __init__(self, volume, instance, device, status): def __init__(self, volume, instance, device, status):
self.volume = volume self.volume = volume
@ -78,6 +108,16 @@ class Volume(TaggedEC2Resource, CloudFormationModel):
self.ec2_backend = ec2_backend self.ec2_backend = ec2_backend
self.encrypted = encrypted self.encrypted = encrypted
self.kms_key_id = kms_key_id self.kms_key_id = kms_key_id
self.modifications = []
def modify(self, target_size=None, target_volume_type=None):
modification = VolumeModification(
volume=self, target_size=target_size, target_volume_type=target_volume_type
)
self.modifications.append(modification)
self.size = modification.target_size
self.volume_type = modification.target_volume_type
@staticmethod @staticmethod
def cloudformation_name_type(): def cloudformation_name_type():
@ -237,6 +277,20 @@ class EBSBackend:
matches = generic_filter(filters, matches) matches = generic_filter(filters, matches)
return matches return matches
def modify_volume(self, volume_id, target_size=None, target_volume_type=None):
volume = self.get_volume(volume_id)
volume.modify(target_size=target_size, target_volume_type=target_volume_type)
return volume
def describe_volumes_modifications(self, volume_ids=None, filters=None):
volumes = self.describe_volumes(volume_ids)
modifications = []
for volume in volumes:
modifications.extend(volume.modifications)
if filters:
modifications = generic_filter(filters, modifications)
return modifications
def get_volume(self, volume_id): def get_volume(self, volume_id):
volume = self.volumes.get(volume_id, None) volume = self.volumes.get(volume_id, None)
if not volume: if not volume:

View File

@ -74,6 +74,27 @@ class ElasticBlockStore(EC2BaseResponse):
template = self.response_template(CREATE_VOLUME_RESPONSE) template = self.response_template(CREATE_VOLUME_RESPONSE)
return template.render(volume=volume) return template.render(volume=volume)
def modify_volume(self):
volume_id = self._get_param("VolumeId")
target_size = self._get_param("Size")
target_volume_type = self._get_param("VolumeType")
if self.is_not_dryrun("ModifyVolume"):
volume = self.ec2_backend.modify_volume(
volume_id, target_size, target_volume_type
)
template = self.response_template(MODIFY_VOLUME_RESPONSE)
return template.render(volume=volume)
def describe_volumes_modifications(self):
filters = self._filters_from_querystring()
volume_ids = self._get_multi_param("VolumeId")
modifications = self.ec2_backend.describe_volumes_modifications(
volume_ids=volume_ids, filters=filters
)
template = self.response_template(DESCRIBE_VOLUMES_MODIFICATIONS_RESPONSE)
return template.render(modifications=modifications)
def delete_snapshot(self): def delete_snapshot(self):
snapshot_id = self._get_param("SnapshotId") snapshot_id = self._get_param("SnapshotId")
if self.is_not_dryrun("DeleteSnapshot"): if self.is_not_dryrun("DeleteSnapshot"):
@ -402,3 +423,40 @@ MODIFY_SNAPSHOT_ATTRIBUTE_RESPONSE = """
<return>true</return> <return>true</return>
</ModifySnapshotAttributeResponse> </ModifySnapshotAttributeResponse>
""" """
MODIFY_VOLUME_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
<ModifyVolumeResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<volumeModification>
{% set volume_modification = volume.modifications[-1] %}
<modificationState>modifying</modificationState>
<originalSize>{{ volume_modification.original_size }}</originalSize>
<originalVolumeType>{{ volume_modification.original_volume_type }}</originalVolumeType>
<progress>0</progress>
<startTime>{{ volume_modification.start_time }}</startTime>
<targetSize>{{ volume_modification.target_size }}</targetSize>
<targetVolumeType>{{ volume_modification.target_volume_type }}</targetVolumeType>
<volumeId>{{ volume.id }}</volumeId>
</volumeModification>
</ModifyVolumeResponse>"""
DESCRIBE_VOLUMES_MODIFICATIONS_RESPONSE = """
<?xml version="1.0" encoding="UTF-8"?>
<DescribeVolumesModificationsResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<volumeModificationSet>
{% for modification in modifications %}
<item>
<endTime>{{ modification.end_time }}</endTime>
<modificationState>completed</modificationState>
<originalSize>{{ modification.original_size }}</originalSize>
<originalVolumeType>{{ modification.original_volume_type }}</originalVolumeType>
<progress>100</progress>
<startTime>{{ modification.start_time }}</startTime>
<targetSize>{{ modification.target_size }}</targetSize>
<targetVolumeType>{{ modification.target_volume_type }}</targetVolumeType>
<volumeId>{{ modification.volume.id }}</volumeId>
</item>
{% endfor %}
</volumeModificationSet>
</DescribeVolumesModificationsResponse>"""

View File

@ -46,6 +46,42 @@ def test_create_and_delete_volume():
ex.value.response["Error"]["Code"].should.equal("InvalidVolume.NotFound") ex.value.response["Error"]["Code"].should.equal("InvalidVolume.NotFound")
@mock_ec2
def test_modify_volumes():
client = boto3.client("ec2", region_name="us-east-1")
ec2 = boto3.resource("ec2", region_name="us-east-1")
old_size = 80
new_size = 160
new_type = "io2"
volume_id = ec2.create_volume(Size=old_size, AvailabilityZone="us-east-1a").id
# Ensure no modification records exist
modifications = client.describe_volumes_modifications()
modifications["VolumesModifications"].should.have.length_of(0)
# Ensure volume size can be modified
response = client.modify_volume(VolumeId=volume_id, Size=new_size)
response["VolumeModification"]["OriginalSize"].should.equal(old_size)
response["VolumeModification"]["TargetSize"].should.equal(new_size)
client.describe_volumes(VolumeIds=[volume_id])["Volumes"][0]["Size"].should.equal(
new_size
)
# Ensure volume type can be modified
response = client.modify_volume(VolumeId=volume_id, VolumeType=new_type)
response["VolumeModification"]["OriginalVolumeType"].should.equal("gp2")
response["VolumeModification"]["TargetVolumeType"].should.equal(new_type)
client.describe_volumes(VolumeIds=[volume_id])["Volumes"][0][
"VolumeType"
].should.equal(new_type)
# Ensure volume modifications are tracked
modifications = client.describe_volumes_modifications()
modifications["VolumesModifications"].should.have.length_of(2)
@mock_ec2 @mock_ec2
def test_delete_attached_volume(): def test_delete_attached_volume():
client = boto3.client("ec2", region_name="us-east-1") client = boto3.client("ec2", region_name="us-east-1")