dumper 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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