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

restore db instance fom db snapshot #217

Merged
merged 5 commits into from
Jan 25, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions fog-aws.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ Gem::Specification.new do |spec|
spec.add_dependency 'fog-json', '~> 1.0'
spec.add_dependency 'fog-xml', '~> 0.1'
spec.add_dependency 'ipaddress', '~> 0.8'
spec.add_dependency 'mime-types'
Copy link
Member

Choose a reason for hiding this comment

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

My preference is to keep this out of the fog/fog-aws gemspec (since different versions are needed depending on which ruby you are using). We can have a guarded/warning if you try to do something that needs it without having it loaded yet.

end
12 changes: 12 additions & 0 deletions lib/fog/aws/dns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ module DNS
class AWS < Fog::Service
extend Fog::AWS::CredentialFetcher::ServiceMethods

require 'fog/aws/requests/dns/change_resource_record_sets'
Copy link
Member

Choose a reason for hiding this comment

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

These should be redundant to the request calls that come a bit later (though a couple new ones should be added). By using request the requires are deferred, however, until initialization time. So, I think we just want the request versions and not these explicit requires.

require 'fog/aws/requests/dns/create_health_check'
require 'fog/aws/requests/dns/create_hosted_zone'
require 'fog/aws/requests/dns/delete_health_check'
require 'fog/aws/requests/dns/delete_hosted_zone'
require 'fog/aws/requests/dns/get_change'
require 'fog/aws/requests/dns/get_health_check'
require 'fog/aws/requests/dns/get_hosted_zone'
require 'fog/aws/requests/dns/list_health_checks'
require 'fog/aws/requests/dns/list_hosted_zones'
require 'fog/aws/requests/dns/list_resource_record_sets'

requires :aws_access_key_id, :aws_secret_access_key
recognizes :host, :path, :port, :scheme, :version, :persistent, :use_iam_profile, :aws_session_token, :aws_credentials_expire_at, :instrumentor, :instrumentor_name

Expand Down
28 changes: 17 additions & 11 deletions lib/fog/aws/models/rds/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class Server < Fog::Model
attribute :tde_credential_arn, :aliases => 'TdeCredentialArn'
attribute :vpc_security_groups, :aliases => 'VpcSecurityGroups', :type => :array

attr_accessor :password, :parameter_group_name, :security_group_names, :port
attr_accessor :password, :parameter_group_name, :security_group_names, :port, :source_snapshot_id

def create_read_replica(replica_id, options={})
options[:security_group_names] ||= options['DBSecurityGroups']
Expand Down Expand Up @@ -104,16 +104,22 @@ def modify(immediately, options)
end

def save
requires :engine
requires :allocated_storage
requires :master_username
requires :password

self.flavor_id ||= 'db.m1.small'

data = service.create_db_instance(id, attributes_to_params)
merge_attributes(data.body['CreateDBInstanceResult']['DBInstance'])
true
if source_snapshot_id
Copy link
Member

Choose a reason for hiding this comment

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

Since this is pretty different, maybe we should just have a separate restore function (instead of having save pull double duty). I suspect that might make it clearer and less error prone for users. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Calling it restore is a probably a bit of a gaff on amazon's part, as it creates an entirely new server. There is no true "restore existing" functionality, you are just expected to create replacements.

The behavior of this save method therefore is consistent with Volume#save / Volume.create (optionally taking a snapshot_id).

see: http://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_RestoreDBInstanceFromDBSnapshot.html

Copy link
Member

Choose a reason for hiding this comment

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

Ah, yeah, that makes sense. Thanks for clarifying.

requires :id
data = service.restore_db_instance_from_db_snapshot(source_snapshot_id, id, attributes_to_params)
merge_attributes(data.body['RestoreDBInstanceFromDBSnapshotResult']['DBInstance'])
else
requires :engine
requires :allocated_storage
requires :master_username
requires :password

self.flavor_id ||= 'db.m1.small'

data = service.create_db_instance(id, attributes_to_params)
merge_attributes(data.body['CreateDBInstanceResult']['DBInstance'])
true
end
end

# Converts attributes to a parameter hash suitable for requests
Expand Down
4 changes: 4 additions & 0 deletions lib/fog/aws/models/rds/servers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ def get(identity)
rescue Fog::AWS::RDS::NotFound
nil
end

def restore(options)
create(options)
end
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/fog/aws/models/rds/snapshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Snapshot < Fog::Model
attribute :state, :aliases => 'Status'
attribute :port, :aliases => 'Port', :type => :integer
attribute :allocated_storage, :aliases => 'AllocatedStorage', :type => :integer
attribute :storage_type, :aliases => 'StorageType', :type => :string
attribute :iops, :aliases => 'Iops', :type => :integer
attribute :availability_zone, :aliases => 'AvailabilityZone'
attribute :type, :aliases => 'SnapshotType'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module RDS

class RestoreDBInstanceFromDBSnapshot < Fog::Parsers::AWS::RDS::DbParser
def reset
@response = { 'RestoreDBInstanceFromDBSnapshot' => {}, 'ResponseMetadata' => {} }
@response = { 'RestoreDBInstanceFromDBSnapshotResult' => {}, 'ResponseMetadata' => {} }
super
end

Expand All @@ -17,7 +17,7 @@ def start_element(name, attrs = [])
def end_element(name)
case name
when 'DBInstance'
@response['RestoreDBInstanceFromDBSnapshot']['DBInstance'] = @db_instance
@response['RestoreDBInstanceFromDBSnapshotResult']['DBInstance'] = @db_instance
@db_instance = fresh_instance
when 'RequestId'
@response['ResponseMetadata'][name] = value
Expand Down
2 changes: 1 addition & 1 deletion lib/fog/aws/requests/rds/create_db_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def create_db_instance(db_name, options={})
end

# These are the required parameters according to the API
required_params = %w{AllocatedStorage DBInstanceClass Engine MasterUserPassword MasterUsername }
required_params = %w{AllocatedStorage DBInstanceClass Engine MasterUsername }
required_params.each do |key|
unless options.key?(key) and options[key] and !options[key].to_s.empty?
#response.status = 400
Expand Down
2 changes: 1 addition & 1 deletion lib/fog/aws/requests/rds/create_db_snapshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def create_db_snapshot(identifier, name)
'InstanceCreateTime' => Time.now
}
# Copy attributes from server
%w(Engine EngineVersion AvailabilityZone AllocatedStorage Iops MasterUsername InstanceCreateTime).each do |key|
%w(Engine EngineVersion AvailabilityZone AllocatedStorage StorageType Iops MasterUsername InstanceCreateTime).each do |key|
snapshot_data[key] = server_data[key]
end
snapshot_data['Port'] = server_data['Endpoint']['Port']
Expand Down
102 changes: 52 additions & 50 deletions lib/fog/aws/requests/rds/modify_db_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,60 +47,62 @@ def modify_db_instance(db_name, apply_immediately, options={})

class Mock
def modify_db_instance(db_name, apply_immediately, _options={})
options = _options.dup
options = _options
response = Excon::Response.new
if server = self.data[:servers][db_name]
if server["DBInstanceStatus"] != "available"
raise Fog::AWS::RDS::NotFound.new("DBInstance #{db_name} not available for modification")
else
self.data[:modify_time] = Time.now
# TODO verify the params options
# if apply_immediately is false, all the options go to pending_modified_values and then apply and clear after either
# a reboot or the maintainance window
#if apply_immediately
# modified_server = server.merge(options)
#else
# modified_server = server["PendingModifiedValues"].merge!(options) # it appends
#end
if options["NewDBInstanceIdentifier"]
options["DBInstanceIdentifier"] = options.delete("NewDBInstanceIdentifier")
end

rds_security_groups = self.data[:security_groups].values
ec2_security_groups = Fog::Compute::AWS::Mock.data[@region][@aws_access_key_id][:security_groups].values

db_security_group_names = Array(options.delete("DBSecurityGroups"))
db_security_groups = db_security_group_names.inject([]) do |r, group_name|
unless rds_security_groups.find { |sg| sg["DBSecurityGroupName"] == group_name }
raise Fog::AWS::RDS::Error.new("InvalidParameterValue => Invalid security group , groupId= , groupName=#{group_name}")
end
r << {"Status" => "active", "DBSecurityGroupName" => group_name }
end

vpc_security_groups = Array(options.delete("VpcSecurityGroups")).inject([]) do |r, group_id|
unless ec2_security_groups.find { |sg| sg["groupId"] == group_id }
raise Fog::AWS::RDS::Error.new("InvalidParameterValue => Invalid security group , groupId=#{group_id} , groupName=")
end

r << {"Status" => "active", "VpcSecurityGroupId" => group_id }
end

options.merge!(
"DBSecurityGroups" => db_security_groups,
"VpcSecurityGroups" => vpc_security_groups
)

self.data[:servers][db_name]["PendingModifiedValues"].merge!(options) # it appends
self.data[:servers][db_name]["DBInstanceStatus"] = "modifying"
response.status = 200
response.body = {
"ResponseMetadata" => { "RequestId" => Fog::AWS::Mock.request_id },
"ModifyDBInstanceResult" => { "DBInstance" => self.data[:servers][db_name] }
}
response

server = self.data[:servers][db_name]

if server.nil?
raise Fog::AWS::RDS::NotFound.new("DBInstance #{db_name} not found")
end

if server["DBInstanceStatus"] != "available"
raise Fog::AWS::RDS::NotFound.new("DBInstance #{db_name} not available for modification")
end

db_security_group_names = Array(options.delete("DBSecurityGroups"))

rds_security_groups = self.data[:security_groups].values

db_security_groups = db_security_group_names.map do |r, group_name|
unless rds_security_groups.find { |sg| sg["DBSecurityGroupName"] == group_name }
raise Fog::AWS::RDS::Error.new("InvalidParameterValue => Invalid security group , groupId= , groupName=#{group_name}")
end
r << {"Status" => "active", "DBSecurityGroupName" => group_name }
end

ec2_security_groups = Fog::Compute::AWS::Mock.data[@region][@aws_access_key_id][:security_groups].values

vpc_security_groups = Array(options.delete("VpcSecurityGroups")).map do |group_id|
unless ec2_security_groups.find { |sg| sg["groupId"] == group_id }
raise Fog::AWS::RDS::Error.new("InvalidParameterValue => Invalid security group , groupId=#{group_id} , groupName=")
end

{"Status" => "active", "VpcSecurityGroupId" => group_id }
end

options.merge!(
"DBSecurityGroups" => db_security_groups,
"VpcSecurityGroups" => vpc_security_groups
)

if options["NewDBInstanceIdentifier"]
options["DBInstanceIdentifier"] = options.delete("NewDBInstanceIdentifier")
else
raise Fog::AWS::RDS::NotFound.new("DBInstance #{db_name} not found")
end

self.data[:modify_time] = Time.now
server["PendingModifiedValues"].merge!(options)
server["DBInstanceStatus"] = "modifying"

response.status = 200
response.body = {
"ResponseMetadata" => { "RequestId" => Fog::AWS::Mock.request_id },
"ModifyDBInstanceResult" => { "DBInstance" => server }
}

response
end
end
end
Expand Down
65 changes: 63 additions & 2 deletions lib/fog/aws/requests/rds/restore_db_instance_from_db_snapshot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,69 @@ def restore_db_instance_from_db_snapshot(snapshot_id, db_name, opts={})
end

