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 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
- ** This app will be launched soon! **
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.
@@ -10,7 +10,8 @@ module Dumper
10
10
  autoload :VERSION, 'dumper/version'
11
11
 
12
12
  module Database
13
- autoload :Base, 'dumper/database/base'
14
- autoload :MySQL, 'dumper/database/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
@@ -31,7 +31,7 @@ module Dumper
31
31
  end
32
32
 
33
33
  def start
34
- return unless @app_key and @stack.supported?
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.", :debug
45
+ log "sleeping #{sec} seconds for agent/register", :debug
46
46
  sleep sec
47
- json = send_request(api: 'agent/register', json: MultiJson.encode(register_hash))
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 = send_request(api: 'agent/poll', params: { token: @token })
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 @token ? 60.seconds : 1.hour
79
+ sleep [ json[:interval].to_i, 60 ].max
80
80
  end
81
81
  end
82
82
 
83
83
  def register_hash
84
84
  {
85
- # :pid => Process.pid,
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 send_request(options)
94
- uri = URI.parse("#{@api_base}/api/#{options[: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.decode(response.body).with_indifferent_access
115
+ MultiJson.load(response.body).with_indifferent_access
117
116
  else
118
- log "******** ERROR on api: #{options[:api]}, resp code: #{response.code} ********", :error
117
+ log "******** ERROR on api: #{method_name}, resp code: #{response.code} ********", :error
119
118
  {} # return empty hash
120
119
  end
121
120
  rescue
@@ -1,8 +1,30 @@
1
1
  module Dumper
2
2
  module Database
3
3
  class Base
4
- def initialize(stack)
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(:mysqldump)} #{connection_options} #{additional_options} #{@stack.activerecord_config['database']} | gzip"
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.activerecord_config[option.to_s].blank?
13
- "--#{option}='#{@stack.activerecord_config[option.to_s]}'".gsub('--username', '--user')
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
@@ -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
- # Prepare
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
- abort 'invalid server type!' # TBD
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
- tempfile.write buffer
53
- if tempfile.size > Backup::MAX_FILESIZE
54
- raise 'Max filesize exceeded.'
55
- end
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
- @agent.send_request(api: 'backup/fail', params: { backup_id: backup_id, code: 'dump_error', message: $!.to_s })
60
- abort
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.send_request(api: 'backup/commit', params: { backup_id: backup_id, dump_duration: dump_duration.to_i })
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
@@ -1,10 +1,17 @@
1
1
  module Dumper
2
2
  class Stack
3
- DUMP_TOOLS = {}
3
+ include Dumper::Utility::ObjectFinder
4
4
 
5
- attr_accessor :rails_env, :dispatcher, :framework, :rackup, :activerecord_config
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
- @activerecord_config = ActiveRecord::Base.configurations[@rails_env]
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
- activerecord_config: @activerecord_config.reject{|k,v| k == 'password' },
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 && mysql?
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
- # defined?(::Thin::Server) && find_instance_in_object_space(Thin::Server)
70
- @rackup and @rackup.server.to_s.demodulize == 'Thin'
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Dumper
2
- VERSION = '0.0.7'
2
+ VERSION = '0.1.0'
3
3
  end
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.7
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-30 00:00:00.000000000 Z
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