EC2: implement ModifyVolumes and DescribeVolumesModifications (#5399)
This commit is contained in:
		
							parent
							
								
									52c1edce23
								
							
						
					
					
						commit
						37fa5f8bf4
					
				| @ -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 | ||||
| </details> | ||||
| </details> | ||||
|  | ||||
| @ -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: | ||||
|  | ||||
| @ -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 = """ | ||||
|     <return>true</return> | ||||
| </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>""" | ||||
|  | ||||
| @ -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") | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user