class Mock
def restore_db_instance_from_db_snapshot(snapshot_id, db_id, options={})
Fog::Mock.not_implemented
def restore_db_instance_from_db_snapshot(snapshot_id, db_name, options={})

if self.data[:servers] and self.data[:servers][db_name]
raise Fog::AWS::RDS::IdentifierTaken.new("DBInstanceAlreadyExists #{response.body.to_s}")
end

unless self.data[:snapshots] and snapshot = self.data[:snapshots][snapshot_id]
raise Fog::AWS::RDS::NotFound.new("DBSnapshotNotFound #{response.body.to_s}")
end

if !!options["MultiAZ"] && !!options["AvailabilityZone"]
raise Fog::AWS::RDS::InvalidParameterCombination.new('Requesting a specific availability zone is not valid for Multi-AZ instances.')
end

option_group_membership =
if option_group_name = options['OptionGroupName']
[{ 'OptionGroupMembership' =>
[{ 'OptionGroupName' => option_group_name, 'Status' => "pending-apply"}] }]
else
[{ 'OptionGroupMembership' =>
[{ 'OptionGroupName' => 'default: mysql-5.6', 'Status' => "pending-apply"}] }]
end

data = {
"AllocatedStorage" => snapshot['AllocatedStorage'],
"AutoMinorVersionUpgrade" => options['AutoMinorVersionUpgrade'].nil? ? true : options['AutoMinorVersionUpgrade'],
"AvailabilityZone" => options['AvailabilityZone'],
"BackupRetentionPeriod" => options['BackupRetentionPeriod'] || 1,
"CACertificateIdentifier" => 'rds-ca-2015',
"DBInstanceClass" => options['DBInstanceClass'] || 'db.m3.medium',
"DBInstanceIdentifier" => db_name,
"DBInstanceStatus" => 'creating',
"DBName" => options['DBName'],
"DBParameterGroups" => [{'DBParameterGroupName'=>'default.mysql5.5', 'ParameterApplyStatus'=>'in-sync'}],
"DBSecurityGroups" => [{'Status'=>'active', 'DBSecurityGroupName'=>'default'}],
"Endpoint" => {},
"Engine" => options['Engine'] || snapshot['Engine'],
"EngineVersion" => options['EngineVersion'] || snapshot['EngineVersion'],
"InstanceCreateTime" => nil,
"Iops" => options['Iops'],
"LicenseModel" => options['LicenseModel'] || snapshot['LicenseModel'] || 'general-public-license',
"MasterUsername" => options['MasterUsername'] || snapshot['MasterUsername'],
"MultiAZ" => options['MultiAZ'],
"OptiongroupMemberships" => option_group_membership,
"PendingModifiedValues" => { 'MasterUserPassword' => '****' }, # This clears when is available
"PreferredBackupWindow" => '08:00-08:30',
"PreferredMaintenanceWindow" => 'mon:04:30-mon:05:00',
"PubliclyAccessible" => true,
"ReadReplicaDBInstanceIdentifiers" => [],
"StorageType" => options['StorageType'] || (options['Iops'] ? 'io1' : 'standard'),
"VpcSecurityGroups" => nil,
"StorageEncrypted" => false,
}

self.data[:servers][db_name] = data
response = Excon::Response.new
response.body =
{ "ResponseMetadata" => { "RequestId" => Fog::AWS::Mock.request_id },
"RestoreDBInstanceFromDBSnapshotResult" => { "DBInstance" => data }
}
response.status = 200
self.data[:servers][db_name]["InstanceCreateTime"] = Time.now
response
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions tests/models/rds/server_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

@instance.wait_for(20*60) { ready? }
@instance_with_encrypted_storage.wait_for(20*60) { ready? }
@final_snapshot_id = uniq_id('fog-test-snapshot')

tests("#storage_encrypted") do
returns(true) { @instance_with_encrypted_storage.storage_encrypted }
Expand Down Expand Up @@ -114,12 +115,11 @@
replica && replica.destroy

