db_sucker 3.0.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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/CHANGELOG.md +45 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +193 -0
  7. data/Rakefile +1 -0
  8. data/VERSION +1 -0
  9. data/bin/db_sucker +12 -0
  10. data/bin/db_sucker.sh +14 -0
  11. data/db_sucker.gemspec +29 -0
  12. data/doc/config_example.rb +53 -0
  13. data/doc/container_example.yml +150 -0
  14. data/lib/db_sucker/adapters/mysql2.rb +103 -0
  15. data/lib/db_sucker/application/colorize.rb +28 -0
  16. data/lib/db_sucker/application/container/accessors.rb +60 -0
  17. data/lib/db_sucker/application/container/ssh.rb +225 -0
  18. data/lib/db_sucker/application/container/validations.rb +53 -0
  19. data/lib/db_sucker/application/container/variation/accessors.rb +45 -0
  20. data/lib/db_sucker/application/container/variation/helpers.rb +21 -0
  21. data/lib/db_sucker/application/container/variation/worker_api.rb +65 -0
  22. data/lib/db_sucker/application/container/variation.rb +60 -0
  23. data/lib/db_sucker/application/container.rb +70 -0
  24. data/lib/db_sucker/application/container_collection.rb +47 -0
  25. data/lib/db_sucker/application/core.rb +222 -0
  26. data/lib/db_sucker/application/dispatch.rb +364 -0
  27. data/lib/db_sucker/application/evented_resultset.rb +149 -0
  28. data/lib/db_sucker/application/fake_channel.rb +22 -0
  29. data/lib/db_sucker/application/output_helper.rb +197 -0
  30. data/lib/db_sucker/application/sklaven_treiber/log_spool.rb +57 -0
  31. data/lib/db_sucker/application/sklaven_treiber/worker/accessors.rb +105 -0
  32. data/lib/db_sucker/application/sklaven_treiber/worker/core.rb +168 -0
  33. data/lib/db_sucker/application/sklaven_treiber/worker/helpers.rb +144 -0
  34. data/lib/db_sucker/application/sklaven_treiber/worker/io/base.rb +240 -0
  35. data/lib/db_sucker/application/sklaven_treiber/worker/io/file_copy.rb +81 -0
  36. data/lib/db_sucker/application/sklaven_treiber/worker/io/file_gunzip.rb +58 -0
  37. data/lib/db_sucker/application/sklaven_treiber/worker/io/file_import_sql.rb +80 -0
  38. data/lib/db_sucker/application/sklaven_treiber/worker/io/file_shasum.rb +49 -0
  39. data/lib/db_sucker/application/sklaven_treiber/worker/io/pv_wrapper.rb +73 -0
  40. data/lib/db_sucker/application/sklaven_treiber/worker/io/sftp_download.rb +57 -0
  41. data/lib/db_sucker/application/sklaven_treiber/worker/io/throughput.rb +219 -0
  42. data/lib/db_sucker/application/sklaven_treiber/worker/routines.rb +313 -0
  43. data/lib/db_sucker/application/sklaven_treiber/worker.rb +48 -0
  44. data/lib/db_sucker/application/sklaven_treiber.rb +281 -0
  45. data/lib/db_sucker/application/slot_pool.rb +137 -0
  46. data/lib/db_sucker/application/tie.rb +25 -0
  47. data/lib/db_sucker/application/window/core.rb +185 -0
  48. data/lib/db_sucker/application/window/dialog.rb +142 -0
  49. data/lib/db_sucker/application/window/keypad/core.rb +85 -0
  50. data/lib/db_sucker/application/window/keypad.rb +174 -0
  51. data/lib/db_sucker/application/window/prompt.rb +124 -0
  52. data/lib/db_sucker/application/window.rb +329 -0
  53. data/lib/db_sucker/application.rb +168 -0
  54. data/lib/db_sucker/patches/beta-warning.rb +374 -0
  55. data/lib/db_sucker/patches/developer.rb +29 -0
  56. data/lib/db_sucker/patches/net-sftp.rb +20 -0
  57. data/lib/db_sucker/patches/thread-count.rb +30 -0
  58. data/lib/db_sucker/version.rb +4 -0
  59. data/lib/db_sucker.rb +81 -0
  60. metadata +217 -0
