dumper 0.0.7 → 0.1.0
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/README.md +13 -1
- data/lib/dumper.rb +3 -2
- data/lib/dumper/agent.rb +10 -11
- data/lib/dumper/database/base.rb +23 -1
- data/lib/dumper/database/mongodb.rb +42 -0
- data/lib/dumper/database/mysql.rb +22 -5
- data/lib/dumper/job.rb +29 -20
- data/lib/dumper/stack.rb +18 -27
- data/lib/dumper/utility.rb +7 -1
- data/lib/dumper/version.rb +1 -1
- metadata +3 -2
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Dumper is a backup management system that offers a whole new way to take daily backups of your databases.
|
4
4
|
|
5
|
-
**
|
5
|
+
**This app will be launched soon!**
|
6
6
|
|
7
7
|
## Supported Stacks
|
8
8
|
|
@@ -25,3 +25,15 @@ then create `config/initializers/dumper.rb` and put the following line.
|
|
25
25
|
```ruby
|
26
26
|
Dumper::Agent.start(:app_key => 'YOUR_APP_KEY')
|
27
27
|
```
|
28
|
+
|
29
|
+
or, if you want to conditionally start the agent, pass a block that evaluates to true/false to `#start_if` method.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
Dumper::Agent.start_if(:app_key => 'YOUR_APP_KEY') { Rails.env.production? && dumper_enabled_host? }
|
33
|
+
```
|
34
|
+
|
35
|
+
That's it!
|
36
|
+
|
37
|
+
Now, start your server and go to the Dumper site.
|
38
|
+
|
39
|
+
You'll find your application is registered and ready to take backups daily.
|
data/lib/dumper.rb
CHANGED
@@ -10,7 +10,8 @@ module Dumper
|
|
10
10
|
autoload :VERSION, 'dumper/version'
|
11
11
|
|
12
12
|
module Database
|
13
|
-
autoload :Base,
|
14
|
-
autoload :MySQL,
|
13
|
+
autoload :Base, 'dumper/database/base'
|
14
|
+
autoload :MySQL, 'dumper/database/mysql'
|
15
|
+
autoload :MongoDB, 'dumper/database/mongodb'
|
15
16
|
end
|
16
17
|
end
|
data/lib/dumper/agent.rb
CHANGED
@@ -31,7 +31,7 @@ module Dumper
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def start
|
34
|
-
return unless @
|
34
|
+
return unless @stack.supported?
|
35
35
|
log "stack: dispatcher = #{@stack.dispatcher}, framework = #{@stack.framework}, rackup = #{@stack.rackup}"
|
36
36
|
|
37
37
|
@loop_thread = Thread.new { start_loop }
|
@@ -42,9 +42,9 @@ module Dumper
|
|
42
42
|
sec = 1
|
43
43
|
begin
|
44
44
|
sec *= 2
|
45
|
-
log "sleeping #{sec} seconds for agent/register
|
45
|
+
log "sleeping #{sec} seconds for agent/register", :debug
|
46
46
|
sleep sec
|
47
|
-
json =
|
47
|
+
json = api_request('agent/register', :json => MultiJson.dump(register_hash))
|
48
48
|
end until json[:status]
|
49
49
|
|
50
50
|
return log("agent stopped: #{json.to_s}") if json[:status] == 'error'
|
@@ -54,7 +54,7 @@ module Dumper
|
|
54
54
|
sleep 1.hour unless @token
|
55
55
|
|
56
56
|
loop do
|
57
|
-
json =
|
57
|
+
json = api_request('agent/poll', :params => { :token => @token })
|
58
58
|
|
59
59
|
if json[:status] == 'ok'
|
60
60
|
# Promoted or demoted?
|
@@ -76,22 +76,21 @@ module Dumper
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
79
|
-
sleep
|
79
|
+
sleep [ json[:interval].to_i, 60 ].max
|
80
80
|
end
|
81
81
|
end
|
82
82
|
|
83
83
|
def register_hash
|
84
84
|
{
|
85
|
-
|
86
|
-
# :host => Socket.gethostname,
|
85
|
+
:hostname => Socket.gethostname,
|
87
86
|
:agent_version => Dumper::VERSION,
|
88
87
|
:app_name => @app_name,
|
89
88
|
:stack => @stack.to_hash,
|
90
89
|
}
|
91
90
|
end
|
92
91
|
|
93
|
-
def
|
94
|
-
uri = URI.parse("#{@api_base}/api/#{
|
92
|
+
def api_request(method_name, options)
|
93
|
+
uri = URI.parse("#{@api_base}/api/#{method_name}")
|
95
94
|
http = Net::HTTP.new(uri.host, uri.port)
|
96
95
|
if uri.is_a? URI::HTTPS
|
97
96
|
http.use_ssl = true
|
@@ -113,9 +112,9 @@ module Dumper
|
|
113
112
|
response = http.request(request)
|
114
113
|
if response.code == '200'
|
115
114
|
log response.body, :debug
|
116
|
-
MultiJson.
|
115
|
+
MultiJson.load(response.body).with_indifferent_access
|
117
116
|
else
|
118
|
-
log "******** ERROR on api: #{
|
117
|
+
log "******** ERROR on api: #{method_name}, resp code: #{response.code} ********", :error
|
119
118
|
{} # return empty hash
|
120
119
|
end
|
121
120
|
rescue
|
data/lib/dumper/database/base.rb
CHANGED
@@ -1,8 +1,30 @@
|
|
1
1
|
module Dumper
|
2
2
|
module Database
|
3
3
|
class Base
|
4
|
-
|
4
|
+
include Dumper::Utility::ObjectFinder
|
5
|
+
|
6
|
+
attr_accessor :tempfile
|
7
|
+
|
8
|
+
def initialize(stack = nil, options = {})
|
5
9
|
@stack = stack
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def file_ext
|
14
|
+
self.class::FILE_EXT
|
15
|
+
end
|
16
|
+
|
17
|
+
def dump_tool_path
|
18
|
+
tool = self.class::DUMP_TOOL
|
19
|
+
path = `which #{tool}`.chomp
|
20
|
+
if path.empty?
|
21
|
+
# /usr/local/mysql/bin = OSX binary, /usr/local/bin = homebrew, /usr/bin = linux
|
22
|
+
dir = [ '/usr/local/mysql/bin', '/usr/local/bin', '/usr/bin' ].find do |i|
|
23
|
+
File.exist?("#{i}/#{tool}")
|
24
|
+
end
|
25
|
+
path = "#{dir}/#{tool}" if dir
|
26
|
+
end
|
27
|
+
path
|
6
28
|
end
|
7
29
|
end
|
8
30
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Dumper
|
2
|
+
module Database
|
3
|
+
class MongoDB < Base
|
4
|
+
DUMP_TOOL = 'mongodump'
|
5
|
+
FILE_EXT = 'tar.gz'
|
6
|
+
|
7
|
+
def command
|
8
|
+
"#{@stack.configs[:mongodb][:dump_tool]} #{connection_options} #{additional_options} && cd #{@options[:tmpdir]} && tar -czf #{@tempfile.path} ."
|
9
|
+
end
|
10
|
+
|
11
|
+
def connection_options
|
12
|
+
[ :database, :host, :port, :username, :password ].map do |option|
|
13
|
+
next if @stack.configs[:mongodb][option].blank?
|
14
|
+
"--#{option}='#{@stack.configs[:mongodb][option]}'".gsub('--database', '--db')
|
15
|
+
end.compact.join(' ')
|
16
|
+
end
|
17
|
+
|
18
|
+
def additional_options
|
19
|
+
"--out='#{@options[:tmpdir]}'"
|
20
|
+
end
|
21
|
+
|
22
|
+
def finalize
|
23
|
+
FileUtils.remove_entry_secure @options[:tmpdir] if File.exist? @options[:tmpdir]
|
24
|
+
end
|
25
|
+
|
26
|
+
def config_for(rails_env=nil)
|
27
|
+
return unless mongo = find_instance_in_object_space(Mongo::DB)
|
28
|
+
|
29
|
+
{
|
30
|
+
:host => mongo.connection.host,
|
31
|
+
:port => mongo.connection.port,
|
32
|
+
:database => mongo.name,
|
33
|
+
:dump_tool => dump_tool_path
|
34
|
+
}.tap do |h|
|
35
|
+
if auth = mongo.connection.auths.first
|
36
|
+
h.update(:username => auth['username'], :password => auth['password'])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -1,22 +1,39 @@
|
|
1
1
|
module Dumper
|
2
2
|
module Database
|
3
3
|
class MySQL < Base
|
4
|
+
DUMP_TOOL = 'mysqldump'
|
5
|
+
FILE_EXT = 'sql.gz'
|
6
|
+
|
4
7
|
def command
|
5
|
-
"#{@stack.dump_tool
|
8
|
+
"#{@stack.configs[:mysql][:dump_tool]} #{connection_options} #{additional_options} #{@stack.configs[:mysql][:database]} | gzip > #{@tempfile.path}"
|
6
9
|
end
|
7
10
|
|
8
|
-
protected
|
9
|
-
|
10
11
|
def connection_options
|
11
12
|
[ :host, :port, :username, :password ].map do |option|
|
12
|
-
next if @stack.
|
13
|
-
"--#{option}='#{@stack.
|
13
|
+
next if @stack.configs[:mysql][option].blank?
|
14
|
+
"--#{option}='#{@stack.configs[:mysql][option]}'".gsub('--username', '--user')
|
14
15
|
end.compact.join(' ')
|
15
16
|
end
|
16
17
|
|
17
18
|
def additional_options
|
18
19
|
'--single-transaction'
|
19
20
|
end
|
21
|
+
|
22
|
+
def config_for(rails_env=nil)
|
23
|
+
return unless defined?(ActiveRecord::Base) &&
|
24
|
+
ActiveRecord::Base.configurations &&
|
25
|
+
(config = ActiveRecord::Base.configurations[rails_env]) &&
|
26
|
+
%w(mysql mysql2).include?(config['adapter'])
|
27
|
+
|
28
|
+
{
|
29
|
+
:host => config['host'],
|
30
|
+
:port => config['port'],
|
31
|
+
:username => config['username'],
|
32
|
+
:password => config['password'],
|
33
|
+
:database => config['database'],
|
34
|
+
:dump_tool => dump_tool_path
|
35
|
+
}
|
36
|
+
end
|
20
37
|
end
|
21
38
|
end
|
22
39
|
end
|
data/lib/dumper/job.rb
CHANGED
@@ -5,6 +5,8 @@ module Dumper
|
|
5
5
|
include POSIX::Spawn
|
6
6
|
include Dumper::Utility::LoggingMethods
|
7
7
|
|
8
|
+
MAX_FILESIZE = 4.gigabytes
|
9
|
+
|
8
10
|
def initialize(agent, job)
|
9
11
|
@agent = agent
|
10
12
|
@stack = agent.stack
|
@@ -18,26 +20,32 @@ module Dumper
|
|
18
20
|
ensure
|
19
21
|
log_last_error if $!
|
20
22
|
log 'exiting...'
|
21
|
-
exit
|
23
|
+
exit!(true) # Do not use exit or abort to skip at_exit execution, or pid could get deleted on thin
|
22
24
|
end
|
23
25
|
|
24
26
|
def perform(server)
|
25
|
-
#
|
26
|
-
json = @agent.send_request(api: 'backup/prepare', params: { server_id: server[:id], manual: server[:manual].to_s })
|
27
|
-
return unless json[:status] == 'ok'
|
28
|
-
|
27
|
+
# Initialize database
|
29
28
|
case server[:type]
|
30
29
|
when 'mysql'
|
31
30
|
@database = Dumper::Database::MySQL.new(@stack)
|
31
|
+
when 'mongodb'
|
32
|
+
@database = Dumper::Database::MongoDB.new(@stack, :tmpdir => Dir.mktmpdir)
|
32
33
|
else
|
33
|
-
|
34
|
+
log "invalid server type: #{server[:type]}"
|
35
|
+
exit!
|
34
36
|
end
|
37
|
+
|
38
|
+
# Prepare
|
39
|
+
json = @agent.api_request('backup/prepare', :params => { :server_id => server[:id], :manual => server[:manual].to_s, :ext => @database.file_ext })
|
40
|
+
return unless json[:status] == 'ok'
|
41
|
+
|
35
42
|
backup_id = json[:backup][:id]
|
36
43
|
filename = json[:backup][:filename]
|
37
44
|
|
38
45
|
# Dump
|
39
46
|
start_at = Time.now
|
40
47
|
tempfile = ruby19? ? Tempfile.new(filename, encoding: 'ascii-8bit') : Tempfile.new(filename)
|
48
|
+
@database.tempfile = tempfile
|
41
49
|
log 'starting backup...'
|
42
50
|
log "tempfile = #{tempfile.path}"
|
43
51
|
log "command = #{@database.command}"
|
@@ -45,36 +53,37 @@ module Dumper
|
|
45
53
|
begin
|
46
54
|
pid, stdin, stdout, stderr = popen4(@database.command)
|
47
55
|
stdin.close
|
48
|
-
# Reuse buffer: http://www.ruby-forum.com/topic/134164
|
49
|
-
buffer_size = 1.megabytes
|
50
|
-
buffer = "\x00" * buffer_size # fixed-size malloc optimization
|
51
|
-
while stdout.read(buffer_size, buffer)
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
end
|
56
|
+
# # Reuse buffer: http://www.ruby-forum.com/topic/134164
|
57
|
+
# buffer_size = 1.megabytes
|
58
|
+
# buffer = "\x00" * buffer_size # fixed-size malloc optimization
|
59
|
+
# while stdout.read(buffer_size, buffer)
|
60
|
+
# tempfile.write buffer
|
61
|
+
# if tempfile.size > MAX_FILESIZE
|
62
|
+
# raise 'Max filesize exceeded.'
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
# tempfile.flush
|
57
66
|
rescue
|
58
67
|
Process.kill(:INT, pid) rescue SystemCallError
|
59
|
-
@
|
60
|
-
|
68
|
+
@database.finalize if @database.respond_to?(:finalize)
|
69
|
+
@agent.api_request('backup/fail', :params => { :backup_id => backup_id, :code => 'dump_error', :message => $!.to_s })
|
70
|
+
exit!
|
61
71
|
ensure
|
62
72
|
[stdin, stdout, stderr].each{|io| io.close unless io.closed? }
|
63
73
|
Process.waitpid(pid)
|
64
74
|
end
|
65
75
|
|
66
|
-
tempfile.flush
|
67
|
-
|
68
76
|
dump_duration = Time.now - start_at
|
69
77
|
log "dump_duration = #{dump_duration}"
|
70
78
|
|
71
79
|
upload_to_s3(json[:url], json[:fields], tempfile.path, filename)
|
72
80
|
|
73
|
-
json = @agent.
|
81
|
+
json = @agent.api_request('backup/commit', :params => { :backup_id => backup_id, :dump_duration => dump_duration.to_i })
|
74
82
|
rescue
|
75
83
|
log_last_error
|
76
84
|
ensure
|
77
85
|
tempfile.close(true)
|
86
|
+
@database.finalize if @database.respond_to?(:finalize)
|
78
87
|
end
|
79
88
|
|
80
89
|
# Upload
|
data/lib/dumper/stack.rb
CHANGED
@@ -1,10 +1,17 @@
|
|
1
1
|
module Dumper
|
2
2
|
class Stack
|
3
|
-
|
3
|
+
include Dumper::Utility::ObjectFinder
|
4
4
|
|
5
|
-
|
5
|
+
DATABASES = {
|
6
|
+
:mysql => Dumper::Database::MySQL,
|
7
|
+
:mongodb => Dumper::Database::MongoDB,
|
8
|
+
}
|
9
|
+
|
10
|
+
attr_accessor :rails_env, :dispatcher, :framework, :rackup, :configs
|
6
11
|
|
7
12
|
def initialize
|
13
|
+
@configs = {}
|
14
|
+
|
8
15
|
# Rackup?
|
9
16
|
@rackup = find_instance_in_object_space(Rack::Server)
|
10
17
|
|
@@ -14,7 +21,11 @@ module Dumper
|
|
14
21
|
@rails_env = Rails.env.to_s
|
15
22
|
@rails_version = Rails::VERSION::STRING
|
16
23
|
@is_supported_rails_version = (::Rails::VERSION::MAJOR >= 3)
|
17
|
-
|
24
|
+
DATABASES.each do |key, klass|
|
25
|
+
next unless config = klass.new.config_for(@rails_env)
|
26
|
+
@configs[key] = config
|
27
|
+
end
|
28
|
+
|
18
29
|
else
|
19
30
|
@framework = :ruby
|
20
31
|
end
|
@@ -31,29 +42,13 @@ module Dumper
|
|
31
42
|
rails_env: @rails_env,
|
32
43
|
rails_version: @rails_version,
|
33
44
|
dispatcher: @dispatcher,
|
34
|
-
|
35
|
-
dump_tools: DUMP_TOOLS,
|
45
|
+
configs: Hash[@configs.map{|k, config| [ k, config.reject{|k,v| k == :password } ] }]
|
36
46
|
}
|
37
47
|
end
|
38
48
|
|
39
49
|
# Compatibility
|
40
50
|
def supported?
|
41
|
-
@is_supported_rails_version && @dispatcher &&
|
42
|
-
end
|
43
|
-
|
44
|
-
# Database
|
45
|
-
def mysql?
|
46
|
-
dump_tool(:mysqldump) # Just assign DUMP_TOOLS
|
47
|
-
%w(mysql mysql2).include?(@activerecord_config['adapter'])
|
48
|
-
end
|
49
|
-
|
50
|
-
# Dump Tool
|
51
|
-
def dump_tool(name)
|
52
|
-
DUMP_TOOLS[name] ||= `which #{name}`.chomp
|
53
|
-
end
|
54
|
-
|
55
|
-
def dump_tool_installed?(name)
|
56
|
-
!dump_tool(name).empty?
|
51
|
+
@is_supported_rails_version && @dispatcher && !@configs.empty?
|
57
52
|
end
|
58
53
|
|
59
54
|
# Dispatcher
|
@@ -66,8 +61,8 @@ module Dumper
|
|
66
61
|
end
|
67
62
|
|
68
63
|
def thin?
|
69
|
-
|
70
|
-
|
64
|
+
defined?(::Thin::Server) && find_instance_in_object_space(::Thin::Server) ||
|
65
|
+
(@rackup && @rackup.server.to_s.demodulize == 'Thin')
|
71
66
|
end
|
72
67
|
|
73
68
|
def mongrel?
|
@@ -79,9 +74,5 @@ module Dumper
|
|
79
74
|
# defined?(::WEBrick::VERSION)
|
80
75
|
@rackup and @rackup.server.to_s.demodulize == 'WEBrick'
|
81
76
|
end
|
82
|
-
|
83
|
-
def find_instance_in_object_space(klass)
|
84
|
-
ObjectSpace.each_object(klass).first
|
85
|
-
end
|
86
77
|
end
|
87
78
|
end
|
data/lib/dumper/utility.rb
CHANGED
@@ -48,7 +48,7 @@ module Dumper
|
|
48
48
|
class SlimFormatter < Logger::Formatter
|
49
49
|
# This method is invoked when a log event occurs
|
50
50
|
def call(severity, time, progname, msg)
|
51
|
-
"[%s] %5s : %s\n" % [format_datetime(time), severity, msg2str(msg)]
|
51
|
+
"[%s (%d)] %5s : %s\n" % [format_datetime(time), $$, severity, msg2str(msg)]
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
@@ -72,5 +72,11 @@ module Dumper
|
|
72
72
|
log [ $!.class.name, $!.to_s ].join(', ')
|
73
73
|
end
|
74
74
|
end
|
75
|
+
|
76
|
+
module ObjectFinder
|
77
|
+
def find_instance_in_object_space(klass)
|
78
|
+
ObjectSpace.each_object(klass).first
|
79
|
+
end
|
80
|
+
end
|
75
81
|
end
|
76
82
|
end
|
data/lib/dumper/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dumper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-05-
|
12
|
+
date: 2012-05-31 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: multi_json
|
@@ -112,6 +112,7 @@ files:
|
|
112
112
|
- lib/dumper/agent.rb
|
113
113
|
- lib/dumper/cli.rb
|
114
114
|
- lib/dumper/database/base.rb
|
115
|
+
- lib/dumper/database/mongodb.rb
|
115
116
|
- lib/dumper/database/mysql.rb
|
116
117
|
- lib/dumper/dependency.rb
|
117
118
|
- lib/dumper/job.rb
|