db2s3 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/HISTORY +4 -0
- data/README +37 -0
- data/Rakefile +18 -0
- data/db2s3.gemspec +55 -0
- data/init.rb +1 -0
- data/lib/db2s3.rb +118 -0
- data/lib/db2s3/tasks.rb +1 -0
- data/rails/init.rb +1 -0
- data/spec/db2s3_spec.rb +50 -0
- data/spec/mysql_drop_schema.sql +1 -0
- data/spec/mysql_schema.sql +4 -0
- data/spec/s3_config.example.rb +5 -0
- data/spec/spec_helper.rb +18 -0
- data/tasks/tasks.rake +40 -0
- metadata +71 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
spec/s3_config.rb
|
data/HISTORY
ADDED
data/README
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
DB2S3 - A rails plugin to backup Mysql to Amazon S3
|
2
|
+
---------------------------------------------------
|
3
|
+
You're looking at a monthly spend of four cents
|
4
|
+
So pony up you cheap bastard, and store your backups on S3
|
5
|
+
|
6
|
+
Usage:
|
7
|
+
# In config/environment.rb
|
8
|
+
config.gem "xaviershay-db2s3", :lib => "db2s3", :source => "http://gems.github.com"
|
9
|
+
|
10
|
+
# In Rakefile
|
11
|
+
require 'db2s3/tasks'
|
12
|
+
|
13
|
+
# In config/initializers/db2s3.rb
|
14
|
+
DB2S3::Config.instance_eval do
|
15
|
+
S3 = {
|
16
|
+
:access_key_id => 'yourkey',
|
17
|
+
:secret_access_key => 'yoursecretkey',
|
18
|
+
:bucket => 'yourapp-db-backup'
|
19
|
+
}
|
20
|
+
end
|
21
|
+
# DB credentials are read from your rails environment
|
22
|
+
|
23
|
+
rake gems:install
|
24
|
+
|
25
|
+
# Add to your crontab or whatever
|
26
|
+
rake db2s3:backup:full
|
27
|
+
rake db2s3:backup:incremental # Unimplemented
|
28
|
+
|
29
|
+
# Handy tasks
|
30
|
+
rake db2s3:metrics # Estimated costs
|
31
|
+
rake db2s3:backup:restore # You should be testing this regularly
|
32
|
+
|
33
|
+
Caveats:
|
34
|
+
Currently does not clean up old back ups
|
35
|
+
|
36
|
+
Kudos:
|
37
|
+
http://github.com/pauldowman/blog_code_examples/tree/master/mysql_s3_backup
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
desc "Create test database"
|
2
|
+
task :create_test_db do
|
3
|
+
`mysqladmin -u root create db2s3_unittest`
|
4
|
+
end
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gemspec|
|
9
|
+
gemspec.name = 'db2s3'
|
10
|
+
gemspec.summary = "Summarize your gem"
|
11
|
+
gemspec.description = 'db2s3 provides rake tasks for backing up and restoring your DB to S3'
|
12
|
+
gemspec.email = 'contact@rhnh.net'
|
13
|
+
gemspec.homepage = 'http://github.com/xaviershay/db2s3'
|
14
|
+
gemspec.authors = ['Xavier Shay']
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
18
|
+
end
|
data/db2s3.gemspec
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{db2s3}
|
8
|
+
s.version = "0.2.5"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Xavier Shay"]
|
12
|
+
s.date = %q{2009-11-07}
|
13
|
+
s.description = %q{db2s3 provides rake tasks for backing up and restoring your DB to S3}
|
14
|
+
s.email = %q{contact@rhnh.net}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"HISTORY",
|
21
|
+
"README",
|
22
|
+
"Rakefile",
|
23
|
+
"db2s3.gemspec",
|
24
|
+
"init.rb",
|
25
|
+
"lib/db2s3.rb",
|
26
|
+
"lib/db2s3/tasks.rb",
|
27
|
+
"rails/init.rb",
|
28
|
+
"spec/db2s3_spec.rb",
|
29
|
+
"spec/mysql_drop_schema.sql",
|
30
|
+
"spec/mysql_schema.sql",
|
31
|
+
"spec/s3_config.example.rb",
|
32
|
+
"spec/spec_helper.rb",
|
33
|
+
"tasks/tasks.rake"
|
34
|
+
]
|
35
|
+
s.homepage = %q{http://github.com/xaviershay/db2s3}
|
36
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubygems_version = %q{1.3.4}
|
39
|
+
s.summary = %q{Summarize your gem}
|
40
|
+
s.test_files = [
|
41
|
+
"spec/db2s3_spec.rb",
|
42
|
+
"spec/s3_config.example.rb",
|
43
|
+
"spec/spec_helper.rb"
|
44
|
+
]
|
45
|
+
|
46
|
+
if s.respond_to? :specification_version then
|
47
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
48
|
+
s.specification_version = 3
|
49
|
+
|
50
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
51
|
+
else
|
52
|
+
end
|
53
|
+
else
|
54
|
+
end
|
55
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/rails/init"
|
data/lib/db2s3.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'aws/s3'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
class DB2S3
|
5
|
+
class Config
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
end
|
10
|
+
|
11
|
+
def full_backup
|
12
|
+
file_name = "dump-#{db_credentials[:database]}-#{Time.now.utc.strftime("%Y%m%d%H%M")}.sql.gz"
|
13
|
+
store.store(file_name, open(dump_db.path))
|
14
|
+
store.store(most_recent_dump_file_name, file_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def restore
|
18
|
+
dump_file_name = store.fetch(most_recent_dump_file_name).read
|
19
|
+
file = store.fetch(dump_file_name)
|
20
|
+
run "gunzip -c #{file.path} | mysql #{mysql_options}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def metrics
|
24
|
+
dump_file = dump_db
|
25
|
+
|
26
|
+
storage_dollars_per_byte_per_month = 0.15 / 1024.0 / 1024.0 / 1024.0
|
27
|
+
transfer_dollars_per_byte_per_month = 0.10 / 1024.0 / 1024.0 / 1024.0
|
28
|
+
full_dumps_per_month = 30
|
29
|
+
|
30
|
+
storage_cost = (dump_file.size * storage_dollars_per_byte_per_month * 100).ceil / 100.0
|
31
|
+
transfer_cost = (dump_file.size * full_dumps_per_month * transfer_dollars_per_byte_per_month * 100).ceil / 100.0
|
32
|
+
requests_cost = 0.02 # TODO: Actually calculate this, with incremental backups could be more
|
33
|
+
|
34
|
+
{
|
35
|
+
:db_size => dump_file.size,
|
36
|
+
:storage_cost => storage_cost,
|
37
|
+
:transfer_cost => transfer_cost,
|
38
|
+
:total_cost => storage_cost + transfer_cost + requests_cost,
|
39
|
+
:requests_cost => requests_cost,
|
40
|
+
:full_backups_per_month => full_dumps_per_month
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def dump_db
|
47
|
+
dump_file = Tempfile.new("dump")
|
48
|
+
|
49
|
+
#cmd = "mysqldump --quick --single-transaction --create-options -u#{db_credentials[:user]} --flush-logs --master-data=2 --delete-master-logs"
|
50
|
+
cmd = "mysqldump --quick --single-transaction --create-options #{mysql_options}"
|
51
|
+
cmd += " | gzip > #{dump_file.path}"
|
52
|
+
run(cmd)
|
53
|
+
|
54
|
+
dump_file
|
55
|
+
end
|
56
|
+
|
57
|
+
def mysql_options
|
58
|
+
cmd = " -u #{db_credentials[:username]} "
|
59
|
+
cmd += " -p'#{db_credentials[:password]}'" unless db_credentials[:password].nil?
|
60
|
+
cmd += " -h '#{db_credentials[:host]}'" unless db_credentials[:host].nil?
|
61
|
+
cmd += " #{db_credentials[:database]}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def store
|
65
|
+
@store ||= S3Store.new
|
66
|
+
end
|
67
|
+
|
68
|
+
def most_recent_dump_file_name
|
69
|
+
"most-recent-dump-#{db_credentials[:database]}.txt"
|
70
|
+
end
|
71
|
+
|
72
|
+
def run(command)
|
73
|
+
result = system(command)
|
74
|
+
raise("error, process exited with status #{$?.exitstatus}") unless result
|
75
|
+
end
|
76
|
+
|
77
|
+
def db_credentials
|
78
|
+
ActiveRecord::Base.connection.instance_eval { @config } # Dodgy!
|
79
|
+
end
|
80
|
+
|
81
|
+
class S3Store
|
82
|
+
def initialize
|
83
|
+
@connected = false
|
84
|
+
end
|
85
|
+
|
86
|
+
def ensure_connected
|
87
|
+
return if @connected
|
88
|
+
AWS::S3::Base.establish_connection!(DB2S3::Config::S3.slice(:access_key_id, :secret_access_key).merge(:use_ssl => true))
|
89
|
+
AWS::S3::Bucket.create(bucket)
|
90
|
+
@connected = true
|
91
|
+
end
|
92
|
+
|
93
|
+
def store(file_name, file)
|
94
|
+
ensure_connected
|
95
|
+
AWS::S3::S3Object.store(file_name, file, bucket)
|
96
|
+
end
|
97
|
+
|
98
|
+
def fetch(file_name)
|
99
|
+
ensure_connected
|
100
|
+
AWS::S3::S3Object.find(file_name, bucket)
|
101
|
+
|
102
|
+
file = Tempfile.new("dump")
|
103
|
+
open(file.path, 'w') do |f|
|
104
|
+
AWS::S3::S3Object.stream(file_name, bucket) do |chunk|
|
105
|
+
f.write chunk
|
106
|
+
end
|
107
|
+
end
|
108
|
+
file
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def bucket
|
114
|
+
DB2S3::Config::S3[:bucket]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
data/lib/db2s3/tasks.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Dir["#{File.dirname(__FILE__)}/../../tasks/*.rake"].each { |ext| load ext }
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/db2s3')
|
data/spec/db2s3_spec.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe 'db2s3' do
|
4
|
+
def load_schema
|
5
|
+
`cat '#{File.dirname(__FILE__) + '/mysql_schema.sql'}' | mysql -u #{DBConfig[:user]} #{DBConfig[:database]}`
|
6
|
+
end
|
7
|
+
|
8
|
+
def drop_schema
|
9
|
+
`cat '#{File.dirname(__FILE__) + '/mysql_drop_schema.sql'}' | mysql -u #{DBConfig[:user]} #{DBConfig[:database]}`
|
10
|
+
end
|
11
|
+
|
12
|
+
class Person < ActiveRecord::Base
|
13
|
+
end
|
14
|
+
|
15
|
+
if DB2S3::Config.const_defined?('S3')
|
16
|
+
it 'can save and restore a backup to S3' do
|
17
|
+
db2s3 = DB2S3.new
|
18
|
+
load_schema
|
19
|
+
Person.create!(:name => "Baxter")
|
20
|
+
db2s3.full_backup
|
21
|
+
drop_schema
|
22
|
+
db2s3.restore
|
23
|
+
Person.find_by_name("Baxter").should_not be_nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'provides estimated metrics' do
|
28
|
+
db2s3 = DB2S3.new
|
29
|
+
# 1 GB DB
|
30
|
+
db2s3.stub!(:dump_db).and_return(stub("dump file", :size => 1024 * 1024 * 1024))
|
31
|
+
metrics = db2s3.metrics
|
32
|
+
metrics.should == {
|
33
|
+
:storage_cost => 0.15, # 15c/GB-Month rounded up to nearest cent, we're only storing one backup
|
34
|
+
:transfer_cost => 3.0, # 10c/GB-Month * 30 backups
|
35
|
+
:db_size => 1024 * 1024 * 1024, # 1 GB
|
36
|
+
:total_cost => 3.17,
|
37
|
+
:requests_cost => 0.02,
|
38
|
+
:full_backups_per_month => 30 # Default 1 backup/day
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'rounds transfer cost metric up to nearest cent' do
|
43
|
+
db2s3 = DB2S3.new
|
44
|
+
# 1 KB DB
|
45
|
+
db2s3.stub!(:dump_db).and_return(stub("dump file", :size => 1024))
|
46
|
+
metrics = db2s3.metrics
|
47
|
+
metrics[:storage_cost].should == 0.01
|
48
|
+
metrics[:transfer_cost].should == 0.01
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
DROP TABLE IF EXISTS people;
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec'
|
2
|
+
require 'activerecord'
|
3
|
+
require File.dirname(__FILE__) + '/../lib/db2s3'
|
4
|
+
if File.exists?(File.dirname(__FILE__) + '/s3_config.rb')
|
5
|
+
require File.dirname(__FILE__) + '/s3_config.rb'
|
6
|
+
else
|
7
|
+
puts "s3_config.rb does not exist - not running live tests"
|
8
|
+
end
|
9
|
+
|
10
|
+
DBConfig = {
|
11
|
+
:adapter => "mysql",
|
12
|
+
:encoding => "utf8",
|
13
|
+
:database => 'db2s3_unittest',
|
14
|
+
:user => "root"
|
15
|
+
}
|
16
|
+
|
17
|
+
ActiveRecord::Base.configurations = { 'production' => DBConfig }
|
18
|
+
ActiveRecord::Base.establish_connection(:production)
|
data/tasks/tasks.rake
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
namespace :db2s3 do
|
2
|
+
namespace :backup do
|
3
|
+
desc "Save a full back to S3"
|
4
|
+
task :full => :environment do
|
5
|
+
DB2S3.new.full_backup
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Restore your DB from S3"
|
9
|
+
task :restore => :environment do
|
10
|
+
DB2S3.new.restore
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Provide estimated costs for backing up your DB to S3"
|
15
|
+
task :metrics => :environment do
|
16
|
+
def format_size(size)
|
17
|
+
units = %w{B KB MB GB TB}
|
18
|
+
e = (Math.log(size)/Math.log(1024)).floor
|
19
|
+
s = "%.3f" % (size.to_f / 1024**e)
|
20
|
+
s.sub(/\.?0*$/, units[e])
|
21
|
+
end
|
22
|
+
|
23
|
+
def format_cost(cost)
|
24
|
+
"%.2f" % [cost]
|
25
|
+
end
|
26
|
+
|
27
|
+
metrics = DB2S3.new.metrics
|
28
|
+
puts <<-EOS
|
29
|
+
Estimates only, does not take into account metadata overhead
|
30
|
+
Code has recently been added that keeps old backups around - this is not taken into account in these estimates
|
31
|
+
|
32
|
+
DB Size: #{format_size(metrics[:db_size])}
|
33
|
+
Full backups/month: #{metrics[:full_backups_per_month]}
|
34
|
+
Storage Cost $US: #{format_cost(metrics[:storage_cost])}
|
35
|
+
Transfer Cost $US: #{format_cost(metrics[:transfer_cost])}
|
36
|
+
Requests Cost $US: #{format_cost(metrics[:requests_cost])}
|
37
|
+
Total Cost $US: #{format_cost(metrics[:total_cost])}
|
38
|
+
EOS
|
39
|
+
end
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: db2s3
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Xavier Shay
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-07 00:00:00 +11:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: db2s3 provides rake tasks for backing up and restoring your DB to S3
|
17
|
+
email: contact@rhnh.net
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
files:
|
25
|
+
- .gitignore
|
26
|
+
- HISTORY
|
27
|
+
- README
|
28
|
+
- Rakefile
|
29
|
+
- db2s3.gemspec
|
30
|
+
- init.rb
|
31
|
+
- lib/db2s3.rb
|
32
|
+
- lib/db2s3/tasks.rb
|
33
|
+
- rails/init.rb
|
34
|
+
- spec/db2s3_spec.rb
|
35
|
+
- spec/mysql_drop_schema.sql
|
36
|
+
- spec/mysql_schema.sql
|
37
|
+
- spec/s3_config.example.rb
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
- tasks/tasks.rake
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://github.com/xaviershay/db2s3
|
42
|
+
licenses: []
|
43
|
+
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options:
|
46
|
+
- --charset=UTF-8
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.3.4
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Summarize your gem
|
68
|
+
test_files:
|
69
|
+
- spec/db2s3_spec.rb
|
70
|
+
- spec/s3_config.example.rb
|
71
|
+
- spec/spec_helper.rb
|