diff --git a/lib/fog/aws/compute.rb b/lib/fog/aws/compute.rb index 85830e1f7..7865bcdb4 100644 --- a/lib/fog/aws/compute.rb +++ b/lib/fog/aws/compute.rb @@ -120,6 +120,7 @@ module Fog request :describe_subnets request :describe_tags request :describe_volumes + request :describe_volumes_modifications request :describe_volume_status request :describe_vpcs request :describe_vpc_attribute @@ -140,6 +141,7 @@ module Fog request :modify_network_interface_attribute request :modify_snapshot_attribute request :modify_subnet_attribute + request :modify_volume request :modify_volume_attribute request :modify_vpc_attribute request :move_address_to_vpc @@ -290,6 +292,7 @@ module Fog } ], :spot_requests => {}, + :volume_modifications => {} } end end diff --git a/lib/fog/aws/models/compute/volume.rb b/lib/fog/aws/models/compute/volume.rb index 374746890..dec8b0147 100644 --- a/lib/fog/aws/models/compute/volume.rb +++ b/lib/fog/aws/models/compute/volume.rb @@ -36,31 +36,50 @@ module Fog state == 'available' end + def modification_in_progress? + modifications.any? { |m| m['modificationState'] != 'completed' } + end + + def modifications + requires :identity + service.describe_volumes_modifications('volume-id' => self.identity).body['volumeModificationSet'] + end + def save - raise Fog::Errors::Error.new('Resaving an existing object may create a duplicate') if persisted? - requires :availability_zone - requires_one :size, :snapshot_id + if identity + update_params = { + 'Size' => self.size, + 'Iops' => self.iops, + 'VolumeType' => self.type + } - if type == 'io1' - requires :iops + service.modify_volume(self.identity, update_params) + true + else + requires :availability_zone + requires_one :size, :snapshot_id + + if type == 'io1' + requires :iops + end + + data = service.create_volume(availability_zone, size, create_params).body + merge_attributes(data) + + if tags = self.tags + # expect eventual consistency + Fog.wait_for { self.reload rescue nil } + service.create_tags( + self.identity, + tags + ) + end + + if @server + self.server = @server + end + true end - - data = service.create_volume(availability_zone, size, create_params).body - merge_attributes(data) - - if tags = self.tags - # expect eventual consistency - Fog.wait_for { self.reload rescue nil } - service.create_tags( - self.identity, - tags - ) - end - - if @server - self.server = @server - end - true end def server diff --git a/lib/fog/aws/parsers/compute/describe_volumes_modifications.rb b/lib/fog/aws/parsers/compute/describe_volumes_modifications.rb new file mode 100644 index 000000000..70a665c23 --- /dev/null +++ b/lib/fog/aws/parsers/compute/describe_volumes_modifications.rb @@ -0,0 +1,30 @@ +module Fog + module Parsers + module Compute + module AWS + class DescribeVolumesModifications < Fog::Parsers::Base + def reset + @response = { 'volumeModificationSet' => [] } + @modification = {} + end + + def end_element(name) + case name + when 'modificationState', 'originalVolumeType', 'statusMessage', 'targetVolumeType', 'volumeId' + @modification[name] = value + when 'startTime', 'endTime' + @modification[name] = Time.parse(value) + when 'originalIops', 'originalSize', 'progress', 'targetIops', 'targetSize' + @modification[name] = value.to_i + when 'requestId' + @response[name] = value + when 'item' + @response['volumeModificationSet'] << @modification.dup + @modification = {} + end + end + end + end + end + end +end diff --git a/lib/fog/aws/parsers/compute/modify_volume.rb b/lib/fog/aws/parsers/compute/modify_volume.rb new file mode 100644 index 000000000..544e51491 --- /dev/null +++ b/lib/fog/aws/parsers/compute/modify_volume.rb @@ -0,0 +1,26 @@ +module Fog + module Parsers + module Compute + module AWS + class ModifyVolume < Fog::Parsers::Base + def reset + @response = {'volumeModification' => {}} + end + + def end_element(name) + case name + when 'modificationState', 'originalVolumeType', 'statusMessage', 'targetVolumeType', 'volumeId' + @response['volumeModification'][name] = value + when 'startTime', 'endTime' + @response['volumeModification'][name] = Time.parse(value) + when 'originalIops', 'originalSize', 'progress', 'targetIops', 'targetSize' + @response['volumeModification'][name] = value.to_i + when 'requestId' + @response[name] = value + end + end + end + end + end + end +end diff --git a/lib/fog/aws/requests/compute/describe_volumes_modifications.rb b/lib/fog/aws/requests/compute/describe_volumes_modifications.rb new file mode 100644 index 000000000..59e790a94 --- /dev/null +++ b/lib/fog/aws/requests/compute/describe_volumes_modifications.rb @@ -0,0 +1,93 @@ +module Fog + module Compute + class AWS + class Real + require 'fog/aws/parsers/compute/describe_volumes_modifications' + + # Reports the current modification status of EBS volumes. + # + # ==== Parameters + # * filters<~Hash> - List of filters to limit results with + # + # ==== Returns + # * response<~Excon::Response>: + # * body<~Hash> + # * 'volumeModificationSet'<~Array>: + # * 'targetIops'<~Integer> - Target IOPS rate of the volume being modified. + # * 'originalIops'<~Integer> - Original IOPS rate of the volume being modified. + # * 'modificationState'<~String> - Current state of modification. Modification state is null for unmodified volumes. + # * 'targetSize'<~Integer> - Target size of the volume being modified. + # * 'targetVolumeType'<~String> - Target EBS volume type of the volume being modified. + # * 'volumeId'<~String> - ID of the volume being modified. + # * 'progress'<~Integer> - Modification progress from 0 to 100%. + # * 'startTime'<~Time> - Modification start time + # * 'endTime'<~Time> - Modification end time + # * 'originalSize'<~Integer> - Original size of the volume being modified. + # * 'originalVolumeType'<~String> - Original EBS volume type of the volume being modified. + + def describe_volumes_modifications(filters = {}) + params = {} + if volume_id = filters.delete('volume-id') + params.merge!(Fog::AWS.indexed_param('VolumeId.%d', [*volume_id])) + end + params.merge!(Fog::AWS.indexed_filters(filters)) + request({ + 'Action' => 'DescribeVolumesModifications', + :idempotent => true, + :parser => Fog::Parsers::Compute::AWS::DescribeVolumesModifications.new + }.merge(params)) + end + end + + class Mock + def describe_volumes_modifications(filters = {}) + response = Excon::Response.new + + modification_set = self.data[:volume_modifications].values + + aliases = { + 'volume-id' => 'volumeId', + 'modification-state' => 'modificationState', + 'target-size' => 'targetSize', + 'target-iops' => 'targetIops', + 'target-volume-type' => 'targetVolumeType', + 'original-size' => 'originalSize', + 'original-iops' => 'originalIops', + 'original-volume-type' => 'originalVolumeType', + 'start-time' => 'startTime' + } + + attribute_aliases = { + 'targetSize' => 'size', + 'targetVolumeType' => 'volumeType', + 'targetIops' => 'iops' + } + + for filter_key, filter_value in filters + aliased_key = aliases[filter_key] + modification_set = modification_set.reject { |m| ![*filter_value].include?(m[aliased_key]) } + end + + modification_set.each do |modification| + case modification['modificationState'] + when 'modifying' + volume = self.data[:volumes][modification['volumeId']] + modification['modificationState'] = 'optimizing' + %w(targetSize targetIops targetVolumeType).each do |attribute| + aliased_attribute = attribute_aliases[attribute] + volume[aliased_attribute] = modification[attribute] if modification[attribute] + end + self.data[:volumes][modification['volumeId']] = volume + when 'optimizing' + modification['modificationState'] = 'completed' + modification['endTime'] = Time.now + end + end + + response.body = {'requestId' => Fog::AWS::Mock.request_id, 'volumeModificationSet' => modification_set} + response + end + end + end + end +end diff --git a/lib/fog/aws/requests/compute/modify_volume.rb b/lib/fog/aws/requests/compute/modify_volume.rb new file mode 100644 index 000000000..72f211ace --- /dev/null +++ b/lib/fog/aws/requests/compute/modify_volume.rb @@ -0,0 +1,88 @@ +module Fog + module Compute + class AWS + class Real + require 'fog/aws/parsers/compute/modify_volume' + + # Modifies a volume + # + # ==== Parameters + # * volume_id<~String> - The ID of the volume + # * options<~Hash>: + # * 'VolumeType'<~String> - Type of volume + # * 'Size'<~Integer> - Size in GiBs fo the volume + # * 'Iops'<~Integer> - Number of IOPS the volume supports + # + # ==== Response + # * response<~Excon::Response>: + # * body<~Hash>: + # * 'targetIops'<~Integer> - Target IOPS rate of the volume being modified. + # * 'originalIops'<~Integer> - Original IOPS rate of the volume being modified. + # * 'modificationState'<~String> - Current state of modification. Modification state is null for unmodified volumes. + # * 'targetSize'<~Integer> - Target size of the volume being modified. + # * 'targetVolumeType'<~String> - Target EBS volume type of the volume being modified. + # * 'volumeId'<~String> - ID of the volume being modified. + # * 'progress'<~Integer> - Modification progress from 0 to 100%. + # * 'startTime'<~Time> - Modification start time + # * 'endTime'<~Time> - Modification end time + # * 'originalSize'<~Integer> - Original size of the volume being modified. + # * 'originalVolumeType'<~String> - Original EBS volume type of the volume being modified. + + def modify_volume(volume_id, options={}) + request({ + 'Action' => "ModifyVolume", + 'VolumeId' => volume_id, + :parser => Fog::Parsers::Compute::AWS::ModifyVolume.new + }.merge(options)) + end + end + + class Mock + def modify_volume(volume_id, options={}) + response = Excon::Response.new + volume = self.data[:volumes][volume_id] + + if volume["volumeType"] == 'standard' && options['VolumeType'] + raise Fog::Compute::AWS::Error.new("InvalidParameterValue => Volume type EBS Magnetic is not supported.") + end + + volume_modification = { + 'modificationState' => 'modifying', + 'progress' => 0, + 'startTime' => Time.now, + 'volumeId' => volume_id + } + + if options['Size'] + volume_modification.merge!( + 'originalSize' => volume['size'], + 'targetSize' => options['Size'] + ) + end + + if options['Iops'] + volume_modification.merge!( + 'originalIops' => volume['iops'], + 'targetIops' => options['Iops'] + ) + end + + if options['VolumeType'] + if options["VolumeType"] == 'standard' + raise Fog::Compute::AWS::Error.new("InvalidParameterValue => Volume type EBS Magnetic is not supported.") + end + volume_modification.merge!( + 'originalVolumeType' => volume['volumeType'], + 'targetVolumeType' => options['VolumeType'] + ) + end + + self.data[:volume_modifications][volume_id] = volume_modification + + response.body = {'volumeModification' => volume_modification, 'requestId' => Fog::AWS::Mock.request_id} + response + end + end + end + end +end diff --git a/tests/models/compute/volume_tests.rb b/tests/models/compute/volume_tests.rb index fd902c7fb..3445de664 100644 --- a/tests/models/compute/volume_tests.rb +++ b/tests/models/compute/volume_tests.rb @@ -3,7 +3,7 @@ Shindo.tests("Fog::Compute[:aws] | volume", ['aws']) do @server = Fog::Compute[:aws].servers.create @server.wait_for { ready? } - model_tests(Fog::Compute[:aws].volumes, {:availability_zone => @server.availability_zone, :size => 1, :device => '/dev/sdz1', :tags => {"key" => "value"}}, true) do + model_tests(Fog::Compute[:aws].volumes, {:availability_zone => @server.availability_zone, :size => 1, :device => '/dev/sdz1', :tags => {"key" => "value"}, :type => 'gp2'}, true) do @instance.wait_for { ready? } @@ -32,6 +32,21 @@ Shindo.tests("Fog::Compute[:aws] | volume", ['aws']) do @instance.wait_for { ready? } + @instance.type = 'io1' + @instance.iops = 5000 + @instance.size = 100 + @instance.save + + returns(true) { @instance.modification_in_progress? } + @instance.wait_for { !modification_in_progress? } + + # avoid weirdness with merge_attributes + @instance = Fog::Compute[:aws].volumes.get(@instance.identity) + + returns('io1') { @instance.type } + returns(5000) { @instance.iops } + returns(100) { @instance.size } + tests('@instance.reload.tags').returns({'key' => 'value'}) do @instance.reload.tags end diff --git a/tests/requests/compute/volume_tests.rb b/tests/requests/compute/volume_tests.rb index 9302dd9ca..73d0f6757 100644 --- a/tests/requests/compute/volume_tests.rb +++ b/tests/requests/compute/volume_tests.rb @@ -68,6 +68,29 @@ Shindo.tests('Fog::Compute[:aws] | volume requests', ['aws']) do 'requestId' => String } + @volume_modification_format = { + 'endTime' => Fog::Nullable::Time, + 'modificationState' => String, + 'originalIops' => Fog::Nullable::Integer, + 'originalSize' => Fog::Nullable::Integer, + 'originalVolumeType' => Fog::Nullable::String, + 'startTime' => Time, + 'targetIops' => Fog::Nullable::Integer, + 'targetSize' => Fog::Nullable::Integer, + 'targetVolumeType' => Fog::Nullable::String, + 'volumeId' => String, + } + + @modify_volume_format = { + 'requestId' => String, + 'volumeModification' => @volume_modification_format + } + + @describe_volume_modifications_format = { + 'requestId' => String, + 'volumeModificationSet' => [@volume_modification_format] + } + @server = Fog::Compute[:aws].servers.create @server.wait_for { ready? } @@ -113,13 +136,13 @@ Shindo.tests('Fog::Compute[:aws] | volume requests', ['aws']) do Fog::Compute[:aws].delete_volume(@volume_id) tests('#create_volume from snapshot with size').formats(@volume_format) do - volume = Fog::Compute[:aws].volumes.create(:availability_zone => 'us-east-1d', :size => 1) + volume = Fog::Compute[:aws].volumes.create(:availability_zone => 'us-east-1d', :size => 1, :type => 'gp2') volume.wait_for { ready? } snapshot = Fog::Compute[:aws].create_snapshot(volume.identity).body Fog::Compute[:aws].snapshots.new(snapshot).wait_for { ready? } - data = Fog::Compute[:aws].create_volume(@server.availability_zone, 1, 'SnapshotId' => snapshot['snapshotId']).body + data = Fog::Compute[:aws].create_volume(@server.availability_zone, 1, 'SnapshotId' => snapshot['snapshotId'], 'VolumeType' => 'gp2').body @volume_id = data['volumeId'] data end @@ -155,6 +178,23 @@ Shindo.tests('Fog::Compute[:aws] | volume requests', ['aws']) do Fog::Compute[:aws].volumes.get(@volume_id).wait_for { ready? } + tests("#modify_volume('#{@volume_id}', 'Size' => 100, 'VolumeType' => 'io1', 'Iops' => 5000").formats(@modify_volume_format) do + Fog::Compute[:aws].modify_volume(@volume_id, 'Size' => 100, 'VolumeType' => 'io1', 'Iops' => 5000).body + end + + tests("#describe_volumes_modifications('volume-id' => '#{@volume_id}')").formats(@describe_volume_modifications_format) do + Fog.wait_for do + Fog::Compute[:aws].describe_volumes_modifications('volume-id' => @volume_id).body['volumeModificationSet'].first['modificationState'] == 'completed' + end + + volume = Fog::Compute[:aws].describe_volumes('volume-id' => @volume_id).body['volumeSet'].first + returns(100) { volume['size'] } + returns('io1') { volume['volumeType'] } + returns(5000) { volume['iops'] } + + Fog::Compute[:aws].describe_volumes_modifications('volume-id' => @volume_id).body + end + tests("#modify_volume_attribute('#{@volume_id}', true)").formats(AWS::Compute::Formats::BASIC) do Fog::Compute[:aws].modify_volume_attribute(@volume_id, true).body end