diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 60e9bb278..e19c4f95a 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -1856,7 +1856,7 @@
- [ ] describe_volume_attribute
- [ ] describe_volume_status
- [X] describe_volumes
-- [ ] describe_volumes_modifications
+- [X] describe_volumes_modifications
- [X] describe_vpc_attribute
- [ ] describe_vpc_classic_link
- [ ] describe_vpc_classic_link_dns_support
@@ -1998,7 +1998,7 @@
- [X] modify_transit_gateway
- [ ] modify_transit_gateway_prefix_list_reference
- [X] modify_transit_gateway_vpc_attachment
-- [ ] modify_volume
+- [X] modify_volume
- [ ] modify_volume_attribute
- [X] modify_vpc_attribute
- [ ] modify_vpc_endpoint
@@ -6418,4 +6418,4 @@
- workspaces
- workspaces-web
- xray
-
\ No newline at end of file
+
diff --git a/moto/ec2/models/elastic_block_store.py b/moto/ec2/models/elastic_block_store.py
index c45702f10..44833edf1 100644
--- a/moto/ec2/models/elastic_block_store.py
+++ b/moto/ec2/models/elastic_block_store.py
@@ -9,6 +9,7 @@ from ..exceptions import (
InvalidVolumeAttachmentError,
InvalidVolumeDetachmentError,
InvalidParameterDependency,
+ InvalidParameterValueError,
)
from .core import TaggedEC2Resource
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):
def __init__(self, volume, instance, device, status):
self.volume = volume
@@ -78,6 +108,16 @@ class Volume(TaggedEC2Resource, CloudFormationModel):
self.ec2_backend = ec2_backend
self.encrypted = encrypted
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
def cloudformation_name_type():
@@ -237,6 +277,20 @@ class EBSBackend:
matches = generic_filter(filters, 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):
volume = self.volumes.get(volume_id, None)
if not volume:
diff --git a/moto/ec2/responses/elastic_block_store.py b/moto/ec2/responses/elastic_block_store.py
index 5a88a2a85..d99b8eed0 100644
--- a/moto/ec2/responses/elastic_block_store.py
+++ b/moto/ec2/responses/elastic_block_store.py
@@ -74,6 +74,27 @@ class ElasticBlockStore(EC2BaseResponse):
template = self.response_template(CREATE_VOLUME_RESPONSE)
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):
snapshot_id = self._get_param("SnapshotId")
if self.is_not_dryrun("DeleteSnapshot"):
@@ -402,3 +423,40 @@ MODIFY_SNAPSHOT_ATTRIBUTE_RESPONSE = """
true
"""
+
+MODIFY_VOLUME_RESPONSE = """
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ {% set volume_modification = volume.modifications[-1] %}
+ modifying
+ {{ volume_modification.original_size }}
+ {{ volume_modification.original_volume_type }}
+
+ {{ volume_modification.start_time }}
+ {{ volume_modification.target_size }}
+ {{ volume_modification.target_volume_type }}
+ {{ volume.id }}
+
+"""
+
+DESCRIBE_VOLUMES_MODIFICATIONS_RESPONSE = """
+
+
+ 59dbff89-35bd-4eac-99ed-be587EXAMPLE
+
+ {% for modification in modifications %}
+ -
+ {{ modification.end_time }}
+ completed
+ {{ modification.original_size }}
+ {{ modification.original_volume_type }}
+
+ {{ modification.start_time }}
+ {{ modification.target_size }}
+ {{ modification.target_volume_type }}
+ {{ modification.volume.id }}
+
+ {% endfor %}
+
+"""
diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py
index 9ce9d226b..e80d0bb62 100644
--- a/tests/test_ec2/test_elastic_block_store.py
+++ b/tests/test_ec2/test_elastic_block_store.py
@@ -46,6 +46,42 @@ def test_create_and_delete_volume():
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
def test_delete_attached_volume():
client = boto3.client("ec2", region_name="us-east-1")