test("Destroying with a final snapshot") do
final_snapshot_id = uniq_id('fog-test-snapshot')

@instance_with_final_snapshot.wait_for { ready? }
@instance_with_final_snapshot.destroy(final_snapshot_id)
@instance_with_final_snapshot.destroy(@final_snapshot_id)
returns(true, "Final snapshot created") do
@final_snapshot = Fog::AWS[:rds].snapshots.get(final_snapshot_id)
@final_snapshot = Fog::AWS[:rds].snapshots.get(@final_snapshot_id)
!@final_snapshot.nil?
end

Expand Down
18 changes: 18 additions & 0 deletions tests/models/rds/servers_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,22 @@
collection_tests(Fog::AWS[:rds].servers, rds_default_server_params) do
@instance.wait_for { ready? }
end

tests("#restore").succeeds do
instance = Fog::AWS[:rds].servers.create(rds_default_server_params.merge(:id => uniq_id("fog-snapshot-test")))

snapshot_id = uniq_id('fog-snapshot-test')
@snapshot = instance.snapshots.create(:id => snapshot_id )
instance.destroy

db_name = uniq_id('fog-db-name')
@restore_instance = Fog::AWS[:rds].servers.restore('master_username' => instance.master_username, 'flavor_id' => 'db.m3.medium', 'source_snapshot_id' => snapshot_id, 'id' => uniq_id('restored-instance'))
end

if Fog.mocking? && @restore_instance.respond_to?(:ready?)
@restore_instance.wait_for { ready? }
end

@snapshot.destroy
@restore_instance.destroy
end
6 changes: 6 additions & 0 deletions tests/requests/rds/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,12 @@ module Formats
'DBEngineVersions' => [DB_ENGINE_VERSION]
}
)

RESTORE_DB_INSTANCE_FROM_DB_SNAPSHOT = BASIC.merge({
'RestoreDBInstanceFromDBSnapshotResult' => {
'DBInstance' => INSTANCE
}
})
end
end
end
20 changes: 16 additions & 4 deletions tests/requests/rds/instance_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
# serveral minutes for deleted servers to disappear
suffix = rand(65536).to_s(16)

@db_instance_id = "fog-test-#{suffix}"
@db_replica_id = "fog-replica-#{suffix}"
@db_snapshot_id = "fog-snapshot-#{suffix}"
@db_final_snapshot_id = "fog-final-snapshot-#{suffix}"
@db_instance_id = "fog-test-#{suffix}"
@db_replica_id = "fog-replica-#{suffix}"
@db_snapshot_id = "fog-snapshot-#{suffix}"
@db_final_snapshot_id = "fog-final-snapshot-#{suffix}"
@db_instance_restore_id = "fog-test-#{suffix}"

tests('success') do

Expand Down Expand Up @@ -109,6 +110,17 @@
body
end

tests("#restore_db_instance_from_db_snapshot").formats(AWS::RDS::Formats::RESTORE_DB_INSTANCE_FROM_DB_SNAPSHOT) do
snapshot = Fog::AWS[:rds].snapshots.get(@db_final_snapshot_id)
snapshot.wait_for { state == 'available' }
result = Fog::AWS[:rds].restore_db_instance_from_db_snapshot(@db_final_snapshot_id, @db_instance_restore_id).body
instance = result['RestoreDBInstanceFromDBSnapshotResult']['DBInstance']
returns('creating') { instance['DBInstanceStatus'] }
result
end
restore_server = Fog::AWS[:rds].servers.get(@db_instance_restore_id)
restore_server.wait_for { state == 'available' }

tests("#delete_db_snapshot").formats(AWS::RDS::Formats::DELETE_DB_SNAPSHOT) do
Fog::AWS[:rds].snapshots.get(@db_snapshot_id).wait_for { ready? }
Fog::AWS[:rds].delete_db_snapshot(@db_snapshot_id).body
Expand Down