@@ -0,0 +1,28 @@
1
+ module DbSucker
2
+ class Application
3
+ module Colorize
4
+ UnknownColorError = Class.new(::ArgumentError)
5
+ COLORMAP = {
6
+ black: 30,
7
+ gray: 30,
8
+ red: 31,
9
+ green: 32,
10
+ yellow: 33,
11
+ blue: 34,
12
+ magenta: 35,
13
+ cyan: 36,
14
+ white: 37,
15
+ }
16
+
17
+ def colorize str, color = :yellow
18
+ ccode = COLORMAP[color.to_sym] || raise(UnknownColorError, "unknown color `#{color}'")
19
+ @opts[:colorize] ? "\e[#{ccode}m#{str}\e[0m" : "#{str}"
20
+ end
21
+ alias_method :c, :colorize
22
+
23
+ def decolorize str
24
+ str.to_s.gsub(/\e\[.*?(\d)+m/ , '')
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,60 @@
1
+ module DbSucker
2
+ class Application
3
+ class Container
4
+ module Accessors
5
+ def ctn
6
+ self
7
+ end
8
+
9
+ def source
10
+ ctn.data["source"]
11
+ end
12
+
13
+ def tmp_path
14
+ source["ssh"]["tmp_location"].presence || "/tmp/db_sucker_tmp"
15
+ end
16
+
17
+ def variations
18
+ Hash[data["variations"].keys.map{|id| [id, variation(id)] }]
19
+ end
20
+
21
+ def variation v
22
+ return unless vd = data["variations"][v]
23
+ Variation.new(self, v, vd)
24
+ end
25
+
26
+ def integrity_binary
27
+ (source["integrity_binary"].nil? ? "shasum -ba" : source["integrity_binary"]).presence
28
+ end
29
+
30
+ def integrity_sha
31
+ source["integrity_sha"].nil? ? 512 : source["integrity_sha"]
32
+ end
33
+
34
+ def integrity?
35
+ !!integrity_sha
36
+ end
37
+
38
+ def ssh_key_files
39
+ @ssh_key_files ||= begin
40
+ files = [*data["source"]["ssh"]["keyfile"]].reject(&:blank?).map do |f|
41
+ if f.start_with?("~")
42
+ Pathname.new(File.expand_path(f))
43
+ else
44
+ Pathname.new(File.dirname(src)).join(f)
45
+ end
46
+ end
47
+ files.each do |f|
48
+ begin
49
+ File.open(f)
50
+ rescue Errno::ENOENT
51
+ warning("SSH identity file `#{f}' for identifier `#{name}' does not exist! (in `#{src}')")
52
+ end
53
+ end
54
+ files
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,225 @@
1
+ module DbSucker
2
+ class Application
3
+ class Container
4
+ module SSH
5
+ CommandExecutionError = Class.new(::RuntimeError)
6
+ ChannelOpenFailedError = Class.new(::RuntimeError)
7
+
8
+ begin # SSH
9
+ def ssh_begin
10
+ debug "Opening SSH connection for identifier `#{name}'"
11
+ @ssh = ssh_start
12
+ begin
13
+ yield(@ssh)
14
+ ensure
15
+ ssh_end
16
+ end if block_given?
17
+ end
18
+
19
+ def ssh_end
20
+ return unless @ssh
21
+ debug "Closing SSH connection for identifier `#{name}'"
22
+ @ssh.try(:close) rescue false
23
+ debug "CLOSED SSH connection for identifier `#{name}'"
24
+ @ssh = nil
25
+ end
26
+
27
+ def ssh_sync &block
28
+ @ssh_mutex.synchronize(&block)
29
+ end
30
+
31
+ def ssh_start new_connection = false, &block
32
+ if @ssh && !new_connection
33
+ ssh_sync do
34
+ debug "Reusing SSH connection in start for identifier `#{name}'"
35
+ return block ? block.call(@ssh) : @ssh
36
+ end
37
+ end
38
+ debug "Opening new SSH connection in start for identifier `#{name}'"
39
+
40
+ opt = {}
41
+ opt[:user] = source["ssh"]["username"] if source["ssh"]["username"].present?
42
+ opt[:password] = source["ssh"]["password"] if source["ssh"]["password"].present?
43
+ opt[:keys] = ssh_key_files if ssh_key_files.any?
44
+ opt[:port] = source["ssh"]["port"] if source["ssh"]["port"].present?
45
+ if block
46
+ Net::SSH.start(source["ssh"]["hostname"], nil, opt) do |ssh|
47
+ block.call(ssh)
48
+ end
49
+ else
50
+ Net::SSH.start(source["ssh"]["hostname"], nil, opt)
51
+ end
52
+ end
53
+ end
54
+
55
+ begin # SFTP
56
+ def sftp_begin
57
+ debug "Opening SFTP connection for identifier `#{name}'"
58
+ @sftp = sftp_start
59
+ begin
60
+ yield(@sftp)
61
+ ensure
62
+ sftp_end
63
+ end if block_given?
64
+ end
65
+
66
+ def sftp_end
67
+ return unless @sftp
68
+ debug "Closing SFTP connection for identifier `#{name}'"
69
+ @sftp.try(:close) rescue false
70
+ debug "CLOSED SFTP connection for identifier `#{name}'"
71
+ @sftp = nil
72
+ end
73
+
74
+ def sftp_sync &block
75
+ @sftp_mutex.synchronize(&block)
76
+ end
77
+
78
+ def sftp_start new_connection = false, &block
79
+ if @sftp && !new_connection
80
+ sftp_sync do
81
+ debug "Reusing SFTP connection in start for identifier `#{name}'"
82
+ return block ? block.call(@sftp) : @sftp
83
+ end
84
+ end
85
+ debug "Opening new SFTP connection in start for identifier `#{name}'"
86
+
87
+ opt = {}
88
+ opt[:user] = source["ssh"]["username"] if source["ssh"]["username"].present?
89
+ opt[:password] = source["ssh"]["password"] if source["ssh"]["password"].present?
90
+ opt[:keys] = ssh_key_files if ssh_key_files.any?
91
+ opt[:port] = source["ssh"]["port"] if source["ssh"]["port"].present?
92
+ if block
93
+ Net::SFTP.start(source["ssh"]["hostname"], nil, opt) do |sftp|
94
+ block.call(sftp)
95
+ end
96
+ else
97
+ Net::SFTP.start(source["ssh"]["hostname"], nil, opt)
98
+ end
99
+ end
100
+ end
101
+
102
+ begin # SSH helpers
103
+ def loop_ssh *args, &block
104
+ return false unless @ssh
105
+ @ssh.loop(*args, &block)
106
+ end
107
+
108
+ def blocking_channel ssh = nil, result = nil, &block
109
+ waitlock = Queue.new
110
+ (ssh || ssh_start).open_channel do |ch|
111
+ waitlock.pop
112
+ block.call(ch)
113
+ end.tap do |ch|
114
+ # catch open_fail errors
115
+ ch.on_open_failed do |_ch, code, desc|
116
+ result.try(:close!)
117
+ _ch[:open_failed] = true
118
+ raise ChannelOpenFailedError, "#{code}: #{desc}"
119
+ end
120
+
121
+ ch[:wait_monitor] = Monitor.new
122
+ ch[:wait_condition] = ch[:wait_monitor].new_cond
123
+ st = app.sklaventreiber
124
+ waitlock << true
125
+ if !ssh && st && st.sync{ st.try(:poll) }
126
+ ch[:wait_monitor].synchronize do
127
+ ch[:wait_condition].wait(0.1) while ch.active?
128
+ end
129
+ else
130
+ ch.wait
131
+ end
132
+ end
133
+ end
134
+
135
+ def nonblocking_channel ssh = nil, result = nil, &block
136
+ (ssh || ssh_start).open_channel do |ch|
137
+ ch[:wait_monitor] = Monitor.new
138
+ ch[:wait_condition] = ch[:wait_monitor].new_cond
139
+ block.call(ch)
140
+ end.tap do |ch|
141
+ # catch open_fail errors
142
+ ch.on_open_failed do |_ch, code, desc|
143
+ result.try(:close!)
144
+ _ch[:open_failed] = true
145
+ raise ChannelOpenFailedError, "#{code}: #{desc}"
146
+ end
147
+ end
148
+ end
149
+
150
+ def kill_remote_process pid, sig = :INT
151
+ ssh_start(true) do |ssh|
152
+ blocking_channel_result("kill -#{sig} -#{pid}", ssh: ssh)
153
+ end
154
+ end
155
+
156
+ def blocking_channel_result cmd, opts = {}
157
+ opts = opts.reverse_merge(ssh: nil, blocking: true, channel: false, request_pty: false, use_sh: false)
158
+ if opts[:use_sh]
159
+ cmd = %{/bin/sh -c 'echo $(ps -o pgid= $$ | grep -o [0-9]*) && #{cmd}'}
160
+ pid_monitor = Monitor.new
161
+ pid_signal = pid_monitor.new_cond
162
+ end
163
+ result = EventedResultset.new
164
+ chan = send(opts[:blocking] ? :blocking_channel : :nonblocking_channel, opts[:ssh], result) do |ch|
165
+ chproc = ->(ch, cmd, result) {
166
+ ch.exec(cmd) do |ch, success|
167
+ Thread.main[:app].debug "START: #{Thread.current == Thread.main ? :main : Thread.current[:itype]}-#{cmd} #{success}"
168
+ raise CommandExecutionError, "could not execute command" unless success
169
+
170
+ # "on_data" is called when the process writes something to stdout
171
+ ch.on_data do |c, data|
172
+ Thread.main[:app].debug "#{Thread.current == Thread.main ? :main : Thread.current[:itype]}-#{Time.current.to_f}: STDOUT: #{data}".chomp
173
+ if opts[:use_sh] && result.empty?
174
+ ch[:pid] = data.to_i
175
+ ch[:pid] = false if ch[:pid].zero?
176
+ pid_monitor.synchronize { pid_signal.broadcast } if opts[:use_sh]
177
+ next
178
+ end
179
+ result.enq(data, :stdout)
180
+ end
181
+
182
+ # "on_extended_data" is called when the process writes something to stderr
183
+ ch.on_extended_data do |c, type, data|
184
+ Thread.main[:app].debug "#{Thread.current == Thread.main ? :main : Thread.current[:itype]}-#{Time.current.to_f}: STDERR: #{data}".chomp
185
+ result.enq(data, :stderr)
186
+ end
187
+
188
+ ch.on_request "exit-status" do |ch, data|
189
+ Thread.main[:app].debug "#{Thread.current == Thread.main ? :main : Thread.current[:itype]}-#{Time.current.to_f}: EXIT: #{data.read_long} #{cmd}".chomp
190
+ end
191
+
192
+ ch.on_close do |ch|
193
+ ch[:wait_monitor].synchronize { ch[:wait_condition].broadcast }
194
+ Thread.main[:app].debug "#{Thread.current == Thread.main ? :main : Thread.current[:itype]}-#{Time.current.to_f}: CLOSED: #{cmd}".chomp
195
+ end
196
+
197
+ ch.on_eof do
198
+ Thread.main[:app].debug "#{Thread.current == Thread.main ? :main : Thread.current[:itype]}-#{Time.current.to_f}: EOF: #{cmd}".chomp
199
+ result.close!
200
+ ch[:handler].try(:signal)
201
+ end
202
+ end
203
+ }
204
+ if opts[:request_pty]
205
+ ch.request_pty do |ch, success|
206
+ raise CommandExecutionError, "could not obtain pty" unless success
207
+ ch[:pty] = true
208
+ chproc.call(ch, cmd, result)
209
+ end
210
+ else
211
+ chproc.call(ch, cmd, result)
212
+ end
213
+ end
214
+ pid_monitor.synchronize { pid_signal.wait(1) if !chan[:pid] } if opts[:use_sh]
215
+ opts[:channel] ? [chan, result] : result
216
+ end
217
+
218
+ def nonblocking_channel_result cmd, opts = {}
219
+ blocking_channel_result(cmd, opts.merge(blocking: false))
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,53 @@
1
+ module DbSucker
2
+ class Application
3
+ class Container
4
+ module Validations
5
+ def verify!
6
+ _verify("/", data, __keys_for(:root))
7
+
8
+ # validate source
9
+ if sd = data["source"]
10
+ _verify("/source", sd, __keys_for(:source))
11
+ if sd["ssh"]
12
+ _verify("/source/ssh", sd["ssh"], __keys_for(:source_ssh))
13
+ ssh_key_files
14
+ end
15
+ end
16
+
17
+ # validate variations
18
+ if sd = data["variations"]
19
+ sd.each do |name, vd|
20
+ begin
21
+ _verify("/variations/#{name}", vd, __keys_for(:variation))
22
+ base = sd[vd["base"]] if vd["base"]
23
+ raise(ConfigurationError, "variation `#{name}' cannot base from `#{vd["base"]}' since it doesn't exist (in `#{src}')") if vd["base"] && !base
24
+ raise ConfigurationError, "variation `#{name}' must define an adapter (mysql2, postgres, ...)" if vd["adapter"].blank? && vd["database"] != false && (!base || base["adapter"].blank?)
25
+ rescue ConfigurationError => ex
26
+ abort "#{ex.message} (in `#{src}' [/variations/#{name}])"
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def _verify token, hash, keys
33
+ begin
34
+ hash.assert_valid_keys(keys)
35
+ raise ConfigurationError, "A source must define an adapter (mysql2, postgres, ...)" if token == "/source" && hash["adapter"].blank?
36
+ raise ConfigurationError, "A variation `#{name}' can only define either a `only' or `except' option" if hash["only"] && hash["except"]
37
+ rescue ConfigurationError, ArgumentError => ex
38
+ abort "#{ex.message} (in `#{src}' [#{token}])"
39
+ end
40
+ end
41
+
42
+ def __keys_for which
43
+ {
44
+ root: %w[source variations],
45
+ source: %w[adapter ssh database hostname username password args client_binary dump_binary gzip_binary integrity_sha integrity_binary],
46
+ source_ssh: %w[hostname username keyfile password port tmp_location],
47
+ variation: %w[adapter label base database hostname username password args client_binary incremental file only except importer importer_flags ignore_always constraints],
48
+ }[which] || []
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ module DbSucker
2
+ class Application
3
+ class Container
4
+ class Variation
5
+ module Accessors
6
+ def ctn
7
+ cfg
8
+ end
9
+
10
+ def source
11
+ ctn.source
12
+ end
13
+
14
+ def label
15
+ data["label"]
16
+ end
17
+
18
+ def incrementals
19
+ data["incremental"] || {}
20
+ end
21
+
22
+ def gzip_binary
23
+ source["gzip_binary"] || "gzip"
24
+ end
25
+
26
+ def copies_file?
27
+ data["file"]
28
+ end
29
+
30
+ def copies_file_compressed?
31
+ copies_file? && data["file"].end_with?(".gz")
32
+ end
33
+
34
+ def requires_uncompression?
35
+ !copies_file_compressed? || data["database"]
36
+ end
37
+
38
+ def constraint table
39
+ data["constraints"] && (data["constraints"][table] || data["constraints"]["__default"])
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ module DbSucker
2
+ class Application
3
+ class Container
4
+ class Variation
5
+ module Helpers
6
+ def parse_flags flags
7
+ flags.to_s.split(" ").map(&:strip).reject(&:blank?).each_with_object({}) do |fstr, res|
8
+ if m = fstr.match(/\+(?<key>[^=]+)(?:=(?<value>))?/)
9
+ res[m[:key].strip] = m[:value].nil? ? true : m[:value]
10
+ elsif m = fstr.match(/\-(?<key>[^=]+)/)
11
+ res[m[:key]] = false
12
+ else
13
+ raise InvalidImporterFlagError, "invalid flag `#{fstr}' for variation `#{cfg.name}/#{name}' (in `#{cfg.src}')"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,65 @@
1
+ module DbSucker
2
+ class Application
3
+ class Container
4
+ class Variation
5
+ module WorkerApi
6
+ def tables_to_transfer
7
+ all = cfg.table_list(cfg.data["source"]["database"]).map(&:first)
8
+ keep = []
9
+ if data["only"]
10
+ [*data["only"]].each do |t|
11
+ unless all.include?(t)
12
+ raise TableNotFoundError, "table `#{t}' for the database `#{cfg.source["database"]}' could not be found (provided by variation `#{cfg.name}/#{name}' in `#{cfg.src}')"
13
+ end
14
+ keep << t
15
+ end
16
+ elsif data["except"]
17
+ keep = all.dup
18
+ [*data["except"]].each do |t|
19
+ unless all.include?(t)
20
+ raise TableNotFoundError, "table `#{t}' for the database `#{cfg.source["database"]}' could not be found (provided by variation `#{cfg.name}/#{name}' in `#{cfg.src}')"
21
+ end
22
+ keep.delete(t)
23
+ end
24
+ else
25
+ keep = all.dup
26
+ end
27
+ keep -= data["ignore_always"] if data["ignore_always"].is_a?(Array)
28
+
29
+ [keep, all]
30
+ end
31
+
32
+ def dump_to_remote_command worker, pv_binary = false
33
+ tmpfile = worker.tmp_filename(true)
34
+ cmd = dump_command_for(worker.table)
35
+ if pv_binary.presence
36
+ cmd << %{ | #{pv_binary} -n -b > #{tmpfile}}
37
+ else
38
+ cmd << %{ > #{tmpfile}}
39
+ end
40
+ [tmpfile, cmd]
41
+ end
42
+
43
+ def dump_to_remote worker, blocking = true
44
+ tfile, cmd = dump_to_remote_command(worker)
45
+ [tfile, cfg.blocking_channel_result(cmd, channel: true, use_sh: true, blocking: blocking)]
46
+ end
47
+
48
+ def compress_file_command file, pv_binary = false
49
+ if pv_binary.presence
50
+ cmd = %{#{pv_binary} -n -b #{file} | #{gzip_binary} > #{file}.gz && rm #{file} }
51
+ else
52
+ cmd = %{#{gzip_binary} #{file}}
53
+ end
54
+ ["#{file}.gz", cmd]
55
+ end
56
+
57
+ def compress_file file, blocking = true
58
+ nfile, cmd = compress_file_command(file)
59
+ [nfile, cfg.blocking_channel_result(cmd, channel: true, use_sh: true, blocking: blocking)]
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,60 @@
1
+ module DbSucker
2
+ class Application
3
+ class Container
4
+ class Variation
5
+ ImporterNotFoundError = Class.new(::RuntimeError)
6
+ InvalidImporterFlagError = Class.new(::RuntimeError)
7
+
8
+ include Accessors
9
+ include Helpers
10
+ include WorkerApi
11
+
12
+ attr_reader :cfg, :name, :data
13
+
14
+ def initialize cfg, name, data
15
+ @cfg, @name, @data = cfg, name, data
16
+
17
+ if data["base"]
18
+ bdata = cfg.variation(data["base"]) || raise(ConfigurationError, "variation `#{cfg.name}/#{name}' cannot base from `#{data["base"]}' since it doesn't exist (in `#{cfg.src}')")
19
+ @data = data.reverse_merge(bdata.data)
20
+ end
21
+
22
+ if @data["adapter"]
23
+ begin
24
+ adapter = "DbSucker::Adapters::#{@data["adapter"].camelize}::Api".constantize
25
+ @cfg.app.sync { adapter.require_dependencies }
26
+ extend adapter
27
+ rescue NameError => ex
28
+ raise(AdapterNotFoundError, "variation `#{cfg.name}/#{name}' defines invalid adapter `#{@data["adapter"]}' (in `#{cfg.src}'): #{ex.message}", ex.backtrace)
29
+ end
30
+ elsif @data["database"]
31
+ raise(ConfigurationError, "variation `#{cfg.name}/#{name}' must define an adapter (mysql2, postgres, ...) if database is provided (in `#{cfg.src}')")
32
+ end
33
+ end
34
+
35
+ # ===============
36
+ # = Adapter API =
37
+ # ===============
38
+ [
39
+ :client_binary,
40
+ :local_client_binary,
41
+ :dump_binary,
42
+ :client_call,
43
+ :local_client_call,
44
+ :dump_call,
45
+ :database_list,
46
+ :table_list,
47
+ :hostname,
48
+ ].each do |meth|
49
+ define_method meth do
50
+ raise NotImplementedError, "your selected adapter `#{@data["adapter"]}' must implement `##{meth}' for variation `#{cfg.name}/#{name}' (in `#{cfg.src}')"
51
+ end
52
+ end
53
+
54
+ def dump_command_for table
55
+ raise NotImplementedError, "your selected adapter `#{@data["adapter"]}' must implement `#dump_command_for(table)' for variation `#{cfg.name}/#{name}' (in `#{cfg.src}')"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,70 @@
1
+ module DbSucker
2
+ class Application
3
+ class Container
4
+ AdapterNotFoundError = Class.new(::ArgumentError)
5
+ TableNotFoundError = Class.new(::RuntimeError)
6
+ ConfigurationError = Class.new(::ArgumentError)
7
+
8
+ include Accessors
9
+ include Validations
10
+ include SSH
11
+ OutputHelper.hook(self)
12
+
13
+ attr_reader :app, :name, :src, :data
14
+
15
+ def initialize app, name, src, data
16
+ @app = app
17
+ @name = name
18
+ @src = src
19
+ @data = data
20
+ @ssh_mutex = Monitor.new
21
+ @sftp_mutex = Monitor.new
22
+
23
+ verify!
24
+
25
+ begin
26
+ adapter = "DbSucker::Adapters::#{source["adapter"].camelize}::Api".constantize
27
+ app.sync { adapter.require_dependencies }
28
+ extend adapter
29
+ rescue NameError => ex
30
+ raise(AdapterNotFoundError, "identifier `#{name}' defines invalid source adapter `#{source["adapter"]}' (in `#{@src}'): #{ex.message}", ex.backtrace)
31
+ end
32
+ end
33
+
34
+ def pv_utility
35
+ app.sync do
36
+ if @_pv_utility.nil?
37
+ if app.opts[:pv_enabled]
38
+ res = blocking_channel_result("which pv && pv --version")
39
+ app.debug "#{Time.current.to_f}: #{res.to_a.inspect}"
40
+ if m = res[1].to_s.match(/pv\s([0-9\.]+)\s/i)
41
+ if Gem::Version.new(m[1]) >= Gem::Version.new("1.3.8")
42
+ @_pv_utility = res[0].strip.presence
43
+ end
44
+ end
45
+ end
46
+ @_pv_utility = false unless @_pv_utility
47
+ end
48
+ @_pv_utility
49
+ end
50
+ rescue Errno::ECONNRESET
51
+ end
52
+
53
+ def calculate_remote_integrity_hash_command file, pv_binary = false
54
+ return unless integrity?
55
+ icmd = "#{integrity_binary}#{integrity_sha}"
56
+ if pv_binary.presence
57
+ %{#{pv_binary} -n -b #{file} | #{icmd}}
58
+ else
59
+ %{#{icmd} #{file}}
60
+ end
61
+ end
62
+
63
+ def calculate_remote_integrity_hash file, blocking = true
64
+ cmd = calculate_remote_integrity_hash_command(file)
65
+ return unless cmd
66
+ [cmd, blocking_channel_result(cmd, channel: true, use_sh: true, blocking: blocking)]
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,47 @@
1
+ module DbSucker
2
+ class Application
3
+ class ContainerCollection
4
+ DuplicateIdentifierError = Class.new(::ArgumentError)
5
+ YAMLParseError = Class.new(::RuntimeError)
6
+
7
+ attr_reader :app, :data
8
+
9
+ def initialize app
10
+ @app = app
11
+ @data = {}
12
+ end
13
+
14
+ def yml_configs disabled = false
15
+ files = Dir["#{app.core_cfg_path}/**/*.yml"].select{|f| File.file?(f) }
16
+ return files if disabled
17
+ files.reject do |f|
18
+ f.gsub("#{app.core_cfg_path}/", "").split("/").any?{|fp| fp.start_with?("__") }
19
+ end
20
+ end
21
+
22
+ def load_all_configs
23
+ yml_configs.each{|f| load_yml_config(f) }
24
+ end
25
+
26
+ def load_yml_config file
27
+ YAML.load_file(file).each do |id, cfg|
28
+ if @data.key?(id)
29
+ raise DuplicateIdentifierError, "double use of identifier `#{id}' in `#{file}'"
30
+ else
31
+ @data[id] = Container.new(app, id, file, cfg)
32
+ end
33
+ end
34
+ rescue Psych::SyntaxError => ex
35
+ app.abort ex.message
36
+ end
37
+
38
+ def get id
39
+ @data[id]
40
+ end
41
+
42
+ def each &block
43
+ @data.each(&block)
44
+ end
45
+ end
46
+ end
47
+ end