db2s3 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|