dumper 0.0.3 → 0.0.4

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