dumper 0.0.3 → 0.0.4

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 CHANGED
@@ -1,17 +1,2 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
1
  Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
2
  pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/README.md CHANGED
@@ -1,19 +1,27 @@
1
- # Dumper
1
+ # Dumper Agent for Rails
2
2
 
3
- Utility that checks the status of a database.
3
+ Dumper is a backup management system that offers a whole new way to take daily backups of your databases.
4
4
 
5
- ## Installation
5
+ ** This app will be launched soon! **
6
+
7
+ ## Supported Stacks
8
+
9
+ * Ruby 1.8.7 , Ruby 1.9.2 or later
10
+ * Rails 3.0 or later
11
+ * MySQL with ActiveRecord
6
12
 
7
- $ gem install dumper
13
+ Support for PostgreSQL, MongoDB and Redis are coming soon.
14
+
15
+ ## Installation
8
16
 
9
- ## Usage
17
+ Add the following line to your Rails project Gemfile:
10
18
 
11
- TODO: Write usage instructions here
19
+ ```ruby
20
+ gem 'dumper'
21
+ ```
12
22
 
13
- ## Contributing
23
+ then create `config/initializers/dumper.rb` and put the following line.
14
24
 
15
- 1. Fork it
16
- 2. Create your feature branch (`git checkout -b my-new-feature`)
17
- 3. Commit your changes (`git commit -am 'Added some feature'`)
18
- 4. Push to the branch (`git push origin my-new-feature`)
19
- 5. Create new Pull Request
25
+ ```ruby
26
+ Dumper::Agent.start(:app_key => 'YOUR_APP_KEY')
27
+ ```
data/Rakefile CHANGED
@@ -1,2 +1,7 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
+
4
+ # RSpec
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ task :default => :spec
@@ -2,19 +2,23 @@
2
2
  require File.expand_path('../lib/dumper/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
- gem.authors = ["Kenn Ejima"]
6
- gem.email = ["kenn.ejima@gmail.com"]
7
- gem.description = %q{Utility that checks the status of a database}
8
- gem.summary = %q{Utility that checks the status of a database}
9
- gem.homepage = ""
5
+ gem.authors = ['Kenn Ejima']
6
+ gem.email = ['kenn.ejima@gmail.com']
7
+ gem.description = 'Dumper is a backup management system that offers a whole new way to take daily backups of your databases. (coming soon!)'
8
+ gem.summary = 'The Dumper Agent for Rails (coming soon!)'
9
+ gem.homepage = 'https://github.com/kenn/dumper'
10
10
 
11
11
  gem.files = `git ls-files`.split($\)
12
12
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
13
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
- gem.name = "dumper"
15
- gem.require_paths = ["lib"]
14
+ gem.name = 'dumper'
15
+ gem.require_paths = ['lib']
16
16
  gem.version = Dumper::VERSION
17
17
 
18
- gem.add_runtime_dependency "thor"
19
- gem.add_runtime_dependency "rainbow"
18
+ gem.add_runtime_dependency 'multi_json', '~> 1.0'
19
+ gem.add_runtime_dependency 'posix-spawn', '~> 0.3.6'
20
+ gem.add_development_dependency 'rspec'
21
+
22
+ # For Travis
23
+ gem.add_development_dependency 'rake'
20
24
  end
@@ -1,9 +1,16 @@
1
- require 'thor'
2
- require 'rainbow'
3
1
  require 'socket'
4
2
 
5
3
  module Dumper
6
- autoload :Cli, 'dumper/cli'
7
- autoload :Utility, 'dumper/utility'
8
- autoload :VERSION, 'dumper/version'
4
+ autoload :Agent, 'dumper/agent'
5
+ autoload :Cli, 'dumper/cli'
6
+ autoload :Dependency, 'dumper/dependency'
7
+ autoload :Job, 'dumper/job'
8
+ autoload :Stack, 'dumper/stack'
9
+ autoload :Utility, 'dumper/utility'
10
+ autoload :VERSION, 'dumper/version'
11
+
12
+ module Database
13
+ autoload :Base, 'dumper/database/base'
14
+ autoload :MySQL, 'dumper/database/mysql'
15
+ end
9
16
  end
@@ -0,0 +1,112 @@
1
+ require 'timeout'
2
+ require 'net/http'
3
+
4
+ module Dumper
5
+ class Agent
6
+ include Dumper::Utility::LoggingMethods
7
+
8
+ API_VERSION = 1
9
+
10
+ attr_reader :stack
11
+
12
+ class << self
13
+ def start(options = {})
14
+ new(options).start
15
+ end
16
+
17
+ def start_if(options = {})
18
+ start(options) if yield
19
+ end
20
+ end
21
+
22
+ def initialize(options = {})
23
+ log '**** Dumper requires :app_key! ****' if options[:app_key].blank?
24
+
25
+ @stack = Dumper::Stack.new
26
+ @api_base = options[:api_base] || 'http://dumper.io'
27
+ @app_key = options[:app_key]
28
+ @app_env = @stack.rails_env
29
+ @app_name = ObjectSpace.each_object(Rails::Application).first.class.name.split("::").first
30
+ end
31
+
32
+ def start
33
+ return unless @app_key and @stack.supported?
34
+ log "stack: dispatcher = #{@stack.dispatcher}, framework = #{@stack.framework}, rackup = #{@stack.rackup}"
35
+
36
+ @loop_thread = Thread.new { start_loop }
37
+ @loop_thread[:name] = 'Loop Thread'
38
+ end
39
+
40
+ def start_loop
41
+ sleep 3
42
+ json = send_request(api: 'agent/register', json: MultiJson.encode(register_hash))
43
+ @token = json[:token] if json && json[:token]
44
+ sleep 3
45
+ loop do
46
+ json = send_request(api: 'agent/poll', params: { token: @token })
47
+ if json
48
+ # Promoted or demoted?
49
+ if json[:token]
50
+ @token = json[:token] # promote
51
+ else
52
+ @token = nil # demote
53
+ end
54
+
55
+ if json[:job]
56
+ if pid = fork
57
+ # Parent
58
+ srand # Ruby 1.8.7 needs reseeding - http://bugs.ruby-lang.org/issues/4338
59
+ Process.detach(pid)
60
+ else
61
+ # Child
62
+ Dumper::Job.new(self, json[:job]).run_and_exit
63
+ end
64
+ end
65
+ end
66
+ sleep @token ? 60.seconds : 1.hour
67
+ end
68
+ end
69
+
70
+ def register_hash
71
+ {
72
+ # :pid => Process.pid,
73
+ # :host => Socket.gethostname,
74
+ :agent_version => Dumper::VERSION,
75
+ :app_name => @app_name,
76
+ :stack => @stack.to_hash,
77
+ }
78
+ end
79
+
80
+ def send_request(options)
81
+ uri = URI.parse("#{@api_base}/api/#{options[:api]}")
82
+ http = Net::HTTP.new(uri.host, uri.port)
83
+ if uri.is_a? URI::HTTPS
84
+ http.use_ssl = true
85
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
86
+ end
87
+ request = Net::HTTP::Post.new(uri.request_uri)
88
+ request['x-app-key'] = @app_key
89
+ request['x-app-env'] = @app_env
90
+ request['x-api-version'] = API_VERSION.to_s
91
+ request['user-agent'] = "Dumper-RailsAgent/#{Dumper::VERSION} (ruby #{::RUBY_VERSION} #{::RUBY_PLATFORM} / rails #{Rails::VERSION::STRING})"
92
+ if options[:params]
93
+ request.set_form_data(options[:params])
94
+ else
95
+ # Without empty string, WEBrick would complain WEBrick::HTTPStatus::LengthRequired for empty POSTs
96
+ request.body = options[:json] || ''
97
+ request['Content-Type'] = 'application/octet-stream'
98
+ end
99
+
100
+ response = http.request(request)
101
+ if response.code == '200'
102
+ log response.body, :debug
103
+ else
104
+ log '******** ERROR!! ********', :error
105
+ end
106
+ MultiJson.decode(response.body).with_indifferent_access
107
+ rescue
108
+ log_last_error
109
+ {} # return empty hash
110
+ end
111
+ end
112
+ end
@@ -1,3 +1,6 @@
1
+ Dumper::Dependency.load('thor')
2
+ Dumper::Dependency.load('rainbow')
3
+
1
4
  module Dumper
2
5
  class Cli < Thor
3
6
  include Thor::Actions
@@ -0,0 +1,9 @@
1
+ module Dumper
2
+ module Database
3
+ class Base
4
+ def initialize(stack)
5
+ @stack = stack
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ module Dumper
2
+ module Database
3
+ class MySQL < Base
4
+ def command
5
+ "mysqldump #{connection_options} #{additional_options} #{@stack.activerecord_config['database']} | gzip"
6
+ end
7
+
8
+ protected
9
+
10
+ def connection_options
11
+ [ :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')
14
+ end.compact.join(' ')
15
+ end
16
+
17
+ def additional_options
18
+ '--single-transaction'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module Dumper
2
+ class Dependency
3
+ LIBS = {
4
+ 'thor' => { :require => 'thor', :version => '~> 0.14.0' },
5
+ 'rainbow' => { :require => 'rainbow', :version => '~> 1.1.4' },
6
+ }
7
+
8
+ def self.load(name)
9
+ begin
10
+ gem(name, LIBS[name][:version])
11
+ require(LIBS[name][:require])
12
+ rescue LoadError
13
+ abort <<-EOS
14
+ Dependency missing: #{name}
15
+ To install the gem, issue the following command:
16
+
17
+ gem install #{name} -v '#{LIBS[name][:version]}'
18
+
19
+ Please try again after installing the missing dependency.
20
+ EOS
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,103 @@
1
+ require 'posix/spawn'
2
+
3
+ module Dumper
4
+ class Job
5
+ include POSIX::Spawn
6
+ include Dumper::Utility::LoggingMethods
7
+
8
+ def initialize(agent, job)
9
+ @agent = agent
10
+ @stack = agent.stack
11
+ @job = job
12
+ end
13
+
14
+ def run_and_exit
15
+ @job[:servers].each do |server|
16
+ perform(server)
17
+ end
18
+ ensure
19
+ log_last_error if $!
20
+ log 'exiting...'
21
+ exit
22
+ end
23
+
24
+ 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
+
29
+ case server[:type]
30
+ when 'mysql'
31
+ @database = Dumper::Database::MySQL.new(@stack)
32
+ else
33
+ abort 'invalid server type!' # TBD
34
+ end
35
+ backup_id = json[:backup][:id]
36
+ filename = json[:backup][:filename]
37
+
38
+ # Dump
39
+ start_at = Time.now
40
+ tempfile = ruby19? ? Tempfile.new(filename, encoding: 'ascii-8bit') : Tempfile.new(filename)
41
+ log 'starting backup...'
42
+ log "tempfile = #{tempfile.path}"
43
+ log "command = #{@database.command}"
44
+
45
+ begin
46
+ pid, stdin, stdout, stderr = popen4(@database.command)
47
+ 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
57
+ rescue
58
+ 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
61
+ ensure
62
+ [stdin, stdout, stderr].each{|io| io.close unless io.closed? }
63
+ Process.waitpid(pid)
64
+ end
65
+
66
+ tempfile.flush
67
+
68
+ dump_duration = Time.now - start_at
69
+ log "dump_duration = #{dump_duration}"
70
+
71
+ upload_to_s3(json[:url], json[:fields], tempfile.path, filename)
72
+
73
+ json = @agent.send_request(api: 'backup/commit', params: { backup_id: backup_id, dump_duration: dump_duration.to_i })
74
+ rescue
75
+ log_last_error
76
+ ensure
77
+ tempfile.close(true)
78
+ end
79
+
80
+ # Upload
81
+ def upload_to_s3(url, fields, local_file, remote_file)
82
+ require 'net/http/post/multipart'
83
+ fields['file'] = UploadIO.new(local_file, 'application/octet-stream', remote_file)
84
+ uri = URI.parse(url)
85
+ request = Net::HTTP::Post::Multipart.new uri.path, fields
86
+ http = Net::HTTP.new(uri.host, uri.port)
87
+ if uri.is_a? URI::HTTPS
88
+ http.use_ssl = true
89
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
90
+ end
91
+ response = http.request(request)
92
+ log "response from S3 = #{response.to_s}"
93
+ response
94
+ rescue
95
+ log_last_error
96
+ end
97
+
98
+ # Helper
99
+ def ruby19?
100
+ RUBY_VERSION >= '1.9.0'
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,69 @@
1
+ module Dumper
2
+ class Stack
3
+ attr_accessor :rails_env, :dispatcher, :framework, :rackup, :activerecord_config
4
+
5
+ def initialize
6
+ # Rackup?
7
+ @rackup = find_instance_in_object_space(Rack::Server)
8
+
9
+ # Rails?
10
+ if defined?(::Rails)
11
+ @framework = :rails
12
+ @rails_env = Rails.env.to_s
13
+ @rails_version = Rails::VERSION::STRING
14
+ @is_supported_rails_version = (::Rails::VERSION::MAJOR >= 3)
15
+ @activerecord_config = ActiveRecord::Base.configurations[@rails_env]
16
+ else
17
+ @framework = :ruby
18
+ end
19
+
20
+ # Which dispatcher?
21
+ [ :unicorn, :passenger, :thin, :mongrel, :webrick ].find do |name|
22
+ @dispatcher = send("#{name}?") ? name : nil
23
+ end
24
+ end
25
+
26
+ def to_hash
27
+ {
28
+ framework: @framework,
29
+ rails_env: @rails_env,
30
+ rails_version: @rails_version,
31
+ dispatcher: @dispatcher,
32
+ activerecord_config: @activerecord_config.reject{|k,v| k == 'password' },
33
+ }
34
+ end
35
+
36
+ # Compatibility
37
+ def supported?
38
+ @is_supported_rails_version && @dispatcher && %w(mysql mysql2).include?(@activerecord_config['adapter'])
39
+ end
40
+
41
+ # Dispatcher
42
+ def unicorn?
43
+ defined?(::Unicorn::HttpServer) && find_instance_in_object_space(::Unicorn::HttpServer)
44
+ end
45
+
46
+ def passenger?
47
+ defined?(::Passenger::AbstractServer) || defined?(::IN_PHUSION_PASSENGER)
48
+ end
49
+
50
+ def thin?
51
+ # defined?(::Thin::Server) && find_instance_in_object_space(Thin::Server)
52
+ @rackup and @rackup.server.to_s.demodulize == 'Thin'
53
+ end
54
+
55
+ def mongrel?
56
+ # defined?(::Mongrel::HttpServer)
57
+ @rackup and @rackup.server.to_s.demodulize == 'Mongrel'
58
+ end
59
+
60
+ def webrick?
61
+ # defined?(::WEBrick::VERSION)
62
+ @rackup and @rackup.server.to_s.demodulize == 'WEBrick'
63
+ end
64
+
65
+ def find_instance_in_object_space(klass)
66
+ ObjectSpace.each_object(klass).first
67
+ end
68
+ end
69
+ end
@@ -1,4 +1,5 @@
1
1
  require 'ipaddr'
2
+ require 'logger'
2
3
 
3
4
  module Dumper
4
5
  module Utility
@@ -35,5 +36,37 @@ module Dumper
35
36
  IPAddr.new("192.168.0.0/16") ].any?{|i| i.include? self }
36
37
  end
37
38
  end
39
+
40
+ class SlimLogger < ::Logger
41
+ def initialize(logdev, shift_age = 0, shift_size = 1048576)
42
+ super
43
+ self.formatter = SlimFormatter.new
44
+ self.formatter.datetime_format = "%Y-%m-%dT%H:%M:%S"
45
+ self.level = Logger::INFO
46
+ end
47
+
48
+ class SlimFormatter < ::Logger::Formatter
49
+ # This method is invoked when a log event occurs
50
+ def call(severity, time, progname, msg)
51
+ "[%s] %5s : %s\n" % [format_datetime(time), severity, msg2str(msg)]
52
+ end
53
+ end
54
+ end
55
+
56
+ module LoggingMethods
57
+ def logger
58
+ @logger ||= Dumper::Utility::SlimLogger.new("#{Rails.root}/log/dumper_agent.log", 1, 10.megabytes)
59
+ end
60
+
61
+ def log(msg, level=:info)
62
+ STDOUT.puts "** [Dumper] " + msg
63
+ return unless true #should_log?
64
+ logger.send level, msg
65
+ end
66
+
67
+ def log_last_error
68
+ log [ $!.class.name, $!.to_s ].join(', ')
69
+ end
70
+ end
38
71
  end
39
72
  end
@@ -1,3 +1,3 @@
1
1
  module Dumper
2
- VERSION = "0.0.3"
2
+ VERSION = '0.0.4'
3
3
  end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dumper do
4
+ it 'initializes' do
5
+ Dumper::Agent.respond_to?(:start).should be_true
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'dumper'
5
+
6
+ RSpec.configure do |config|
7
+ 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.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,17 +9,49 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-29 00:00:00.000000000 Z
12
+ date: 2012-05-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: thor
15
+ name: multi_json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: posix-spawn
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.3.6
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.3.6
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
16
48
  requirement: !ruby/object:Gem::Requirement
17
49
  none: false
18
50
  requirements:
19
51
  - - ! '>='
20
52
  - !ruby/object:Gem::Version
21
53
  version: '0'
22
- type: :runtime
54
+ type: :development
23
55
  prerelease: false
24
56
  version_requirements: !ruby/object:Gem::Requirement
25
57
  none: false
@@ -28,14 +60,14 @@ dependencies:
28
60
  - !ruby/object:Gem::Version
29
61
  version: '0'
30
62
  - !ruby/object:Gem::Dependency
31
- name: rainbow
63
+ name: rake
32
64
  requirement: !ruby/object:Gem::Requirement
33
65
  none: false
34
66
  requirements:
35
67
  - - ! '>='
36
68
  - !ruby/object:Gem::Version
37
69
  version: '0'
38
- type: :runtime
70
+ type: :development
39
71
  prerelease: false
40
72
  version_requirements: !ruby/object:Gem::Requirement
41
73
  none: false
@@ -43,7 +75,8 @@ dependencies:
43
75
  - - ! '>='
44
76
  - !ruby/object:Gem::Version
45
77
  version: '0'
46
- description: Utility that checks the status of a database
78
+ description: Dumper is a backup management system that offers a whole new way to take
79
+ daily backups of your databases. (coming soon!)
47
80
  email:
48
81
  - kenn.ejima@gmail.com
49
82
  executables:
@@ -52,6 +85,7 @@ extensions: []
52
85
  extra_rdoc_files: []
53
86
  files:
54
87
  - .gitignore
88
+ - .rspec
55
89
  - Gemfile
56
90
  - LICENSE
57
91
  - README.md
@@ -59,10 +93,18 @@ files:
59
93
  - bin/dumper
60
94
  - dumper.gemspec
61
95
  - lib/dumper.rb
96
+ - lib/dumper/agent.rb
62
97
  - lib/dumper/cli.rb
98
+ - lib/dumper/database/base.rb
99
+ - lib/dumper/database/mysql.rb
100
+ - lib/dumper/dependency.rb
101
+ - lib/dumper/job.rb
102
+ - lib/dumper/stack.rb
63
103
  - lib/dumper/utility.rb
64
104
  - lib/dumper/version.rb
65
- homepage: ''
105
+ - spec/dumper_spec.rb
106
+ - spec/spec_helper.rb
107
+ homepage: https://github.com/kenn/dumper
66
108
  licenses: []
67
109
  post_install_message:
68
110
  rdoc_options: []
@@ -85,6 +127,8 @@ rubyforge_project:
85
127
  rubygems_version: 1.8.19
86
128
  signing_key:
87
129
  specification_version: 3
88
- summary: Utility that checks the status of a database
89
- test_files: []
130
+ summary: The Dumper Agent for Rails (coming soon!)
131
+ test_files:
132
+ - spec/dumper_spec.rb
133
+ - spec/spec_helper.rb
90
134
  has_rdoc: